Merge remote-tracking branch 'upstream/dev' into playwright

# Conflicts:
#	backend/open_webui/retrieval/web/utils.py
#	backend/open_webui/routers/retrieval.py
This commit is contained in:
Rory 2025-02-17 21:53:39 -06:00
commit 10e0c81de9
226 changed files with 3402 additions and 1802 deletions

View file

@ -2,6 +2,8 @@ import json
import logging import logging
import os import os
import shutil import shutil
import base64
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Generic, Optional, TypeVar from typing import Generic, Optional, TypeVar
@ -586,6 +588,20 @@ load_oauth_providers()
STATIC_DIR = Path(os.getenv("STATIC_DIR", OPEN_WEBUI_DIR / "static")).resolve() STATIC_DIR = Path(os.getenv("STATIC_DIR", OPEN_WEBUI_DIR / "static")).resolve()
def override_static(path: str, content: str):
# Ensure path is safe
if "/" in path or ".." in path:
log.error(f"Invalid path: {path}")
return
file_path = os.path.join(STATIC_DIR, path)
os.makedirs(os.path.dirname(file_path), exist_ok=True)
with open(file_path, "wb") as f:
f.write(base64.b64decode(content)) # Convert Base64 back to raw binary
frontend_favicon = FRONTEND_BUILD_DIR / "static" / "favicon.png" frontend_favicon = FRONTEND_BUILD_DIR / "static" / "favicon.png"
if frontend_favicon.exists(): if frontend_favicon.exists():
@ -593,8 +609,6 @@ if frontend_favicon.exists():
shutil.copyfile(frontend_favicon, STATIC_DIR / "favicon.png") shutil.copyfile(frontend_favicon, STATIC_DIR / "favicon.png")
except Exception as e: except Exception as e:
logging.error(f"An error occurred: {e}") logging.error(f"An error occurred: {e}")
else:
logging.warning(f"Frontend favicon not found at {frontend_favicon}")
frontend_splash = FRONTEND_BUILD_DIR / "static" / "splash.png" frontend_splash = FRONTEND_BUILD_DIR / "static" / "splash.png"
@ -603,12 +617,18 @@ if frontend_splash.exists():
shutil.copyfile(frontend_splash, STATIC_DIR / "splash.png") shutil.copyfile(frontend_splash, STATIC_DIR / "splash.png")
except Exception as e: except Exception as e:
logging.error(f"An error occurred: {e}") logging.error(f"An error occurred: {e}")
else:
logging.warning(f"Frontend splash not found at {frontend_splash}") frontend_loader = FRONTEND_BUILD_DIR / "static" / "loader.js"
if frontend_loader.exists():
try:
shutil.copyfile(frontend_loader, STATIC_DIR / "loader.js")
except Exception as e:
logging.error(f"An error occurred: {e}")
#################################### ####################################
# CUSTOM_NAME # CUSTOM_NAME (Legacy)
#################################### ####################################
CUSTOM_NAME = os.environ.get("CUSTOM_NAME", "") CUSTOM_NAME = os.environ.get("CUSTOM_NAME", "")
@ -650,6 +670,16 @@ if CUSTOM_NAME:
pass pass
####################################
# LICENSE_KEY
####################################
LICENSE_KEY = PersistentConfig(
"LICENSE_KEY",
"license.key",
os.environ.get("LICENSE_KEY", ""),
)
#################################### ####################################
# STORAGE PROVIDER # STORAGE PROVIDER
#################################### ####################################
@ -1347,6 +1377,39 @@ Responses from models: {{responses}}"""
# Code Interpreter # Code Interpreter
#################################### ####################################
CODE_EXECUTION_ENGINE = PersistentConfig(
"CODE_EXECUTION_ENGINE",
"code_execution.engine",
os.environ.get("CODE_EXECUTION_ENGINE", "pyodide"),
)
CODE_EXECUTION_JUPYTER_URL = PersistentConfig(
"CODE_EXECUTION_JUPYTER_URL",
"code_execution.jupyter.url",
os.environ.get("CODE_EXECUTION_JUPYTER_URL", ""),
)
CODE_EXECUTION_JUPYTER_AUTH = PersistentConfig(
"CODE_EXECUTION_JUPYTER_AUTH",
"code_execution.jupyter.auth",
os.environ.get("CODE_EXECUTION_JUPYTER_AUTH", ""),
)
CODE_EXECUTION_JUPYTER_AUTH_TOKEN = PersistentConfig(
"CODE_EXECUTION_JUPYTER_AUTH_TOKEN",
"code_execution.jupyter.auth_token",
os.environ.get("CODE_EXECUTION_JUPYTER_AUTH_TOKEN", ""),
)
CODE_EXECUTION_JUPYTER_AUTH_PASSWORD = PersistentConfig(
"CODE_EXECUTION_JUPYTER_AUTH_PASSWORD",
"code_execution.jupyter.auth_password",
os.environ.get("CODE_EXECUTION_JUPYTER_AUTH_PASSWORD", ""),
)
ENABLE_CODE_INTERPRETER = PersistentConfig( ENABLE_CODE_INTERPRETER = PersistentConfig(
"ENABLE_CODE_INTERPRETER", "ENABLE_CODE_INTERPRETER",
"code_interpreter.enable", "code_interpreter.enable",
@ -1368,26 +1431,37 @@ CODE_INTERPRETER_PROMPT_TEMPLATE = PersistentConfig(
CODE_INTERPRETER_JUPYTER_URL = PersistentConfig( CODE_INTERPRETER_JUPYTER_URL = PersistentConfig(
"CODE_INTERPRETER_JUPYTER_URL", "CODE_INTERPRETER_JUPYTER_URL",
"code_interpreter.jupyter.url", "code_interpreter.jupyter.url",
os.environ.get("CODE_INTERPRETER_JUPYTER_URL", ""), os.environ.get(
"CODE_INTERPRETER_JUPYTER_URL", os.environ.get("CODE_EXECUTION_JUPYTER_URL", "")
),
) )
CODE_INTERPRETER_JUPYTER_AUTH = PersistentConfig( CODE_INTERPRETER_JUPYTER_AUTH = PersistentConfig(
"CODE_INTERPRETER_JUPYTER_AUTH", "CODE_INTERPRETER_JUPYTER_AUTH",
"code_interpreter.jupyter.auth", "code_interpreter.jupyter.auth",
os.environ.get("CODE_INTERPRETER_JUPYTER_AUTH", ""), os.environ.get(
"CODE_INTERPRETER_JUPYTER_AUTH",
os.environ.get("CODE_EXECUTION_JUPYTER_AUTH", ""),
),
) )
CODE_INTERPRETER_JUPYTER_AUTH_TOKEN = PersistentConfig( CODE_INTERPRETER_JUPYTER_AUTH_TOKEN = PersistentConfig(
"CODE_INTERPRETER_JUPYTER_AUTH_TOKEN", "CODE_INTERPRETER_JUPYTER_AUTH_TOKEN",
"code_interpreter.jupyter.auth_token", "code_interpreter.jupyter.auth_token",
os.environ.get("CODE_INTERPRETER_JUPYTER_AUTH_TOKEN", ""), os.environ.get(
"CODE_INTERPRETER_JUPYTER_AUTH_TOKEN",
os.environ.get("CODE_EXECUTION_JUPYTER_AUTH_TOKEN", ""),
),
) )
CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD = PersistentConfig( CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD = PersistentConfig(
"CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD", "CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD",
"code_interpreter.jupyter.auth_password", "code_interpreter.jupyter.auth_password",
os.environ.get("CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD", ""), os.environ.get(
"CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD",
os.environ.get("CODE_EXECUTION_JUPYTER_AUTH_PASSWORD", ""),
),
) )
@ -1706,6 +1780,12 @@ RAG_WEB_SEARCH_ENGINE = PersistentConfig(
os.getenv("RAG_WEB_SEARCH_ENGINE", ""), os.getenv("RAG_WEB_SEARCH_ENGINE", ""),
) )
RAG_WEB_SEARCH_FULL_CONTEXT = PersistentConfig(
"RAG_WEB_SEARCH_FULL_CONTEXT",
"rag.web.search.full_context",
os.getenv("RAG_WEB_SEARCH_FULL_CONTEXT", "False").lower() == "true",
)
# You can provide a list of your own websites to filter after performing a web search. # You can provide a list of your own websites to filter after performing a web search.
# This ensures the highest level of safety and reliability of the information sources. # This ensures the highest level of safety and reliability of the information sources.
RAG_WEB_SEARCH_DOMAIN_FILTER_LIST = PersistentConfig( RAG_WEB_SEARCH_DOMAIN_FILTER_LIST = PersistentConfig(

View file

@ -113,6 +113,7 @@ if WEBUI_NAME != "Open WebUI":
WEBUI_FAVICON_URL = "https://openwebui.com/favicon.png" WEBUI_FAVICON_URL = "https://openwebui.com/favicon.png"
TRUSTED_SIGNATURE_KEY = os.environ.get("TRUSTED_SIGNATURE_KEY", "")
#################################### ####################################
# ENV (dev,test,prod) # ENV (dev,test,prod)

View file

@ -88,6 +88,7 @@ from open_webui.models.models import Models
from open_webui.models.users import UserModel, Users from open_webui.models.users import UserModel, Users
from open_webui.config import ( from open_webui.config import (
LICENSE_KEY,
# Ollama # Ollama
ENABLE_OLLAMA_API, ENABLE_OLLAMA_API,
OLLAMA_BASE_URLS, OLLAMA_BASE_URLS,
@ -99,7 +100,12 @@ from open_webui.config import (
OPENAI_API_CONFIGS, OPENAI_API_CONFIGS,
# Direct Connections # Direct Connections
ENABLE_DIRECT_CONNECTIONS, ENABLE_DIRECT_CONNECTIONS,
# Code Interpreter # Code Execution
CODE_EXECUTION_ENGINE,
CODE_EXECUTION_JUPYTER_URL,
CODE_EXECUTION_JUPYTER_AUTH,
CODE_EXECUTION_JUPYTER_AUTH_TOKEN,
CODE_EXECUTION_JUPYTER_AUTH_PASSWORD,
ENABLE_CODE_INTERPRETER, ENABLE_CODE_INTERPRETER,
CODE_INTERPRETER_ENGINE, CODE_INTERPRETER_ENGINE,
CODE_INTERPRETER_PROMPT_TEMPLATE, CODE_INTERPRETER_PROMPT_TEMPLATE,
@ -175,6 +181,7 @@ from open_webui.config import (
YOUTUBE_LOADER_PROXY_URL, YOUTUBE_LOADER_PROXY_URL,
# Retrieval (Web Search) # Retrieval (Web Search)
RAG_WEB_SEARCH_ENGINE, RAG_WEB_SEARCH_ENGINE,
RAG_WEB_SEARCH_FULL_CONTEXT,
RAG_WEB_SEARCH_RESULT_COUNT, RAG_WEB_SEARCH_RESULT_COUNT,
RAG_WEB_SEARCH_CONCURRENT_REQUESTS, RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
RAG_WEB_SEARCH_TRUST_ENV, RAG_WEB_SEARCH_TRUST_ENV,
@ -316,15 +323,17 @@ from open_webui.utils.middleware import process_chat_payload, process_chat_respo
from open_webui.utils.access_control import has_access from open_webui.utils.access_control import has_access
from open_webui.utils.auth import ( from open_webui.utils.auth import (
get_license_data,
decode_token, decode_token,
get_admin_user, get_admin_user,
get_verified_user, get_verified_user,
) )
from open_webui.utils.oauth import oauth_manager from open_webui.utils.oauth import OAuthManager
from open_webui.utils.security_headers import SecurityHeadersMiddleware from open_webui.utils.security_headers import SecurityHeadersMiddleware
from open_webui.tasks import stop_task, list_tasks # Import from tasks.py from open_webui.tasks import stop_task, list_tasks # Import from tasks.py
if SAFE_MODE: if SAFE_MODE:
print("SAFE MODE ENABLED") print("SAFE MODE ENABLED")
Functions.deactivate_all_functions() Functions.deactivate_all_functions()
@ -371,6 +380,9 @@ async def lifespan(app: FastAPI):
if RESET_CONFIG_ON_START: if RESET_CONFIG_ON_START:
reset_config() reset_config()
if app.state.config.LICENSE_KEY:
get_license_data(app, app.state.config.LICENSE_KEY)
asyncio.create_task(periodic_usage_pool_cleanup()) asyncio.create_task(periodic_usage_pool_cleanup())
yield yield
@ -382,8 +394,12 @@ app = FastAPI(
lifespan=lifespan, lifespan=lifespan,
) )
oauth_manager = OAuthManager(app)
app.state.config = AppConfig() app.state.config = AppConfig()
app.state.WEBUI_NAME = WEBUI_NAME
app.state.config.LICENSE_KEY = LICENSE_KEY
######################################## ########################################
# #
@ -485,10 +501,10 @@ app.state.config.LDAP_CIPHERS = LDAP_CIPHERS
app.state.AUTH_TRUSTED_EMAIL_HEADER = WEBUI_AUTH_TRUSTED_EMAIL_HEADER app.state.AUTH_TRUSTED_EMAIL_HEADER = WEBUI_AUTH_TRUSTED_EMAIL_HEADER
app.state.AUTH_TRUSTED_NAME_HEADER = WEBUI_AUTH_TRUSTED_NAME_HEADER app.state.AUTH_TRUSTED_NAME_HEADER = WEBUI_AUTH_TRUSTED_NAME_HEADER
app.state.USER_COUNT = None
app.state.TOOLS = {} app.state.TOOLS = {}
app.state.FUNCTIONS = {} app.state.FUNCTIONS = {}
######################################## ########################################
# #
# RETRIEVAL # RETRIEVAL
@ -535,6 +551,7 @@ app.state.config.YOUTUBE_LOADER_PROXY_URL = YOUTUBE_LOADER_PROXY_URL
app.state.config.ENABLE_RAG_WEB_SEARCH = ENABLE_RAG_WEB_SEARCH app.state.config.ENABLE_RAG_WEB_SEARCH = ENABLE_RAG_WEB_SEARCH
app.state.config.RAG_WEB_SEARCH_ENGINE = RAG_WEB_SEARCH_ENGINE app.state.config.RAG_WEB_SEARCH_ENGINE = RAG_WEB_SEARCH_ENGINE
app.state.config.RAG_WEB_SEARCH_FULL_CONTEXT = RAG_WEB_SEARCH_FULL_CONTEXT
app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST = RAG_WEB_SEARCH_DOMAIN_FILTER_LIST app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST = RAG_WEB_SEARCH_DOMAIN_FILTER_LIST
app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION = ENABLE_GOOGLE_DRIVE_INTEGRATION app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION = ENABLE_GOOGLE_DRIVE_INTEGRATION
@ -607,10 +624,18 @@ app.state.EMBEDDING_FUNCTION = get_embedding_function(
######################################## ########################################
# #
# CODE INTERPRETER # CODE EXECUTION
# #
######################################## ########################################
app.state.config.CODE_EXECUTION_ENGINE = CODE_EXECUTION_ENGINE
app.state.config.CODE_EXECUTION_JUPYTER_URL = CODE_EXECUTION_JUPYTER_URL
app.state.config.CODE_EXECUTION_JUPYTER_AUTH = CODE_EXECUTION_JUPYTER_AUTH
app.state.config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN = CODE_EXECUTION_JUPYTER_AUTH_TOKEN
app.state.config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD = (
CODE_EXECUTION_JUPYTER_AUTH_PASSWORD
)
app.state.config.ENABLE_CODE_INTERPRETER = ENABLE_CODE_INTERPRETER app.state.config.ENABLE_CODE_INTERPRETER = ENABLE_CODE_INTERPRETER
app.state.config.CODE_INTERPRETER_ENGINE = CODE_INTERPRETER_ENGINE app.state.config.CODE_INTERPRETER_ENGINE = CODE_INTERPRETER_ENGINE
app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE = CODE_INTERPRETER_PROMPT_TEMPLATE app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE = CODE_INTERPRETER_PROMPT_TEMPLATE
@ -1075,7 +1100,7 @@ async def get_app_config(request: Request):
return { return {
**({"onboarding": True} if onboarding else {}), **({"onboarding": True} if onboarding else {}),
"status": True, "status": True,
"name": WEBUI_NAME, "name": app.state.WEBUI_NAME,
"version": VERSION, "version": VERSION,
"default_locale": str(DEFAULT_LOCALE), "default_locale": str(DEFAULT_LOCALE),
"oauth": { "oauth": {
@ -1114,6 +1139,9 @@ async def get_app_config(request: Request):
{ {
"default_models": app.state.config.DEFAULT_MODELS, "default_models": app.state.config.DEFAULT_MODELS,
"default_prompt_suggestions": app.state.config.DEFAULT_PROMPT_SUGGESTIONS, "default_prompt_suggestions": app.state.config.DEFAULT_PROMPT_SUGGESTIONS,
"code": {
"engine": app.state.config.CODE_EXECUTION_ENGINE,
},
"audio": { "audio": {
"tts": { "tts": {
"engine": app.state.config.TTS_ENGINE, "engine": app.state.config.TTS_ENGINE,
@ -1210,7 +1238,7 @@ if len(OAUTH_PROVIDERS) > 0:
@app.get("/oauth/{provider}/login") @app.get("/oauth/{provider}/login")
async def oauth_login(provider: str, request: Request): async def oauth_login(provider: str, request: Request):
return await oauth_manager.handle_login(provider, request) return await oauth_manager.handle_login(request, provider)
# OAuth login logic is as follows: # OAuth login logic is as follows:
@ -1221,14 +1249,14 @@ async def oauth_login(provider: str, request: Request):
# - Email addresses are considered unique, so we fail registration if the email address is already taken # - Email addresses are considered unique, so we fail registration if the email address is already taken
@app.get("/oauth/{provider}/callback") @app.get("/oauth/{provider}/callback")
async def oauth_callback(provider: str, request: Request, response: Response): async def oauth_callback(provider: str, request: Request, response: Response):
return await oauth_manager.handle_callback(provider, request, response) return await oauth_manager.handle_callback(request, provider, response)
@app.get("/manifest.json") @app.get("/manifest.json")
async def get_manifest_json(): async def get_manifest_json():
return { return {
"name": WEBUI_NAME, "name": app.state.WEBUI_NAME,
"short_name": WEBUI_NAME, "short_name": app.state.WEBUI_NAME,
"description": "Open WebUI is an open, extensible, user-friendly interface for AI that adapts to your workflow.", "description": "Open WebUI is an open, extensible, user-friendly interface for AI that adapts to your workflow.",
"start_url": "/", "start_url": "/",
"display": "standalone", "display": "standalone",
@ -1255,8 +1283,8 @@ async def get_manifest_json():
async def get_opensearch_xml(): async def get_opensearch_xml():
xml_content = rf""" xml_content = rf"""
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/" xmlns:moz="http://www.mozilla.org/2006/browser/search/"> <OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/" xmlns:moz="http://www.mozilla.org/2006/browser/search/">
<ShortName>{WEBUI_NAME}</ShortName> <ShortName>{app.state.WEBUI_NAME}</ShortName>
<Description>Search {WEBUI_NAME}</Description> <Description>Search {app.state.WEBUI_NAME}</Description>
<InputEncoding>UTF-8</InputEncoding> <InputEncoding>UTF-8</InputEncoding>
<Image width="16" height="16" type="image/x-icon">{app.state.config.WEBUI_URL}/static/favicon.png</Image> <Image width="16" height="16" type="image/x-icon">{app.state.config.WEBUI_URL}/static/favicon.png</Image>
<Url type="text/html" method="get" template="{app.state.config.WEBUI_URL}/?q={"{searchTerms}"}"/> <Url type="text/html" method="get" template="{app.state.config.WEBUI_URL}/?q={"{searchTerms}"}"/>

View file

@ -304,7 +304,12 @@ def get_sources_from_files(
relevant_contexts = [] relevant_contexts = []
for file in files: for file in files:
if file.get("context") == "full": if file.get("docs"):
context = {
"documents": [[doc.get("content") for doc in file.get("docs")]],
"metadatas": [[doc.get("metadata") for doc in file.get("docs")]],
}
elif file.get("context") == "full":
context = { context = {
"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")}]],

View file

@ -27,8 +27,7 @@ def search_tavily(
""" """
url = "https://api.tavily.com/search" url = "https://api.tavily.com/search"
data = {"query": query, "api_key": api_key} data = {"query": query, "api_key": api_key}
include_domain = filter_list response = requests.post(url, json=data)
response = requests.post(url, include_domain, json=data)
response.raise_for_status() response.raise_for_status()
json_response = response.json() json_response = response.json()

View file

@ -316,7 +316,6 @@ class SafeWebBaseLoader(WebBaseLoader):
results = await self.fetch_all(urls) results = await self.fetch_all(urls)
return self._unpack_fetch_results(results, urls, parser=parser) return self._unpack_fetch_results(results, urls, parser=parser)
def lazy_load(self) -> Iterator[Document]: def lazy_load(self) -> Iterator[Document]:
"""Lazy load text from the url(s) in web_path with error handling.""" """Lazy load text from the url(s) in web_path with error handling."""
for path in self.web_paths: for path in self.web_paths:

View file

@ -251,9 +251,19 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
user = Users.get_user_by_email(mail) user = Users.get_user_by_email(mail)
if not user: if not user:
try: try:
user_count = Users.get_num_users()
if (
request.app.state.USER_COUNT
and user_count >= request.app.state.USER_COUNT
):
raise HTTPException(
status.HTTP_403_FORBIDDEN,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
role = ( role = (
"admin" "admin"
if Users.get_num_users() == 0 if user_count == 0
else request.app.state.config.DEFAULT_USER_ROLE else request.app.state.config.DEFAULT_USER_ROLE
) )
@ -413,6 +423,8 @@ async def signin(request: Request, response: Response, form_data: SigninForm):
@router.post("/signup", response_model=SessionUserResponse) @router.post("/signup", response_model=SessionUserResponse)
async def signup(request: Request, response: Response, form_data: SignupForm): async def signup(request: Request, response: Response, form_data: SignupForm):
user_count = Users.get_num_users()
if WEBUI_AUTH: if WEBUI_AUTH:
if ( if (
not request.app.state.config.ENABLE_SIGNUP not request.app.state.config.ENABLE_SIGNUP
@ -422,7 +434,12 @@ async def signup(request: Request, response: Response, form_data: SignupForm):
status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED
) )
else: else:
if Users.get_num_users() != 0: if user_count != 0:
raise HTTPException(
status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED
)
if request.app.state.USER_COUNT and user_count >= request.app.state.USER_COUNT:
raise HTTPException( raise HTTPException(
status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED
) )
@ -437,12 +454,10 @@ async def signup(request: Request, response: Response, form_data: SignupForm):
try: try:
role = ( role = (
"admin" "admin" if user_count == 0 else request.app.state.config.DEFAULT_USER_ROLE
if Users.get_num_users() == 0
else request.app.state.config.DEFAULT_USER_ROLE
) )
if Users.get_num_users() == 0: if user_count == 0:
# Disable signup after the first user is created # Disable signup after the first user is created
request.app.state.config.ENABLE_SIGNUP = False request.app.state.config.ENABLE_SIGNUP = False
@ -484,6 +499,7 @@ async def signup(request: Request, response: Response, form_data: SignupForm):
if request.app.state.config.WEBHOOK_URL: if request.app.state.config.WEBHOOK_URL:
post_webhook( post_webhook(
request.app.state.WEBUI_NAME,
request.app.state.config.WEBHOOK_URL, request.app.state.config.WEBHOOK_URL,
WEBHOOK_MESSAGES.USER_SIGNUP(user.name), WEBHOOK_MESSAGES.USER_SIGNUP(user.name),
{ {

View file

@ -192,7 +192,7 @@ async def get_channel_messages(
############################ ############################
async def send_notification(webui_url, channel, message, active_user_ids): async def send_notification(name, webui_url, channel, message, active_user_ids):
users = get_users_with_access("read", channel.access_control) users = get_users_with_access("read", channel.access_control)
for user in users: for user in users:
@ -206,6 +206,7 @@ async def send_notification(webui_url, channel, message, active_user_ids):
if webhook_url: if webhook_url:
post_webhook( post_webhook(
name,
webhook_url, webhook_url,
f"#{channel.name} - {webui_url}/channels/{channel.id}\n\n{message.content}", f"#{channel.name} - {webui_url}/channels/{channel.id}\n\n{message.content}",
{ {
@ -302,6 +303,7 @@ async def post_new_message(
background_tasks.add_task( background_tasks.add_task(
send_notification, send_notification,
request.app.state.WEBUI_NAME,
request.app.state.config.WEBUI_URL, request.app.state.config.WEBUI_URL,
channel, channel,
message, message,

View file

@ -70,6 +70,11 @@ async def set_direct_connections_config(
# CodeInterpreterConfig # CodeInterpreterConfig
############################ ############################
class CodeInterpreterConfigForm(BaseModel): class CodeInterpreterConfigForm(BaseModel):
CODE_EXECUTION_ENGINE: str
CODE_EXECUTION_JUPYTER_URL: Optional[str]
CODE_EXECUTION_JUPYTER_AUTH: Optional[str]
CODE_EXECUTION_JUPYTER_AUTH_TOKEN: Optional[str]
CODE_EXECUTION_JUPYTER_AUTH_PASSWORD: Optional[str]
ENABLE_CODE_INTERPRETER: bool ENABLE_CODE_INTERPRETER: bool
CODE_INTERPRETER_ENGINE: str CODE_INTERPRETER_ENGINE: str
CODE_INTERPRETER_PROMPT_TEMPLATE: Optional[str] CODE_INTERPRETER_PROMPT_TEMPLATE: Optional[str]
@ -79,9 +84,14 @@ class CodeInterpreterConfigForm(BaseModel):
CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD: Optional[str] CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD: Optional[str]
@router.get("/code_interpreter", response_model=CodeInterpreterConfigForm) @router.get("/code_execution", response_model=CodeInterpreterConfigForm)
async def get_code_interpreter_config(request: Request, user=Depends(get_admin_user)): async def get_code_execution_config(request: Request, user=Depends(get_admin_user)):
return { return {
"CODE_EXECUTION_ENGINE": request.app.state.config.CODE_EXECUTION_ENGINE,
"CODE_EXECUTION_JUPYTER_URL": request.app.state.config.CODE_EXECUTION_JUPYTER_URL,
"CODE_EXECUTION_JUPYTER_AUTH": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH,
"CODE_EXECUTION_JUPYTER_AUTH_TOKEN": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN,
"CODE_EXECUTION_JUPYTER_AUTH_PASSWORD": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD,
"ENABLE_CODE_INTERPRETER": request.app.state.config.ENABLE_CODE_INTERPRETER, "ENABLE_CODE_INTERPRETER": request.app.state.config.ENABLE_CODE_INTERPRETER,
"CODE_INTERPRETER_ENGINE": request.app.state.config.CODE_INTERPRETER_ENGINE, "CODE_INTERPRETER_ENGINE": request.app.state.config.CODE_INTERPRETER_ENGINE,
"CODE_INTERPRETER_PROMPT_TEMPLATE": request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE, "CODE_INTERPRETER_PROMPT_TEMPLATE": request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE,
@ -92,10 +102,25 @@ async def get_code_interpreter_config(request: Request, user=Depends(get_admin_u
} }
@router.post("/code_interpreter", response_model=CodeInterpreterConfigForm) @router.post("/code_execution", response_model=CodeInterpreterConfigForm)
async def set_code_interpreter_config( async def set_code_execution_config(
request: Request, form_data: CodeInterpreterConfigForm, user=Depends(get_admin_user) request: Request, form_data: CodeInterpreterConfigForm, user=Depends(get_admin_user)
): ):
request.app.state.config.CODE_EXECUTION_ENGINE = form_data.CODE_EXECUTION_ENGINE
request.app.state.config.CODE_EXECUTION_JUPYTER_URL = (
form_data.CODE_EXECUTION_JUPYTER_URL
)
request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH = (
form_data.CODE_EXECUTION_JUPYTER_AUTH
)
request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN = (
form_data.CODE_EXECUTION_JUPYTER_AUTH_TOKEN
)
request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD = (
form_data.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD
)
request.app.state.config.ENABLE_CODE_INTERPRETER = form_data.ENABLE_CODE_INTERPRETER request.app.state.config.ENABLE_CODE_INTERPRETER = form_data.ENABLE_CODE_INTERPRETER
request.app.state.config.CODE_INTERPRETER_ENGINE = form_data.CODE_INTERPRETER_ENGINE request.app.state.config.CODE_INTERPRETER_ENGINE = form_data.CODE_INTERPRETER_ENGINE
request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE = ( request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE = (
@ -118,6 +143,11 @@ async def set_code_interpreter_config(
) )
return { return {
"CODE_EXECUTION_ENGINE": request.app.state.config.CODE_EXECUTION_ENGINE,
"CODE_EXECUTION_JUPYTER_URL": request.app.state.config.CODE_EXECUTION_JUPYTER_URL,
"CODE_EXECUTION_JUPYTER_AUTH": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH,
"CODE_EXECUTION_JUPYTER_AUTH_TOKEN": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN,
"CODE_EXECUTION_JUPYTER_AUTH_PASSWORD": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD,
"ENABLE_CODE_INTERPRETER": request.app.state.config.ENABLE_CODE_INTERPRETER, "ENABLE_CODE_INTERPRETER": request.app.state.config.ENABLE_CODE_INTERPRETER,
"CODE_INTERPRETER_ENGINE": request.app.state.config.CODE_INTERPRETER_ENGINE, "CODE_INTERPRETER_ENGINE": request.app.state.config.CODE_INTERPRETER_ENGINE,
"CODE_INTERPRETER_PROMPT_TEMPLATE": request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE, "CODE_INTERPRETER_PROMPT_TEMPLATE": request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE,

View file

@ -9,6 +9,7 @@ from fastapi import (
status, status,
APIRouter, APIRouter,
) )
import aiohttp
import os import os
import logging import logging
import shutil import shutil
@ -56,96 +57,103 @@ def get_sorted_filters(model_id, models):
return sorted_filters return sorted_filters
def process_pipeline_inlet_filter(request, payload, user, models): async def process_pipeline_inlet_filter(request, payload, user, models):
user = {"id": user.id, "email": user.email, "name": user.name, "role": user.role} user = {"id": user.id, "email": user.email, "name": user.name, "role": user.role}
model_id = payload["model"] model_id = payload["model"]
sorted_filters = get_sorted_filters(model_id, models) sorted_filters = get_sorted_filters(model_id, models)
model = models[model_id] model = models[model_id]
if "pipeline" in model: if "pipeline" in model:
sorted_filters.append(model) sorted_filters.append(model)
async with aiohttp.ClientSession() as session:
for filter in sorted_filters: for filter in sorted_filters:
r = None urlIdx = filter.get("urlIdx")
try: if urlIdx is None:
urlIdx = filter["urlIdx"] continue
url = request.app.state.config.OPENAI_API_BASE_URLS[urlIdx] url = request.app.state.config.OPENAI_API_BASE_URLS[urlIdx]
key = request.app.state.config.OPENAI_API_KEYS[urlIdx] key = request.app.state.config.OPENAI_API_KEYS[urlIdx]
if key == "": if not key:
continue continue
headers = {"Authorization": f"Bearer {key}"} headers = {"Authorization": f"Bearer {key}"}
r = requests.post( request_data = {
f"{url}/{filter['id']}/filter/inlet",
headers=headers,
json={
"user": user, "user": user,
"body": payload, "body": payload,
}, }
try:
async with session.post(
f"{url}/{filter['id']}/filter/inlet",
headers=headers,
json=request_data,
) as response:
response.raise_for_status()
payload = await response.json()
except aiohttp.ClientResponseError as e:
res = (
await response.json()
if response.content_type == "application/json"
else {}
) )
r.raise_for_status()
payload = r.json()
except Exception as e:
# Handle connection error here
print(f"Connection error: {e}")
if r is not None:
res = r.json()
if "detail" in res: if "detail" in res:
raise Exception(r.status_code, res["detail"]) raise Exception(response.status, res["detail"])
except Exception as e:
print(f"Connection error: {e}")
return payload return payload
def process_pipeline_outlet_filter(request, payload, user, models): async def process_pipeline_outlet_filter(request, payload, user, models):
user = {"id": user.id, "email": user.email, "name": user.name, "role": user.role} user = {"id": user.id, "email": user.email, "name": user.name, "role": user.role}
model_id = payload["model"] model_id = payload["model"]
sorted_filters = get_sorted_filters(model_id, models) sorted_filters = get_sorted_filters(model_id, models)
model = models[model_id] model = models[model_id]
if "pipeline" in model: if "pipeline" in model:
sorted_filters = [model] + sorted_filters sorted_filters = [model] + sorted_filters
async with aiohttp.ClientSession() as session:
for filter in sorted_filters: for filter in sorted_filters:
r = None urlIdx = filter.get("urlIdx")
try: if urlIdx is None:
urlIdx = filter["urlIdx"] continue
url = request.app.state.config.OPENAI_API_BASE_URLS[urlIdx] url = request.app.state.config.OPENAI_API_BASE_URLS[urlIdx]
key = request.app.state.config.OPENAI_API_KEYS[urlIdx] key = request.app.state.config.OPENAI_API_KEYS[urlIdx]
if key != "": if not key:
r = requests.post( continue
f"{url}/{filter['id']}/filter/outlet",
headers={"Authorization": f"Bearer {key}"}, headers = {"Authorization": f"Bearer {key}"}
json={ request_data = {
"user": user, "user": user,
"body": payload, "body": payload,
}, }
)
r.raise_for_status()
data = r.json()
payload = data
except Exception as e:
# Handle connection error here
print(f"Connection error: {e}")
if r is not None:
try: try:
res = r.json() async with session.post(
f"{url}/{filter['id']}/filter/outlet",
headers=headers,
json=request_data,
) as response:
response.raise_for_status()
payload = await response.json()
except aiohttp.ClientResponseError as e:
try:
res = (
await response.json()
if "application/json" in response.content_type
else {}
)
if "detail" in res: if "detail" in res:
return Exception(r.status_code, res) raise Exception(response.status, res)
except Exception: except Exception:
pass pass
except Exception as e:
else: print(f"Connection error: {e}")
pass
return payload return payload

View file

@ -371,7 +371,8 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)):
"proxy_url": request.app.state.config.YOUTUBE_LOADER_PROXY_URL, "proxy_url": request.app.state.config.YOUTUBE_LOADER_PROXY_URL,
}, },
"web": { "web": {
"web_loader_ssl_verification": request.app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION, "ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION": request.app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
"RAG_WEB_SEARCH_FULL_CONTEXT": request.app.state.config.RAG_WEB_SEARCH_FULL_CONTEXT,
"search": { "search": {
"enabled": request.app.state.config.ENABLE_RAG_WEB_SEARCH, "enabled": request.app.state.config.ENABLE_RAG_WEB_SEARCH,
"drive": request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION, "drive": request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION,
@ -457,7 +458,8 @@ class WebSearchConfig(BaseModel):
class WebConfig(BaseModel): class WebConfig(BaseModel):
search: WebSearchConfig search: WebSearchConfig
web_loader_ssl_verification: Optional[bool] = None ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION: Optional[bool] = None
RAG_WEB_SEARCH_FULL_CONTEXT: Optional[bool] = None
class ConfigUpdateForm(BaseModel): class ConfigUpdateForm(BaseModel):
@ -512,11 +514,16 @@ async def update_rag_config(
if form_data.web is not None: if form_data.web is not None:
request.app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = ( request.app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = (
# Note: When UI "Bypass SSL verification for Websites"=True then ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION=False # Note: When UI "Bypass SSL verification for Websites"=True then ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION=False
form_data.web.web_loader_ssl_verification form_data.web.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION
) )
request.app.state.config.ENABLE_RAG_WEB_SEARCH = form_data.web.search.enabled request.app.state.config.ENABLE_RAG_WEB_SEARCH = form_data.web.search.enabled
request.app.state.config.RAG_WEB_SEARCH_ENGINE = form_data.web.search.engine request.app.state.config.RAG_WEB_SEARCH_ENGINE = form_data.web.search.engine
request.app.state.config.RAG_WEB_SEARCH_FULL_CONTEXT = (
form_data.web.RAG_WEB_SEARCH_FULL_CONTEXT
)
request.app.state.config.SEARXNG_QUERY_URL = ( request.app.state.config.SEARXNG_QUERY_URL = (
form_data.web.search.searxng_query_url form_data.web.search.searxng_query_url
) )
@ -600,7 +607,8 @@ async def update_rag_config(
"translation": request.app.state.YOUTUBE_LOADER_TRANSLATION, "translation": request.app.state.YOUTUBE_LOADER_TRANSLATION,
}, },
"web": { "web": {
"web_loader_ssl_verification": request.app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION, "ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION": request.app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
"RAG_WEB_SEARCH_FULL_CONTEXT": request.app.state.config.RAG_WEB_SEARCH_FULL_CONTEXT,
"search": { "search": {
"enabled": request.app.state.config.ENABLE_RAG_WEB_SEARCH, "enabled": request.app.state.config.ENABLE_RAG_WEB_SEARCH,
"engine": request.app.state.config.RAG_WEB_SEARCH_ENGINE, "engine": request.app.state.config.RAG_WEB_SEARCH_ENGINE,
@ -1262,6 +1270,7 @@ def search_web(request: Request, engine: str, query: str) -> list[SearchResult]:
request.app.state.config.TAVILY_API_KEY, request.app.state.config.TAVILY_API_KEY,
query, query,
request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT, request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
) )
else: else:
raise Exception("No TAVILY_API_KEY found in environment variables") raise Exception("No TAVILY_API_KEY found in environment variables")
@ -1349,6 +1358,21 @@ async def process_web_search(
trust_env=request.app.state.config.RAG_WEB_SEARCH_TRUST_ENV, trust_env=request.app.state.config.RAG_WEB_SEARCH_TRUST_ENV,
) )
docs = await loader.aload() docs = await loader.aload()
if request.app.state.config.RAG_WEB_SEARCH_FULL_CONTEXT:
return {
"status": True,
"docs": [
{
"content": doc.page_content,
"metadata": doc.metadata,
}
for doc in docs
],
"filenames": urls,
"loaded_count": len(docs),
}
else:
await run_in_threadpool( await run_in_threadpool(
save_docs_to_vector_db, save_docs_to_vector_db,
request, request,

View file

@ -208,7 +208,7 @@ async def generate_title(
"stream": False, "stream": False,
**( **(
{"max_tokens": 1000} {"max_tokens": 1000}
if models[task_model_id]["owned_by"] == "ollama" if models[task_model_id].get("owned_by") == "ollama"
else { else {
"max_completion_tokens": 1000, "max_completion_tokens": 1000,
} }
@ -571,7 +571,7 @@ async def generate_emoji(
"stream": False, "stream": False,
**( **(
{"max_tokens": 4} {"max_tokens": 4}
if models[task_model_id]["owned_by"] == "ollama" if models[task_model_id].get("owned_by") == "ollama"
else { else {
"max_completion_tokens": 4, "max_completion_tokens": 4,
} }

View file

@ -4,45 +4,75 @@ import markdown
from open_webui.models.chats import ChatTitleMessagesForm from open_webui.models.chats import ChatTitleMessagesForm
from open_webui.config import DATA_DIR, ENABLE_ADMIN_EXPORT from open_webui.config import DATA_DIR, ENABLE_ADMIN_EXPORT
from open_webui.constants import ERROR_MESSAGES from open_webui.constants import ERROR_MESSAGES
from fastapi import APIRouter, Depends, HTTPException, Response, status from fastapi import APIRouter, Depends, HTTPException, Request, Response, status
from pydantic import BaseModel from pydantic import BaseModel
from starlette.responses import FileResponse from starlette.responses import FileResponse
from open_webui.utils.misc import get_gravatar_url from open_webui.utils.misc import get_gravatar_url
from open_webui.utils.pdf_generator import PDFGenerator from open_webui.utils.pdf_generator import PDFGenerator
from open_webui.utils.auth import get_admin_user from open_webui.utils.auth import get_admin_user, get_verified_user
from open_webui.utils.code_interpreter import execute_code_jupyter
router = APIRouter() router = APIRouter()
@router.get("/gravatar") @router.get("/gravatar")
async def get_gravatar( async def get_gravatar(email: str, user=Depends(get_verified_user)):
email: str,
):
return get_gravatar_url(email) return get_gravatar_url(email)
class CodeFormatRequest(BaseModel): class CodeForm(BaseModel):
code: str code: str
@router.post("/code/format") @router.post("/code/format")
async def format_code(request: CodeFormatRequest): async def format_code(form_data: CodeForm, user=Depends(get_verified_user)):
try: try:
formatted_code = black.format_str(request.code, mode=black.Mode()) formatted_code = black.format_str(form_data.code, mode=black.Mode())
return {"code": formatted_code} return {"code": formatted_code}
except black.NothingChanged: except black.NothingChanged:
return {"code": request.code} return {"code": form_data.code}
except Exception as e: except Exception as e:
raise HTTPException(status_code=400, detail=str(e)) raise HTTPException(status_code=400, detail=str(e))
@router.post("/code/execute")
async def execute_code(
request: Request, form_data: CodeForm, user=Depends(get_verified_user)
):
if request.app.state.config.CODE_EXECUTION_ENGINE == "jupyter":
output = await execute_code_jupyter(
request.app.state.config.CODE_EXECUTION_JUPYTER_URL,
form_data.code,
(
request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN
if request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH == "token"
else None
),
(
request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD
if request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH == "password"
else None
),
)
return output
else:
raise HTTPException(
status_code=400,
detail="Code execution engine not supported",
)
class MarkdownForm(BaseModel): class MarkdownForm(BaseModel):
md: str md: str
@router.post("/markdown") @router.post("/markdown")
async def get_html_from_markdown( async def get_html_from_markdown(
form_data: MarkdownForm, form_data: MarkdownForm, user=Depends(get_verified_user)
): ):
return {"html": markdown.markdown(form_data.md)} return {"html": markdown.markdown(form_data.md)}
@ -54,7 +84,7 @@ class ChatForm(BaseModel):
@router.post("/pdf") @router.post("/pdf")
async def download_chat_as_pdf( async def download_chat_as_pdf(
form_data: ChatTitleMessagesForm, form_data: ChatTitleMessagesForm, user=Depends(get_verified_user)
): ):
try: try:
pdf_bytes = PDFGenerator(form_data).generate_chat_pdf() pdf_bytes = PDFGenerator(form_data).generate_chat_pdf()

View file

View file

@ -9308,5 +9308,3 @@
.json-schema-2020-12__title:first-of-type { .json-schema-2020-12__title:first-of-type {
font-size: 16px; font-size: 16px;
} }
/*# sourceMappingURL=swagger-ui.css.map*/

View file

@ -1,6 +1,11 @@
import logging import logging
import uuid import uuid
import jwt import jwt
import base64
import hmac
import hashlib
import requests
from datetime import UTC, datetime, timedelta from datetime import UTC, datetime, timedelta
from typing import Optional, Union, List, Dict from typing import Optional, Union, List, Dict
@ -8,7 +13,8 @@ from typing import Optional, Union, List, Dict
from open_webui.models.users import Users from open_webui.models.users import Users
from open_webui.constants import ERROR_MESSAGES from open_webui.constants import ERROR_MESSAGES
from open_webui.env import WEBUI_SECRET_KEY from open_webui.config import override_static
from open_webui.env import WEBUI_SECRET_KEY, TRUSTED_SIGNATURE_KEY
from fastapi import Depends, HTTPException, Request, Response, status from fastapi import Depends, HTTPException, Request, Response, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
@ -24,6 +30,53 @@ ALGORITHM = "HS256"
# Auth Utils # Auth Utils
############## ##############
def verify_signature(payload: str, signature: str) -> bool:
"""
Verifies the HMAC signature of the received payload.
"""
try:
expected_signature = base64.b64encode(
hmac.new(TRUSTED_SIGNATURE_KEY, payload.encode(), hashlib.sha256).digest()
).decode()
# Compare securely to prevent timing attacks
return hmac.compare_digest(expected_signature, signature)
except Exception:
return False
def get_license_data(app, key):
if key:
try:
res = requests.post(
"https://api.openwebui.com/api/v1/license",
json={"key": key, "version": "1"},
timeout=5,
)
if getattr(res, "ok", False):
payload = getattr(res, "json", lambda: {})()
for k, v in payload.items():
if k == "resources":
for p, c in v.items():
globals().get("override_static", lambda a, b: None)(p, c)
elif k == "user_count":
setattr(app.state, "USER_COUNT", v)
elif k == "webui_name":
setattr(app.state, "WEBUI_NAME", v)
return True
else:
print(
f"License: retrieval issue: {getattr(res, 'text', 'unknown error')}"
)
except Exception as ex:
print(f"License: Uncaught Exception: {ex}")
return False
bearer_security = HTTPBearer(auto_error=False) bearer_security = HTTPBearer(auto_error=False)
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

View file

@ -186,12 +186,6 @@ async def generate_chat_completion(
if model_id not in models: if model_id not in models:
raise Exception("Model not found") raise Exception("Model not found")
# Process the form_data through the pipeline
try:
form_data = process_pipeline_inlet_filter(request, form_data, user, models)
except Exception as e:
raise e
model = models[model_id] model = models[model_id]
if getattr(request.state, "direct", False): if getattr(request.state, "direct", False):
@ -206,7 +200,7 @@ async def generate_chat_completion(
except Exception as e: except Exception as e:
raise e raise e
if model["owned_by"] == "arena": if model.get("owned_by") == "arena":
model_ids = model.get("info", {}).get("meta", {}).get("model_ids") model_ids = model.get("info", {}).get("meta", {}).get("model_ids")
filter_mode = model.get("info", {}).get("meta", {}).get("filter_mode") filter_mode = model.get("info", {}).get("meta", {}).get("filter_mode")
if model_ids and filter_mode == "exclude": if model_ids and filter_mode == "exclude":
@ -259,7 +253,7 @@ async def generate_chat_completion(
return await generate_function_chat_completion( return await generate_function_chat_completion(
request, form_data, user=user, models=models request, form_data, user=user, models=models
) )
if model["owned_by"] == "ollama": if model.get("owned_by") == "ollama":
# Using /ollama/api/chat endpoint # Using /ollama/api/chat endpoint
form_data = convert_payload_openai_to_ollama(form_data) form_data = convert_payload_openai_to_ollama(form_data)
response = await generate_ollama_chat_completion( response = await generate_ollama_chat_completion(
@ -308,7 +302,7 @@ async def chat_completed(request: Request, form_data: dict, user: Any):
model = models[model_id] model = models[model_id]
try: try:
data = process_pipeline_outlet_filter(request, data, user, models) data = await process_pipeline_outlet_filter(request, data, user, models)
except Exception as e: except Exception as e:
return Exception(f"Error: {e}") return Exception(f"Error: {e}")

View file

@ -39,7 +39,10 @@ from open_webui.routers.tasks import (
) )
from open_webui.routers.retrieval import process_web_search, SearchForm from open_webui.routers.retrieval import process_web_search, SearchForm
from open_webui.routers.images import image_generations, GenerateImageForm from open_webui.routers.images import image_generations, GenerateImageForm
from open_webui.routers.pipelines import (
process_pipeline_inlet_filter,
process_pipeline_outlet_filter,
)
from open_webui.utils.webhook import post_webhook from open_webui.utils.webhook import post_webhook
@ -359,6 +362,17 @@ async def chat_web_search_handler(
) )
files = form_data.get("files", []) files = form_data.get("files", [])
if request.app.state.config.RAG_WEB_SEARCH_FULL_CONTEXT:
files.append(
{
"docs": results.get("docs", []),
"name": searchQuery,
"type": "web_search_docs",
"urls": results["filenames"],
}
)
else:
files.append( files.append(
{ {
"collection_name": results["collection_name"], "collection_name": results["collection_name"],
@ -676,6 +690,25 @@ async def process_chat_payload(request, form_data, metadata, user, model):
variables = form_data.pop("variables", None) variables = form_data.pop("variables", None)
# Process the form_data through the pipeline
try:
form_data = await process_pipeline_inlet_filter(
request, form_data, user, models
)
except Exception as e:
raise e
try:
form_data, flags = await process_filter_functions(
request=request,
filter_ids=get_sorted_filter_ids(model),
filter_type="inlet",
form_data=form_data,
extra_params=extra_params,
)
except Exception as e:
raise Exception(f"Error: {e}")
features = form_data.pop("features", None) features = form_data.pop("features", None)
if features: if features:
if "web_search" in features and features["web_search"]: if "web_search" in features and features["web_search"]:
@ -698,17 +731,6 @@ async def process_chat_payload(request, form_data, metadata, user, model):
form_data["messages"], form_data["messages"],
) )
try:
form_data, flags = await process_filter_functions(
request=request,
filter_ids=get_sorted_filter_ids(model),
filter_type="inlet",
form_data=form_data,
extra_params=extra_params,
)
except Exception as e:
raise Exception(f"Error: {e}")
tool_ids = form_data.pop("tool_ids", None) tool_ids = form_data.pop("tool_ids", None)
files = form_data.pop("files", None) files = form_data.pop("files", None)
# Remove files duplicates # Remove files duplicates
@ -789,7 +811,7 @@ async def process_chat_payload(request, form_data, metadata, user, model):
# Workaround for Ollama 2.0+ system prompt issue # Workaround for Ollama 2.0+ system prompt issue
# TODO: replace with add_or_update_system_message # TODO: replace with add_or_update_system_message
if model["owned_by"] == "ollama": if model.get("owned_by") == "ollama":
form_data["messages"] = prepend_to_first_user_message_content( form_data["messages"] = prepend_to_first_user_message_content(
rag_template( rag_template(
request.app.state.config.RAG_TEMPLATE, context_string, prompt request.app.state.config.RAG_TEMPLATE, context_string, prompt
@ -997,6 +1019,7 @@ async def process_chat_response(
webhook_url = Users.get_user_webhook_url_by_id(user.id) webhook_url = Users.get_user_webhook_url_by_id(user.id)
if webhook_url: if webhook_url:
post_webhook( post_webhook(
request.app.state.WEBUI_NAME,
webhook_url, webhook_url,
f"{title} - {request.app.state.config.WEBUI_URL}/c/{metadata['chat_id']}\n\n{content}", f"{title} - {request.app.state.config.WEBUI_URL}/c/{metadata['chat_id']}\n\n{content}",
{ {
@ -1335,7 +1358,14 @@ async def process_chat_response(
) )
tool_calls = [] tool_calls = []
content = message.get("content", "") if message else ""
last_assistant_message = get_last_assistant_message(form_data["messages"])
content = (
message.get("content", "")
if message
else last_assistant_message if last_assistant_message else ""
)
content_blocks = [ content_blocks = [
{ {
"type": "text", "type": "text",
@ -1862,6 +1892,7 @@ async def process_chat_response(
webhook_url = Users.get_user_webhook_url_by_id(user.id) webhook_url = Users.get_user_webhook_url_by_id(user.id)
if webhook_url: if webhook_url:
post_webhook( post_webhook(
request.app.state.WEBUI_NAME,
webhook_url, webhook_url,
f"{title} - {request.app.state.config.WEBUI_URL}/c/{metadata['chat_id']}\n\n{content}", f"{title} - {request.app.state.config.WEBUI_URL}/c/{metadata['chat_id']}\n\n{content}",
{ {

View file

@ -142,7 +142,7 @@ async def get_all_models(request):
custom_model.base_model_id == model["id"] custom_model.base_model_id == model["id"]
or custom_model.base_model_id == model["id"].split(":")[0] or custom_model.base_model_id == model["id"].split(":")[0]
): ):
owned_by = model["owned_by"] owned_by = model.get("owned_by", "unknown owner")
if "pipe" in model: if "pipe" in model:
pipe = model["pipe"] pipe = model["pipe"]
break break

View file

@ -36,7 +36,11 @@ from open_webui.config import (
AppConfig, AppConfig,
) )
from open_webui.constants import ERROR_MESSAGES, WEBHOOK_MESSAGES from open_webui.constants import ERROR_MESSAGES, WEBHOOK_MESSAGES
from open_webui.env import WEBUI_AUTH_COOKIE_SAME_SITE, WEBUI_AUTH_COOKIE_SECURE from open_webui.env import (
WEBUI_NAME,
WEBUI_AUTH_COOKIE_SAME_SITE,
WEBUI_AUTH_COOKIE_SECURE,
)
from open_webui.utils.misc import parse_duration from open_webui.utils.misc import parse_duration
from open_webui.utils.auth import get_password_hash, create_token from open_webui.utils.auth import get_password_hash, create_token
from open_webui.utils.webhook import post_webhook from open_webui.utils.webhook import post_webhook
@ -66,8 +70,9 @@ auth_manager_config.JWT_EXPIRES_IN = JWT_EXPIRES_IN
class OAuthManager: class OAuthManager:
def __init__(self): def __init__(self, app):
self.oauth = OAuth() self.oauth = OAuth()
self.app = app
for _, provider_config in OAUTH_PROVIDERS.items(): for _, provider_config in OAUTH_PROVIDERS.items():
provider_config["register"](self.oauth) provider_config["register"](self.oauth)
@ -200,7 +205,7 @@ class OAuthManager:
id=group_model.id, form_data=update_form, overwrite=False id=group_model.id, form_data=update_form, overwrite=False
) )
async def handle_login(self, provider, request): async def handle_login(self, request, provider):
if provider not in OAUTH_PROVIDERS: if provider not in OAUTH_PROVIDERS:
raise HTTPException(404) raise HTTPException(404)
# If the provider has a custom redirect URL, use that, otherwise automatically generate one # If the provider has a custom redirect URL, use that, otherwise automatically generate one
@ -212,7 +217,7 @@ class OAuthManager:
raise HTTPException(404) raise HTTPException(404)
return await client.authorize_redirect(request, redirect_uri) return await client.authorize_redirect(request, redirect_uri)
async def handle_callback(self, provider, request, response): async def handle_callback(self, request, provider, response):
if provider not in OAUTH_PROVIDERS: if provider not in OAUTH_PROVIDERS:
raise HTTPException(404) raise HTTPException(404)
client = self.get_client(provider) client = self.get_client(provider)
@ -266,6 +271,17 @@ class OAuthManager:
Users.update_user_role_by_id(user.id, determined_role) Users.update_user_role_by_id(user.id, determined_role)
if not user: if not user:
user_count = Users.get_num_users()
if (
request.app.state.USER_COUNT
and user_count >= request.app.state.USER_COUNT
):
raise HTTPException(
403,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
# If the user does not exist, check if signups are enabled # If the user does not exist, check if signups are enabled
if auth_manager_config.ENABLE_OAUTH_SIGNUP: if auth_manager_config.ENABLE_OAUTH_SIGNUP:
# Check if an existing user with the same email already exists # Check if an existing user with the same email already exists
@ -334,6 +350,7 @@ class OAuthManager:
if auth_manager_config.WEBHOOK_URL: if auth_manager_config.WEBHOOK_URL:
post_webhook( post_webhook(
WEBUI_NAME,
auth_manager_config.WEBHOOK_URL, auth_manager_config.WEBHOOK_URL,
WEBHOOK_MESSAGES.USER_SIGNUP(user.name), WEBHOOK_MESSAGES.USER_SIGNUP(user.name),
{ {
@ -380,6 +397,3 @@ class OAuthManager:
# Redirect back to the frontend with the JWT token # Redirect back to the frontend with the JWT token
redirect_url = f"{request.base_url}auth#token={jwt_token}" redirect_url = f"{request.base_url}auth#token={jwt_token}"
return RedirectResponse(url=redirect_url, headers=response.headers) return RedirectResponse(url=redirect_url, headers=response.headers)
oauth_manager = OAuthManager()

View file

@ -22,7 +22,7 @@ def get_task_model_id(
# Set the task model # Set the task model
task_model_id = default_model_id task_model_id = default_model_id
# Check if the user has a custom task model and use that model # Check if the user has a custom task model and use that model
if models[task_model_id]["owned_by"] == "ollama": if models[task_model_id].get("owned_by") == "ollama":
if task_model and task_model in models: if task_model and task_model in models:
task_model_id = task_model task_model_id = task_model
else: else:

View file

@ -2,14 +2,14 @@ import json
import logging import logging
import requests import requests
from open_webui.config import WEBUI_FAVICON_URL, WEBUI_NAME from open_webui.config import WEBUI_FAVICON_URL
from open_webui.env import SRC_LOG_LEVELS, VERSION from open_webui.env import SRC_LOG_LEVELS, VERSION
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["WEBHOOK"]) log.setLevel(SRC_LOG_LEVELS["WEBHOOK"])
def post_webhook(url: str, message: str, event_data: dict) -> bool: def post_webhook(name: str, url: str, message: str, event_data: dict) -> bool:
try: try:
log.debug(f"post_webhook: {url}, {message}, {event_data}") log.debug(f"post_webhook: {url}, {message}, {event_data}")
payload = {} payload = {}
@ -39,7 +39,7 @@ def post_webhook(url: str, message: str, event_data: dict) -> bool:
"sections": [ "sections": [
{ {
"activityTitle": message, "activityTitle": message,
"activitySubtitle": f"{WEBUI_NAME} ({VERSION}) - {action}", "activitySubtitle": f"{name} ({VERSION}) - {action}",
"activityImage": WEBUI_FAVICON_URL, "activityImage": WEBUI_FAVICON_URL,
"facts": facts, "facts": facts,
"markdown": True, "markdown": True,

1059
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{ {
"name": "open-webui", "name": "open-webui",
"version": "0.5.12", "version": "0.5.13",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "npm run pyodide:fetch && vite dev --host", "dev": "npm run pyodide:fetch && vite dev --host",
@ -26,10 +26,10 @@
"@sveltejs/kit": "^2.5.20", "@sveltejs/kit": "^2.5.20",
"@sveltejs/vite-plugin-svelte": "^3.1.1", "@sveltejs/vite-plugin-svelte": "^3.1.1",
"@tailwindcss/container-queries": "^0.1.1", "@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/postcss": "^4.0.0",
"@tailwindcss/typography": "^0.5.13", "@tailwindcss/typography": "^0.5.13",
"@typescript-eslint/eslint-plugin": "^6.17.0", "@typescript-eslint/eslint-plugin": "^6.17.0",
"@typescript-eslint/parser": "^6.17.0", "@typescript-eslint/parser": "^6.17.0",
"autoprefixer": "^10.4.16",
"cypress": "^13.15.0", "cypress": "^13.15.0",
"eslint": "^8.56.0", "eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
@ -43,7 +43,7 @@
"svelte": "^4.2.18", "svelte": "^4.2.18",
"svelte-check": "^3.8.5", "svelte-check": "^3.8.5",
"svelte-confetti": "^1.3.2", "svelte-confetti": "^1.3.2",
"tailwindcss": "^3.3.3", "tailwindcss": "^4.0.0",
"tslib": "^2.4.1", "tslib": "^2.4.1",
"typescript": "^5.5.4", "typescript": "^5.5.4",
"vite": "^5.4.14", "vite": "^5.4.14",

View file

@ -1,6 +1,5 @@
export default { export default {
plugins: { plugins: {
tailwindcss: {}, '@tailwindcss/postcss': {}
autoprefixer: {}
} }
}; };

View file

@ -1,3 +1,5 @@
@reference "./tailwind.css";
@font-face { @font-face {
font-family: 'Inter'; font-family: 'Inter';
src: url('/assets/fonts/Inter-Variable.ttf'); src: url('/assets/fonts/Inter-Variable.ttf');
@ -53,11 +55,11 @@ math {
} }
.markdown-prose { .markdown-prose {
@apply prose dark:prose-invert prose-blockquote:border-gray-100 prose-blockquote:dark:border-gray-800 prose-blockquote:border-l-2 prose-blockquote:not-italic prose-blockquote:font-normal prose-headings:font-semibold prose-hr:my-4 prose-hr:border-gray-100 prose-hr:dark:border-gray-800 prose-p:my-0 prose-img:my-1 prose-headings:my-1 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-ul:-my-0 prose-ol:-my-0 prose-li:-my-0 whitespace-pre-line; @apply prose dark:prose-invert prose-blockquote:border-s-gray-100 prose-blockquote:dark:border-gray-800 prose-blockquote:border-s-2 prose-blockquote:not-italic prose-blockquote:font-normal prose-headings:font-semibold prose-hr:my-4 prose-hr:border-gray-100 prose-hr:dark:border-gray-800 prose-p:my-0 prose-img:my-1 prose-headings:my-1 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-ul:-my-0 prose-ol:-my-0 prose-li:-my-0 whitespace-pre-line;
} }
.markdown-prose-xs { .markdown-prose-xs {
@apply text-xs prose dark:prose-invert prose-blockquote:border-gray-100 prose-blockquote:dark:border-gray-800 prose-blockquote:border-l-2 prose-blockquote:not-italic prose-blockquote:font-normal prose-headings:font-semibold prose-hr:my-0 prose-hr:border-gray-100 prose-hr:dark:border-gray-800 prose-p:my-0 prose-img:my-1 prose-headings:my-1 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-ul:-my-0 prose-ol:-my-0 prose-li:-my-0 whitespace-pre-line; @apply text-xs prose dark:prose-invert prose-blockquote:border-s-gray-100 prose-blockquote:dark:border-gray-800 prose-blockquote:border-s-2 prose-blockquote:not-italic prose-blockquote:font-normal prose-headings:font-semibold prose-hr:my-0 prose-hr:border-gray-100 prose-hr:dark:border-gray-800 prose-p:my-0 prose-img:my-1 prose-headings:my-1 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-ul:-my-0 prose-ol:-my-0 prose-li:-my-0 whitespace-pre-line;
} }
.markdown a { .markdown a {
@ -217,8 +219,18 @@ input[type='number'] {
width: 100%; width: 100%;
} }
.cm-scroller { .cm-scroller:active::-webkit-scrollbar-thumb,
@apply scrollbar-hidden; .cm-scroller:focus::-webkit-scrollbar-thumb,
.cm-scroller:hover::-webkit-scrollbar-thumb {
visibility: visible;
}
.cm-scroller::-webkit-scrollbar-thumb {
visibility: hidden;
}
.cm-scroller::-webkit-scrollbar-corner {
display: none;
} }
.cm-editor.cm-focused { .cm-editor.cm-focused {

View file

@ -21,6 +21,7 @@
title="Open WebUI" title="Open WebUI"
href="/opensearch.xml" href="/opensearch.xml"
/> />
<script src="/static/loader.js" defer></script>
<script> <script>
function resizeIframe(obj) { function resizeIframe(obj) {

View file

@ -115,10 +115,10 @@ export const setDirectConnectionsConfig = async (token: string, config: object)
return res; return res;
}; };
export const getCodeInterpreterConfig = async (token: string) => { export const getCodeExecutionConfig = async (token: string) => {
let error = null; let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/configs/code_interpreter`, { const res = await fetch(`${WEBUI_API_BASE_URL}/configs/code_execution`, {
method: 'GET', method: 'GET',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -142,10 +142,10 @@ export const getCodeInterpreterConfig = async (token: string) => {
return res; return res;
}; };
export const setCodeInterpreterConfig = async (token: string, config: object) => { export const setCodeExecutionConfig = async (token: string, config: object) => {
let error = null; let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/configs/code_interpreter`, { const res = await fetch(`${WEBUI_API_BASE_URL}/configs/code_execution`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',

View file

@ -284,14 +284,16 @@ export const updateUserInfo = async (token: string, info: object) => {
export const getAndUpdateUserLocation = async (token: string) => { export const getAndUpdateUserLocation = async (token: string) => {
const location = await getUserPosition().catch((err) => { const location = await getUserPosition().catch((err) => {
throw err; console.log(err);
return null;
}); });
if (location) { if (location) {
await updateUserInfo(token, { location: location }); await updateUserInfo(token, { location: location });
return location; return location;
} else { } else {
throw new Error('Failed to get user location'); console.log('Failed to get user location');
return null;
} }
}; };

View file

@ -1,12 +1,13 @@
import { WEBUI_API_BASE_URL } from '$lib/constants'; import { WEBUI_API_BASE_URL } from '$lib/constants';
export const getGravatarUrl = async (email: string) => { export const getGravatarUrl = async (token: string, email: string) => {
let error = null; let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/utils/gravatar?email=${email}`, { const res = await fetch(`${WEBUI_API_BASE_URL}/utils/gravatar?email=${email}`, {
method: 'GET', method: 'GET',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
} }
}) })
.then(async (res) => { .then(async (res) => {
@ -22,13 +23,14 @@ export const getGravatarUrl = async (email: string) => {
return res; return res;
}; };
export const formatPythonCode = async (code: string) => { export const executeCode = async (token: string, code: string) => {
let error = null; let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/utils/code/format`, { const res = await fetch(`${WEBUI_API_BASE_URL}/utils/code/execute`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
}, },
body: JSON.stringify({ body: JSON.stringify({
code: code code: code
@ -55,13 +57,48 @@ export const formatPythonCode = async (code: string) => {
return res; return res;
}; };
export const downloadChatAsPDF = async (title: string, messages: object[]) => { export const formatPythonCode = async (token: string, code: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/utils/code/format`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
},
body: JSON.stringify({
code: code
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
error = err;
if (err.detail) {
error = err.detail;
}
return null;
});
if (error) {
throw error;
}
return res;
};
export const downloadChatAsPDF = async (token: string, title: string, messages: object[]) => {
let error = null; let error = null;
const blob = await fetch(`${WEBUI_API_BASE_URL}/utils/pdf`, { const blob = await fetch(`${WEBUI_API_BASE_URL}/utils/pdf`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
}, },
body: JSON.stringify({ body: JSON.stringify({
title: title, title: title,
@ -81,13 +118,14 @@ export const downloadChatAsPDF = async (title: string, messages: object[]) => {
return blob; return blob;
}; };
export const getHTMLFromMarkdown = async (md: string) => { export const getHTMLFromMarkdown = async (token: string, md: string) => {
let error = null; let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/utils/markdown`, { const res = await fetch(`${WEBUI_API_BASE_URL}/utils/markdown`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
}, },
body: JSON.stringify({ body: JSON.stringify({
md: md md: md

View file

@ -169,7 +169,7 @@
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none" class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
type="text" type="text"
bind:value={url} bind:value={url}
placeholder={$i18n.t('API Base URL')} placeholder={$i18n.t('API Base URL')}
@ -202,7 +202,7 @@
</button> </button>
</Tooltip> </Tooltip>
<div class="flex flex-col flex-shrink-0 self-end"> <div class="flex flex-col shrink-0 self-end">
<Tooltip content={enable ? $i18n.t('Enabled') : $i18n.t('Disabled')}> <Tooltip content={enable ? $i18n.t('Enabled') : $i18n.t('Disabled')}>
<Switch bind:state={enable} /> <Switch bind:state={enable} />
</Tooltip> </Tooltip>
@ -215,7 +215,7 @@
<div class="flex-1"> <div class="flex-1">
<SensitiveInput <SensitiveInput
className="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none" className="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
bind:value={key} bind:value={key}
placeholder={$i18n.t('API Key')} placeholder={$i18n.t('API Key')}
required={!ollama} required={!ollama}
@ -233,7 +233,7 @@
)} )}
> >
<input <input
class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none" class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
type="text" type="text"
bind:value={prefixId} bind:value={prefixId}
placeholder={$i18n.t('Prefix ID')} placeholder={$i18n.t('Prefix ID')}
@ -258,7 +258,7 @@
<div class=" text-sm flex-1 py-1 rounded-lg"> <div class=" text-sm flex-1 py-1 rounded-lg">
{modelId} {modelId}
</div> </div>
<div class="flex-shrink-0"> <div class="shrink-0">
<button <button
type="button" type="button"
on:click={() => { on:click={() => {
@ -292,7 +292,7 @@
<input <input
class="w-full py-1 text-sm rounded-lg bg-transparent {modelId class="w-full py-1 text-sm rounded-lg bg-transparent {modelId
? '' ? ''
: 'text-gray-500'} placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none" : 'text-gray-500'} placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
bind:value={modelId} bind:value={modelId}
placeholder={$i18n.t('Add a model ID')} placeholder={$i18n.t('Add a model ID')}
/> />

View file

@ -68,7 +68,7 @@
v{version} - {changelog[version].date} v{version} - {changelog[version].date}
</div> </div>
<hr class=" dark:border-gray-800 my-2" /> <hr class="border-gray-100 dark:border-gray-850 my-2" />
{#each Object.keys(changelog[version]).filter((section) => section !== 'date') as section} {#each Object.keys(changelog[version]).filter((section) => section !== 'date') as section}
<div class=""> <div class="">

View file

@ -31,13 +31,13 @@
</script> </script>
<button <button
class="flex gap-2.5 text-left min-w-[var(--width)] w-full dark:bg-gray-850 dark:text-white bg-white text-black border border-gray-50 dark:border-gray-800 rounded-xl px-3.5 py-3.5" class="flex gap-2.5 text-left min-w-[var(--width)] w-full dark:bg-gray-850 dark:text-white bg-white text-black border border-gray-100 dark:border-gray-850 rounded-xl px-3.5 py-3.5"
on:click={() => { on:click={() => {
onClick(); onClick();
dispatch('closeToast'); dispatch('closeToast');
}} }}
> >
<div class="flex-shrink-0 self-top -translate-y-0.5"> <div class="shrink-0 self-top -translate-y-0.5">
<img src={'/static/favicon.png'} alt="favicon" class="size-7 rounded-full" /> <img src={'/static/favicon.png'} alt="favicon" class="size-7 rounded-full" />
</div> </div>

View file

@ -30,10 +30,10 @@
<SlideShow duration={5000} /> <SlideShow duration={5000} />
<div <div
class="w-full h-full absolute top-0 left-0 bg-gradient-to-t from-20% from-black to-transparent" class="w-full h-full absolute top-0 left-0 bg-linear-to-t from-20% from-black to-transparent"
></div> ></div>
<div class="w-full h-full absolute top-0 left-0 backdrop-blur-sm bg-black/50"></div> <div class="w-full h-full absolute top-0 left-0 backdrop-blur-xs bg-black/50"></div>
<div class="relative bg-transparent w-full min-h-screen flex z-10"> <div class="relative bg-transparent w-full min-h-screen flex z-10">
<div class="flex flex-col justify-end w-full items-center pb-10 text-center"> <div class="flex flex-col justify-end w-full items-center pb-10 text-center">

View file

@ -131,14 +131,16 @@
</div> </div>
</div> </div>
<div class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full rounded pt-0.5"> <div
class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full rounded-sm pt-0.5"
>
{#if (feedbacks ?? []).length === 0} {#if (feedbacks ?? []).length === 0}
<div class="text-center text-xs text-gray-500 dark:text-gray-400 py-1"> <div class="text-center text-xs text-gray-500 dark:text-gray-400 py-1">
{$i18n.t('No feedbacks found')} {$i18n.t('No feedbacks found')}
</div> </div>
{:else} {:else}
<table <table
class="w-full text-sm text-left text-gray-500 dark:text-gray-400 table-auto max-w-full rounded" class="w-full text-sm text-left text-gray-500 dark:text-gray-400 table-auto max-w-full rounded-sm"
> >
<thead <thead
class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-850 dark:text-gray-400 -translate-y-0.5" class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-850 dark:text-gray-400 -translate-y-0.5"
@ -169,7 +171,7 @@
<td class=" py-0.5 text-right font-semibold"> <td class=" py-0.5 text-right font-semibold">
<div class="flex justify-center"> <div class="flex justify-center">
<Tooltip content={feedback?.user?.name}> <Tooltip content={feedback?.user?.name}>
<div class="flex-shrink-0"> <div class="shrink-0">
<img <img
src={feedback?.user?.profile_image_url ?? '/user.png'} src={feedback?.user?.profile_image_url ?? '/user.png'}
alt={feedback?.user?.name} alt={feedback?.user?.name}

View file

@ -288,7 +288,7 @@
<MagnifyingGlass className="size-3" /> <MagnifyingGlass className="size-3" />
</div> </div>
<input <input
class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-none bg-transparent" class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-hidden bg-transparent"
bind:value={query} bind:value={query}
placeholder={$i18n.t('Search')} placeholder={$i18n.t('Search')}
on:focus={() => { on:focus={() => {
@ -300,7 +300,9 @@
</div> </div>
</div> </div>
<div class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full rounded pt-0.5"> <div
class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full rounded-sm pt-0.5"
>
{#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">
@ -349,7 +351,7 @@
</td> </td>
<td class="px-3 py-1.5 flex flex-col justify-center"> <td class="px-3 py-1.5 flex flex-col justify-center">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div class="flex-shrink-0"> <div class="shrink-0">
<img <img
src={model?.info?.meta?.profile_image_url ?? '/favicon.png'} src={model?.info?.meta?.profile_image_url ?? '/favicon.png'}
alt={model.name} alt={model.name}

View file

@ -180,12 +180,12 @@
window.addEventListener('keydown', onKeyDown); window.addEventListener('keydown', onKeyDown);
window.addEventListener('keyup', onKeyUp); window.addEventListener('keyup', onKeyUp);
window.addEventListener('blur', onBlur); window.addEventListener('blur-sm', onBlur);
return () => { return () => {
window.removeEventListener('keydown', onKeyDown); window.removeEventListener('keydown', onKeyDown);
window.removeEventListener('keyup', onKeyUp); window.removeEventListener('keyup', onKeyUp);
window.removeEventListener('blur', onBlur); window.removeEventListener('blur-sm', onBlur);
}; };
}); });
</script> </script>
@ -211,7 +211,7 @@
<Search className="size-3.5" /> <Search className="size-3.5" />
</div> </div>
<input <input
class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-none bg-transparent" class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-hidden bg-transparent"
bind:value={query} bind:value={query}
placeholder={$i18n.t('Search Functions')} placeholder={$i18n.t('Search Functions')}
/> />
@ -241,14 +241,14 @@
<div class=" flex-1 self-center pl-1"> <div class=" flex-1 self-center pl-1">
<div class=" font-semibold flex items-center gap-1.5"> <div class=" font-semibold flex items-center gap-1.5">
<div <div
class=" text-xs font-bold px-1 rounded uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200" class=" text-xs font-bold px-1 rounded-sm uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
> >
{func.type} {func.type}
</div> </div>
{#if func?.meta?.manifest?.version} {#if func?.meta?.manifest?.version}
<div <div
class="text-xs font-bold px-1 rounded line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200" class="text-xs font-bold px-1 rounded-sm line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
> >
v{func?.meta?.manifest?.version ?? ''} v{func?.meta?.manifest?.version ?? ''}
</div> </div>
@ -260,7 +260,7 @@
</div> </div>
<div class="flex gap-1.5 px-1"> <div class="flex gap-1.5 px-1">
<div class=" text-gray-500 text-xs font-medium flex-shrink-0">{func.id}</div> <div class=" text-gray-500 text-xs font-medium shrink-0">{func.id}</div>
<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1"> <div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
{func.meta.description} {func.meta.description}

View file

@ -300,7 +300,7 @@ class Pipe:
<div class="flex flex-col flex-1 overflow-auto h-0 rounded-lg"> <div class="flex flex-col flex-1 overflow-auto h-0 rounded-lg">
<div class="w-full mb-2 flex flex-col gap-0.5"> <div class="w-full mb-2 flex flex-col gap-0.5">
<div class="flex w-full items-center"> <div class="flex w-full items-center">
<div class=" flex-shrink-0 mr-2"> <div class=" shrink-0 mr-2">
<Tooltip content={$i18n.t('Back')}> <Tooltip content={$i18n.t('Back')}>
<button <button
class="w-full text-left text-sm py-1.5 px-1 rounded-lg dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-gray-850" class="w-full text-left text-sm py-1.5 px-1 rounded-lg dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-gray-850"
@ -317,7 +317,7 @@ class Pipe:
<div class="flex-1"> <div class="flex-1">
<Tooltip content={$i18n.t('e.g. My Filter')} placement="top-start"> <Tooltip content={$i18n.t('e.g. My Filter')} placement="top-start">
<input <input
class="w-full text-2xl font-medium bg-transparent outline-none font-primary" class="w-full text-2xl font-medium bg-transparent outline-hidden font-primary"
type="text" type="text"
placeholder={$i18n.t('Function Name')} placeholder={$i18n.t('Function Name')}
bind:value={name} bind:value={name}
@ -333,13 +333,13 @@ class Pipe:
<div class=" flex gap-2 px-1 items-center"> <div class=" flex gap-2 px-1 items-center">
{#if edit} {#if edit}
<div class="text-sm text-gray-500 flex-shrink-0"> <div class="text-sm text-gray-500 shrink-0">
{id} {id}
</div> </div>
{:else} {:else}
<Tooltip className="w-full" content={$i18n.t('e.g. my_filter')} placement="top-start"> <Tooltip className="w-full" content={$i18n.t('e.g. my_filter')} placement="top-start">
<input <input
class="w-full text-sm disabled:text-gray-500 bg-transparent outline-none" class="w-full text-sm disabled:text-gray-500 bg-transparent outline-hidden"
type="text" type="text"
placeholder={$i18n.t('Function ID')} placeholder={$i18n.t('Function ID')}
bind:value={id} bind:value={id}
@ -355,7 +355,7 @@ class Pipe:
placement="top-start" placement="top-start"
> >
<input <input
class="w-full text-sm bg-transparent outline-none" class="w-full text-sm bg-transparent outline-hidden"
type="text" type="text"
placeholder={$i18n.t('Function Description')} placeholder={$i18n.t('Function Description')}
bind:value={meta.description} bind:value={meta.description}

View file

@ -42,7 +42,7 @@
<div slot="content"> <div slot="content">
<DropdownMenu.Content <DropdownMenu.Content
class="w-full max-w-[180px] rounded-xl px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow" class="w-full max-w-[180px] rounded-xl px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-sm"
sideOffset={-2} sideOffset={-2}
side="bottom" side="bottom"
align="start" align="start"
@ -63,7 +63,7 @@
</div> </div>
</div> </div>
<hr class="border-gray-100 dark:border-gray-800 my-1" /> <hr class="border-gray-100 dark:border-gray-850 my-1" />
{/if} {/if}
<DropdownMenu.Item <DropdownMenu.Item
@ -122,7 +122,7 @@
<div class="flex items-center">{$i18n.t('Export')}</div> <div class="flex items-center">{$i18n.t('Export')}</div>
</DropdownMenu.Item> </DropdownMenu.Item>
<hr class="border-gray-100 dark:border-gray-800 my-1" /> <hr class="border-gray-100 dark:border-gray-850 my-1" />
<DropdownMenu.Item <DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md" class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"

View file

@ -19,7 +19,7 @@
import ChartBar from '../icons/ChartBar.svelte'; import ChartBar from '../icons/ChartBar.svelte';
import DocumentChartBar from '../icons/DocumentChartBar.svelte'; import DocumentChartBar from '../icons/DocumentChartBar.svelte';
import Evaluations from './Settings/Evaluations.svelte'; import Evaluations from './Settings/Evaluations.svelte';
import CodeInterpreter from './Settings/CodeInterpreter.svelte'; import CodeExecution from './Settings/CodeExecution.svelte';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
@ -191,11 +191,11 @@
<button <button
class="px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === class="px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
'code-interpreter' 'code-execution'
? '' ? ''
: ' text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}" : ' text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}"
on:click={() => { on:click={() => {
selectedTab = 'code-interpreter'; selectedTab = 'code-execution';
}} }}
> >
<div class=" self-center mr-2"> <div class=" self-center mr-2">
@ -212,7 +212,7 @@
/> />
</svg> </svg>
</div> </div>
<div class=" self-center">{$i18n.t('Code Interpreter')}</div> <div class=" self-center">{$i18n.t('Code Execution')}</div>
</button> </button>
<button <button
@ -391,8 +391,8 @@
await config.set(await getBackendConfig()); await config.set(await getBackendConfig());
}} }}
/> />
{:else if selectedTab === 'code-interpreter'} {:else if selectedTab === 'code-execution'}
<CodeInterpreter <CodeExecution
saveHandler={async () => { saveHandler={async () => {
toast.success($i18n.t('Settings saved successfully!')); toast.success($i18n.t('Settings saved successfully!'));

View file

@ -172,7 +172,7 @@
<div class=" self-center text-xs font-medium">{$i18n.t('Speech-to-Text Engine')}</div> <div class=" self-center text-xs font-medium">{$i18n.t('Speech-to-Text Engine')}</div>
<div class="flex items-center relative"> <div class="flex items-center relative">
<select <select
class="dark:bg-gray-900 cursor-pointer w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right" class="dark:bg-gray-900 cursor-pointer w-fit pr-8 rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right"
bind:value={STT_ENGINE} bind:value={STT_ENGINE}
placeholder="Select an engine" placeholder="Select an engine"
> >
@ -188,7 +188,7 @@
<div> <div>
<div class="mt-1 flex gap-2 mb-1"> <div class="mt-1 flex gap-2 mb-1">
<input <input
class="flex-1 w-full bg-transparent outline-none" class="flex-1 w-full bg-transparent outline-hidden"
placeholder={$i18n.t('API Base URL')} placeholder={$i18n.t('API Base URL')}
bind:value={STT_OPENAI_API_BASE_URL} bind:value={STT_OPENAI_API_BASE_URL}
required required
@ -198,7 +198,7 @@
</div> </div>
</div> </div>
<hr class=" dark:border-gray-850 my-2" /> <hr class="border-gray-100 dark:border-gray-850 my-2" />
<div> <div>
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('STT Model')}</div> <div class=" mb-1.5 text-sm font-medium">{$i18n.t('STT Model')}</div>
@ -206,7 +206,7 @@
<div class="flex-1"> <div class="flex-1">
<input <input
list="model-list" list="model-list"
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
bind:value={STT_MODEL} bind:value={STT_MODEL}
placeholder="Select a model" placeholder="Select a model"
/> />
@ -224,14 +224,14 @@
</div> </div>
</div> </div>
<hr class=" dark:border-gray-850 my-2" /> <hr class="border-gray-100 dark:border-gray-850 my-2" />
<div> <div>
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('STT Model')}</div> <div class=" mb-1.5 text-sm font-medium">{$i18n.t('STT Model')}</div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
bind:value={STT_MODEL} bind:value={STT_MODEL}
placeholder="Select a model (optional)" placeholder="Select a model (optional)"
/> />
@ -255,7 +255,7 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
placeholder={$i18n.t('Set whisper model')} placeholder={$i18n.t('Set whisper model')}
bind:value={STT_WHISPER_MODEL} bind:value={STT_WHISPER_MODEL}
/> />
@ -333,7 +333,7 @@
{/if} {/if}
</div> </div>
<hr class=" dark:border-gray-800" /> <hr class="border-gray-100 dark:border-gray-850" />
<div> <div>
<div class=" mb-1 text-sm font-medium">{$i18n.t('TTS Settings')}</div> <div class=" mb-1 text-sm font-medium">{$i18n.t('TTS Settings')}</div>
@ -342,7 +342,7 @@
<div class=" self-center text-xs font-medium">{$i18n.t('Text-to-Speech Engine')}</div> <div class=" self-center text-xs font-medium">{$i18n.t('Text-to-Speech Engine')}</div>
<div class="flex items-center relative"> <div class="flex items-center relative">
<select <select
class=" dark:bg-gray-900 w-fit pr-8 cursor-pointer rounded px-2 p-1 text-xs bg-transparent outline-none text-right" class=" dark:bg-gray-900 w-fit pr-8 cursor-pointer rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right"
bind:value={TTS_ENGINE} bind:value={TTS_ENGINE}
placeholder="Select a mode" placeholder="Select a mode"
on:change={async (e) => { on:change={async (e) => {
@ -372,7 +372,7 @@
<div> <div>
<div class="mt-1 flex gap-2 mb-1"> <div class="mt-1 flex gap-2 mb-1">
<input <input
class="flex-1 w-full bg-transparent outline-none" class="flex-1 w-full bg-transparent outline-hidden"
placeholder={$i18n.t('API Base URL')} placeholder={$i18n.t('API Base URL')}
bind:value={TTS_OPENAI_API_BASE_URL} bind:value={TTS_OPENAI_API_BASE_URL}
required required
@ -385,7 +385,7 @@
<div> <div>
<div class="mt-1 flex gap-2 mb-1"> <div class="mt-1 flex gap-2 mb-1">
<input <input
class="flex-1 w-full rounded-lg py-2 pl-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" class="flex-1 w-full rounded-lg py-2 pl-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
placeholder={$i18n.t('API Key')} placeholder={$i18n.t('API Key')}
bind:value={TTS_API_KEY} bind:value={TTS_API_KEY}
required required
@ -396,13 +396,13 @@
<div> <div>
<div class="mt-1 flex gap-2 mb-1"> <div class="mt-1 flex gap-2 mb-1">
<input <input
class="flex-1 w-full rounded-lg py-2 pl-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" class="flex-1 w-full rounded-lg py-2 pl-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
placeholder={$i18n.t('API Key')} placeholder={$i18n.t('API Key')}
bind:value={TTS_API_KEY} bind:value={TTS_API_KEY}
required required
/> />
<input <input
class="flex-1 w-full rounded-lg py-2 pl-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" class="flex-1 w-full rounded-lg py-2 pl-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
placeholder={$i18n.t('Azure Region')} placeholder={$i18n.t('Azure Region')}
bind:value={TTS_AZURE_SPEECH_REGION} bind:value={TTS_AZURE_SPEECH_REGION}
required required
@ -411,7 +411,7 @@
</div> </div>
{/if} {/if}
<hr class=" dark:border-gray-850 my-2" /> <hr class="border-gray-100 dark:border-gray-850 my-2" />
{#if TTS_ENGINE === ''} {#if TTS_ENGINE === ''}
<div> <div>
@ -419,7 +419,7 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<select <select
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
bind:value={TTS_VOICE} bind:value={TTS_VOICE}
> >
<option value="" selected={TTS_VOICE !== ''}>{$i18n.t('Default')}</option> <option value="" selected={TTS_VOICE !== ''}>{$i18n.t('Default')}</option>
@ -442,7 +442,7 @@
<div class="flex-1"> <div class="flex-1">
<input <input
list="model-list" list="model-list"
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
bind:value={TTS_MODEL} bind:value={TTS_MODEL}
placeholder="CMU ARCTIC speaker embedding name" placeholder="CMU ARCTIC speaker embedding name"
/> />
@ -484,7 +484,7 @@
<div class="flex-1"> <div class="flex-1">
<input <input
list="voice-list" list="voice-list"
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
bind:value={TTS_VOICE} bind:value={TTS_VOICE}
placeholder="Select a voice" placeholder="Select a voice"
/> />
@ -503,7 +503,7 @@
<div class="flex-1"> <div class="flex-1">
<input <input
list="tts-model-list" list="tts-model-list"
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
bind:value={TTS_MODEL} bind:value={TTS_MODEL}
placeholder="Select a model" placeholder="Select a model"
/> />
@ -525,7 +525,7 @@
<div class="flex-1"> <div class="flex-1">
<input <input
list="voice-list" list="voice-list"
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
bind:value={TTS_VOICE} bind:value={TTS_VOICE}
placeholder="Select a voice" placeholder="Select a voice"
/> />
@ -544,7 +544,7 @@
<div class="flex-1"> <div class="flex-1">
<input <input
list="tts-model-list" list="tts-model-list"
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
bind:value={TTS_MODEL} bind:value={TTS_MODEL}
placeholder="Select a model" placeholder="Select a model"
/> />
@ -566,7 +566,7 @@
<div class="flex-1"> <div class="flex-1">
<input <input
list="voice-list" list="voice-list"
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
bind:value={TTS_VOICE} bind:value={TTS_VOICE}
placeholder="Select a voice" placeholder="Select a voice"
/> />
@ -593,7 +593,7 @@
<div class="flex-1"> <div class="flex-1">
<input <input
list="tts-model-list" list="tts-model-list"
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
bind:value={TTS_AZURE_SPEECH_OUTPUT_FORMAT} bind:value={TTS_AZURE_SPEECH_OUTPUT_FORMAT}
placeholder="Select a output format" placeholder="Select a output format"
/> />
@ -603,13 +603,13 @@
</div> </div>
{/if} {/if}
<hr class="dark:border-gray-850 my-2" /> <hr class="border-gray-100 dark:border-gray-850 my-2" />
<div class="pt-0.5 flex w-full justify-between"> <div class="pt-0.5 flex w-full justify-between">
<div class="self-center text-xs font-medium">{$i18n.t('Response splitting')}</div> <div class="self-center text-xs font-medium">{$i18n.t('Response splitting')}</div>
<div class="flex items-center relative"> <div class="flex items-center relative">
<select <select
class="dark:bg-gray-900 w-fit pr-8 cursor-pointer rounded px-2 p-1 text-xs bg-transparent outline-none text-right" class="dark:bg-gray-900 w-fit pr-8 cursor-pointer rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right"
aria-label="Select how to split message text for TTS requests" aria-label="Select how to split message text for TTS requests"
bind:value={TTS_SPLIT_ON} bind:value={TTS_SPLIT_ON}
> >

View file

@ -0,0 +1,277 @@
<script lang="ts">
import { toast } from 'svelte-sonner';
import { onMount, getContext } from 'svelte';
import { getCodeExecutionConfig, setCodeExecutionConfig } from '$lib/apis/configs';
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import Textarea from '$lib/components/common/Textarea.svelte';
import Switch from '$lib/components/common/Switch.svelte';
const i18n = getContext('i18n');
export let saveHandler: Function;
let config = null;
let engines = ['pyodide', 'jupyter'];
const submitHandler = async () => {
const res = await setCodeExecutionConfig(localStorage.token, config);
};
onMount(async () => {
const res = await getCodeExecutionConfig(localStorage.token);
if (res) {
config = res;
}
});
</script>
<form
class="flex flex-col h-full justify-between space-y-3 text-sm"
on:submit|preventDefault={async () => {
await submitHandler();
saveHandler();
}}
>
<div class=" space-y-3 overflow-y-scroll scrollbar-hidden h-full">
{#if config}
<div>
<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="mb-2.5">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Code Execution Engine')}</div>
<div class="flex items-center relative">
<select
class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right"
bind:value={config.CODE_EXECUTION_ENGINE}
placeholder={$i18n.t('Select a engine')}
required
>
<option disabled selected value="">{$i18n.t('Select a engine')}</option>
{#each engines as engine}
<option value={engine}>{engine}</option>
{/each}
</select>
</div>
</div>
{#if config.CODE_EXECUTION_ENGINE === 'jupyter'}
<div class="text-gray-500 text-xs">
{$i18n.t(
'Warning: Jupyter execution enables arbitrary code execution, posing severe security risks—proceed with extreme caution.'
)}
</div>
{/if}
</div>
{#if config.CODE_EXECUTION_ENGINE === 'jupyter'}
<div class="mb-2.5 flex flex-col gap-1.5 w-full">
<div class="text-xs font-medium">
{$i18n.t('Jupyter URL')}
</div>
<div class="flex w-full">
<div class="flex-1">
<input
class="w-full text-sm py-0.5 placeholder:text-gray-300 dark:placeholder:text-gray-700 bg-transparent outline-hidden"
type="text"
placeholder={$i18n.t('Enter Jupyter URL')}
bind:value={config.CODE_EXECUTION_JUPYTER_URL}
autocomplete="off"
/>
</div>
</div>
</div>
<div class=" flex gap-2 w-full items-center justify-between">
<div class="text-xs font-medium">
{$i18n.t('Jupyter Auth')}
</div>
<div>
<select
class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-left"
bind:value={config.CODE_EXECUTION_JUPYTER_AUTH}
placeholder={$i18n.t('Select an auth method')}
>
<option selected value="">{$i18n.t('None')}</option>
<option value="token">{$i18n.t('Token')}</option>
<option value="password">{$i18n.t('Password')}</option>
</select>
</div>
</div>
{#if config.CODE_EXECUTION_JUPYTER_AUTH}
<div class="flex w-full gap-2">
<div class="flex-1">
{#if config.CODE_EXECUTION_JUPYTER_AUTH === 'password'}
<SensitiveInput
type="text"
placeholder={$i18n.t('Enter Jupyter Password')}
bind:value={config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD}
autocomplete="off"
/>
{:else}
<SensitiveInput
type="text"
placeholder={$i18n.t('Enter Jupyter Token')}
bind:value={config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN}
autocomplete="off"
/>
{/if}
</div>
</div>
{/if}
{/if}
</div>
<div class="mb-3.5">
<div class=" mb-2.5 text-base font-medium">{$i18n.t('Code Interpreter')}</div>
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
<div class="mb-2.5">
<div class=" flex w-full justify-between">
<div class=" self-center text-xs font-medium">
{$i18n.t('Enable Code Interpreter')}
</div>
<Switch bind:state={config.ENABLE_CODE_INTERPRETER} />
</div>
</div>
{#if config.ENABLE_CODE_INTERPRETER}
<div class="mb-2.5">
<div class=" flex w-full justify-between">
<div class=" self-center text-xs font-medium">
{$i18n.t('Code Interpreter Engine')}
</div>
<div class="flex items-center relative">
<select
class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right"
bind:value={config.CODE_INTERPRETER_ENGINE}
placeholder={$i18n.t('Select a engine')}
required
>
<option disabled selected value="">{$i18n.t('Select a engine')}</option>
{#each engines as engine}
<option value={engine}>{engine}</option>
{/each}
</select>
</div>
</div>
{#if config.CODE_INTERPRETER_ENGINE === 'jupyter'}
<div class="text-gray-500 text-xs">
{$i18n.t(
'Warning: Jupyter execution enables arbitrary code execution, posing severe security risks—proceed with extreme caution.'
)}
</div>
{/if}
</div>
{#if config.CODE_INTERPRETER_ENGINE === 'jupyter'}
<div class="mb-2.5 flex flex-col gap-1.5 w-full">
<div class="text-xs font-medium">
{$i18n.t('Jupyter URL')}
</div>
<div class="flex w-full">
<div class="flex-1">
<input
class="w-full text-sm py-0.5 placeholder:text-gray-300 dark:placeholder:text-gray-700 bg-transparent outline-hidden"
type="text"
placeholder={$i18n.t('Enter Jupyter URL')}
bind:value={config.CODE_INTERPRETER_JUPYTER_URL}
autocomplete="off"
/>
</div>
</div>
</div>
<div class="flex gap-2 w-full items-center justify-between">
<div class="text-xs font-medium">
{$i18n.t('Jupyter Auth')}
</div>
<div>
<select
class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-left"
bind:value={config.CODE_INTERPRETER_JUPYTER_AUTH}
placeholder={$i18n.t('Select an auth method')}
>
<option selected value="">{$i18n.t('None')}</option>
<option value="token">{$i18n.t('Token')}</option>
<option value="password">{$i18n.t('Password')}</option>
</select>
</div>
</div>
{#if config.CODE_INTERPRETER_JUPYTER_AUTH}
<div class="flex w-full gap-2">
<div class="flex-1">
{#if config.CODE_INTERPRETER_JUPYTER_AUTH === 'password'}
<SensitiveInput
type="text"
placeholder={$i18n.t('Enter Jupyter Password')}
bind:value={config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD}
autocomplete="off"
/>
{:else}
<SensitiveInput
type="text"
placeholder={$i18n.t('Enter Jupyter Token')}
bind:value={config.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN}
autocomplete="off"
/>
{/if}
</div>
</div>
{/if}
{/if}
<hr class="border-gray-100 dark:border-gray-850 my-2" />
<div>
<div class="py-0.5 w-full">
<div class=" mb-2.5 text-xs font-medium">
{$i18n.t('Code Interpreter Prompt Template')}
</div>
<Tooltip
content={$i18n.t(
'Leave empty to use the default prompt, or enter a custom prompt'
)}
placement="top-start"
>
<Textarea
bind:value={config.CODE_INTERPRETER_PROMPT_TEMPLATE}
placeholder={$i18n.t(
'Leave empty to use the default prompt, or enter a custom prompt'
)}
/>
</Tooltip>
</div>
</div>
{/if}
</div>
</div>
{/if}
</div>
<div class="flex justify-end pt-3 text-sm font-medium">
<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"
type="submit"
>
{$i18n.t('Save')}
</button>
</div>
</form>

View file

@ -1,166 +0,0 @@
<script lang="ts">
import { toast } from 'svelte-sonner';
import { onMount, getContext } from 'svelte';
import { getCodeInterpreterConfig, setCodeInterpreterConfig } from '$lib/apis/configs';
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import Textarea from '$lib/components/common/Textarea.svelte';
import Switch from '$lib/components/common/Switch.svelte';
const i18n = getContext('i18n');
export let saveHandler: Function;
let config = null;
let engines = ['pyodide', 'jupyter'];
const submitHandler = async () => {
const res = await setCodeInterpreterConfig(localStorage.token, config);
};
onMount(async () => {
const res = await getCodeInterpreterConfig(localStorage.token);
if (res) {
config = res;
}
});
</script>
<form
class="flex flex-col h-full justify-between space-y-3 text-sm"
on:submit|preventDefault={async () => {
await submitHandler();
saveHandler();
}}
>
<div class=" space-y-3 overflow-y-scroll scrollbar-hidden h-full">
{#if config}
<div>
<div class=" mb-1 text-sm font-medium">
{$i18n.t('Code Interpreter')}
</div>
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">
{$i18n.t('Enable Code Interpreter')}
</div>
<Switch bind:state={config.ENABLE_CODE_INTERPRETER} />
</div>
</div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Code Interpreter Engine')}</div>
<div class="flex items-center relative">
<select
class="dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
bind:value={config.CODE_INTERPRETER_ENGINE}
placeholder={$i18n.t('Select a engine')}
required
>
<option disabled selected value="">{$i18n.t('Select a engine')}</option>
{#each engines as engine}
<option value={engine}>{engine}</option>
{/each}
</select>
</div>
</div>
{#if config.CODE_INTERPRETER_ENGINE === 'jupyter'}
<div class="mt-1 flex flex-col gap-1.5 mb-1 w-full">
<div class="text-xs font-medium">
{$i18n.t('Jupyter URL')}
</div>
<div class="flex w-full">
<div class="flex-1">
<input
class="w-full text-sm py-0.5 placeholder:text-gray-300 dark:placeholder:text-gray-700 bg-transparent outline-none"
type="text"
placeholder={$i18n.t('Enter Jupyter URL')}
bind:value={config.CODE_INTERPRETER_JUPYTER_URL}
autocomplete="off"
/>
</div>
</div>
</div>
<div class="mt-1 flex gap-2 mb-1 w-full items-center justify-between">
<div class="text-xs font-medium">
{$i18n.t('Jupyter Auth')}
</div>
<div>
<select
class="dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-left"
bind:value={config.CODE_INTERPRETER_JUPYTER_AUTH}
placeholder={$i18n.t('Select an auth method')}
>
<option selected value="">{$i18n.t('None')}</option>
<option value="token">{$i18n.t('Token')}</option>
<option value="password">{$i18n.t('Password')}</option>
</select>
</div>
</div>
{#if config.CODE_INTERPRETER_JUPYTER_AUTH}
<div class="flex w-full gap-2">
<div class="flex-1">
{#if config.CODE_INTERPRETER_JUPYTER_AUTH === 'password'}
<SensitiveInput
type="text"
placeholder={$i18n.t('Enter Jupyter Password')}
bind:value={config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD}
autocomplete="off"
/>
{:else}
<SensitiveInput
type="text"
placeholder={$i18n.t('Enter Jupyter Token')}
bind:value={config.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN}
autocomplete="off"
/>
{/if}
</div>
</div>
{/if}
{/if}
</div>
<hr class=" dark:border-gray-850 my-2" />
<div>
<div class="py-0.5 w-full">
<div class=" mb-2.5 text-xs font-medium">
{$i18n.t('Code Interpreter Prompt Template')}
</div>
<Tooltip
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
placement="top-start"
>
<Textarea
bind:value={config.CODE_INTERPRETER_PROMPT_TEMPLATE}
placeholder={$i18n.t(
'Leave empty to use the default prompt, or enter a custom prompt'
)}
/>
</Tooltip>
</div>
</div>
{/if}
</div>
<div class="flex justify-end pt-3 text-sm font-medium">
<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"
type="submit"
>
{$i18n.t('Save')}
</button>
</div>
</form>

View file

@ -234,7 +234,7 @@
</div> </div>
{#if ENABLE_OPENAI_API} {#if ENABLE_OPENAI_API}
<hr class=" border-gray-50 dark:border-gray-850" /> <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">
@ -283,7 +283,7 @@
</div> </div>
</div> </div>
<hr class=" border-gray-50 dark:border-gray-850" /> <hr class=" border-gray-100 dark:border-gray-850" />
<div class="pr-1.5 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">
@ -300,7 +300,7 @@
</div> </div>
{#if ENABLE_OLLAMA_API} {#if ENABLE_OLLAMA_API}
<hr class=" border-gray-50 dark:border-gray-850 my-2" /> <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">
@ -357,7 +357,7 @@
{/if} {/if}
</div> </div>
<hr class=" border-gray-50 dark:border-gray-850" /> <hr class=" border-gray-100 dark:border-gray-850" />
<div class="pr-1.5 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">

View file

@ -16,7 +16,7 @@
<div <div
class="flex w-full justify-between items-center text-lg font-medium self-center font-primary" class="flex w-full justify-between items-center text-lg font-medium self-center font-primary"
> >
<div class=" flex-shrink-0"> <div class=" shrink-0">
{$i18n.t('Manage Ollama')} {$i18n.t('Manage Ollama')}
</div> </div>
</div> </div>

View file

@ -56,7 +56,7 @@
{/if} {/if}
<input <input
class="w-full text-sm bg-transparent outline-none" class="w-full text-sm bg-transparent outline-hidden"
placeholder={$i18n.t('Enter URL (e.g. http://localhost:11434)')} placeholder={$i18n.t('Enter URL (e.g. http://localhost:11434)')}
bind:value={url} bind:value={url}
/> />

View file

@ -54,7 +54,7 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1 relative"> <div class="flex-1 relative">
<input <input
class=" outline-none w-full bg-transparent {pipeline ? 'pr-8' : ''}" class=" outline-hidden w-full bg-transparent {pipeline ? 'pr-8' : ''}"
placeholder={$i18n.t('API Base URL')} placeholder={$i18n.t('API Base URL')}
bind:value={url} bind:value={url}
autocomplete="off" autocomplete="off"
@ -85,7 +85,7 @@
</div> </div>
<SensitiveInput <SensitiveInput
inputClassName=" outline-none bg-transparent w-full" inputClassName=" outline-hidden bg-transparent w-full"
placeholder={$i18n.t('API Key')} placeholder={$i18n.t('API Key')}
bind:value={key} bind:value={key}
/> />

View file

@ -119,7 +119,7 @@
</div> </div>
</button> </button>
<hr class=" dark:border-gray-850 my-1" /> <hr class="border-gray-100 dark:border-gray-850 my-1" />
{#if $config?.features.enable_admin_export ?? true} {#if $config?.features.enable_admin_export ?? true}
<div class=" flex w-full justify-between"> <div class=" flex w-full justify-between">

View file

@ -296,7 +296,7 @@
<div class=" self-center text-xs font-medium">{$i18n.t('Embedding Model Engine')}</div> <div class=" self-center text-xs font-medium">{$i18n.t('Embedding Model Engine')}</div>
<div class="flex items-center relative"> <div class="flex items-center relative">
<select <select
class="dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right" class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right"
bind:value={embeddingEngine} bind:value={embeddingEngine}
placeholder="Select an embedding model engine" placeholder="Select an embedding model engine"
on:change={(e) => { on:change={(e) => {
@ -319,7 +319,7 @@
{#if embeddingEngine === 'openai'} {#if embeddingEngine === 'openai'}
<div class="my-0.5 flex gap-2 pr-2"> <div class="my-0.5 flex gap-2 pr-2">
<input <input
class="flex-1 w-full rounded-lg text-sm bg-transparent outline-none" class="flex-1 w-full rounded-lg text-sm bg-transparent outline-hidden"
placeholder={$i18n.t('API Base URL')} placeholder={$i18n.t('API Base URL')}
bind:value={OpenAIUrl} bind:value={OpenAIUrl}
required required
@ -330,7 +330,7 @@
{: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">
<input <input
class="flex-1 w-full rounded-lg text-sm bg-transparent outline-none" class="flex-1 w-full rounded-lg text-sm bg-transparent outline-hidden"
placeholder={$i18n.t('API Base URL')} placeholder={$i18n.t('API Base URL')}
bind:value={OllamaUrl} bind:value={OllamaUrl}
required required
@ -375,7 +375,7 @@
<div class=" self-center text-xs font-medium">{$i18n.t('Hybrid Search')}</div> <div class=" self-center text-xs font-medium">{$i18n.t('Hybrid Search')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded-sm transition"
on:click={() => { on:click={() => {
toggleHybridSearch(); toggleHybridSearch();
}} }}
@ -390,7 +390,7 @@
</div> </div>
</div> </div>
<hr class="dark:border-gray-850" /> <hr class="border-gray-100 dark:border-gray-850" />
<div class="space-y-2" /> <div class="space-y-2" />
<div> <div>
@ -400,7 +400,7 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
bind:value={embeddingModel} bind:value={embeddingModel}
placeholder={$i18n.t('Set embedding model')} placeholder={$i18n.t('Set embedding model')}
required required
@ -411,7 +411,7 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
placeholder={$i18n.t('Set embedding model (e.g. {{model}})', { placeholder={$i18n.t('Set embedding model (e.g. {{model}})', {
model: embeddingModel.slice(-40) model: embeddingModel.slice(-40)
})} })}
@ -490,7 +490,7 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
placeholder={$i18n.t('Set reranking model (e.g. {{model}})', { placeholder={$i18n.t('Set reranking model (e.g. {{model}})', {
model: 'BAAI/bge-reranker-v2-m3' model: 'BAAI/bge-reranker-v2-m3'
})} })}
@ -555,7 +555,7 @@
{/if} {/if}
</div> </div>
<hr class=" dark:border-gray-850" /> <hr class=" border-gray-100 dark:border-gray-850" />
<div class=""> <div class="">
<div class="text-sm font-medium mb-1">{$i18n.t('Content Extraction')}</div> <div class="text-sm font-medium mb-1">{$i18n.t('Content Extraction')}</div>
@ -564,7 +564,7 @@
<div class="self-center text-xs font-medium">{$i18n.t('Engine')}</div> <div class="self-center text-xs font-medium">{$i18n.t('Engine')}</div>
<div class="flex items-center relative"> <div class="flex items-center relative">
<select <select
class="dark:bg-gray-900 w-fit pr-8 rounded px-2 text-xs bg-transparent outline-none text-right" class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 text-xs bg-transparent outline-hidden text-right"
bind:value={contentExtractionEngine} bind:value={contentExtractionEngine}
on:change={(e) => { on:change={(e) => {
showTikaServerUrl = e.target.value === 'tika'; showTikaServerUrl = e.target.value === 'tika';
@ -580,7 +580,7 @@
<div class="flex w-full mt-1"> <div class="flex w-full mt-1">
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
placeholder={$i18n.t('Enter Tika Server URL')} placeholder={$i18n.t('Enter Tika Server URL')}
bind:value={tikaServerUrl} bind:value={tikaServerUrl}
/> />
@ -589,7 +589,7 @@
{/if} {/if}
</div> </div>
<hr class=" dark:border-gray-850" /> <hr class=" border-gray-100 dark:border-gray-850" />
<div class="text-sm font-medium mb-1">{$i18n.t('Google Drive')}</div> <div class="text-sm font-medium mb-1">{$i18n.t('Google Drive')}</div>
@ -602,7 +602,7 @@
</div> </div>
</div> </div>
<hr class=" dark:border-gray-850" /> <hr class=" border-gray-100 dark:border-gray-850" />
<div class=" "> <div class=" ">
<div class=" text-sm font-medium mb-1">{$i18n.t('Query Params')}</div> <div class=" text-sm font-medium mb-1">{$i18n.t('Query Params')}</div>
@ -613,7 +613,7 @@
<div class="w-full"> <div class="w-full">
<input <input
class=" w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" class=" w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
type="number" type="number"
placeholder={$i18n.t('Enter Top K')} placeholder={$i18n.t('Enter Top K')}
bind:value={querySettings.k} bind:value={querySettings.k}
@ -631,7 +631,7 @@
<div class="w-full"> <div class="w-full">
<input <input
class=" w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" class=" w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
type="number" type="number"
step="0.01" step="0.01"
placeholder={$i18n.t('Enter Score')} placeholder={$i18n.t('Enter Score')}
@ -667,7 +667,7 @@
</div> </div>
</div> </div>
<hr class=" dark:border-gray-850" /> <hr class=" border-gray-100 dark:border-gray-850" />
<div class=" "> <div class=" ">
<div class="mb-1 text-sm font-medium">{$i18n.t('Chunk Params')}</div> <div class="mb-1 text-sm font-medium">{$i18n.t('Chunk Params')}</div>
@ -676,7 +676,7 @@
<div class="self-center text-xs font-medium">{$i18n.t('Text Splitter')}</div> <div class="self-center text-xs font-medium">{$i18n.t('Text Splitter')}</div>
<div class="flex items-center relative"> <div class="flex items-center relative">
<select <select
class="dark:bg-gray-900 w-fit pr-8 rounded px-2 text-xs bg-transparent outline-none text-right" class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 text-xs bg-transparent outline-hidden text-right"
bind:value={textSplitter} bind:value={textSplitter}
> >
<option value="">{$i18n.t('Default')} ({$i18n.t('Character')})</option> <option value="">{$i18n.t('Default')} ({$i18n.t('Character')})</option>
@ -692,7 +692,7 @@
</div> </div>
<div class="self-center"> <div class="self-center">
<input <input
class=" w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" class=" w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
type="number" type="number"
placeholder={$i18n.t('Enter Chunk Size')} placeholder={$i18n.t('Enter Chunk Size')}
bind:value={chunkSize} bind:value={chunkSize}
@ -709,7 +709,7 @@
<div class="self-center"> <div class="self-center">
<input <input
class="w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
type="number" type="number"
placeholder={$i18n.t('Enter Chunk Overlap')} placeholder={$i18n.t('Enter Chunk Overlap')}
bind:value={chunkOverlap} bind:value={chunkOverlap}
@ -731,7 +731,7 @@
</div> </div>
</div> </div>
<hr class=" dark:border-gray-850" /> <hr class=" border-gray-100 dark:border-gray-850" />
<div class=""> <div class="">
<div class="text-sm font-medium mb-1">{$i18n.t('Files')}</div> <div class="text-sm font-medium mb-1">{$i18n.t('Files')}</div>
@ -750,7 +750,7 @@
placement="top-start" placement="top-start"
> >
<input <input
class="w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
type="number" type="number"
placeholder={$i18n.t('Leave empty for unlimited')} placeholder={$i18n.t('Leave empty for unlimited')}
bind:value={fileMaxSize} bind:value={fileMaxSize}
@ -773,7 +773,7 @@
placement="top-start" placement="top-start"
> >
<input <input
class=" w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" class=" w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
type="number" type="number"
placeholder={$i18n.t('Leave empty for unlimited')} placeholder={$i18n.t('Leave empty for unlimited')}
bind:value={fileMaxCount} bind:value={fileMaxCount}
@ -786,7 +786,7 @@
</div> </div>
</div> </div>
<hr class=" dark:border-gray-850" /> <hr class=" border-gray-100 dark:border-gray-850" />
<div> <div>
<button <button

View file

@ -245,7 +245,7 @@
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none" class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
type="text" type="text"
bind:value={name} bind:value={name}
placeholder={$i18n.t('Model Name')} placeholder={$i18n.t('Model Name')}
@ -260,7 +260,7 @@
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none" class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
type="text" type="text"
bind:value={id} bind:value={id}
placeholder={$i18n.t('Model ID')} placeholder={$i18n.t('Model ID')}
@ -277,7 +277,7 @@
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none" class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
type="text" type="text"
bind:value={description} bind:value={description}
placeholder={$i18n.t('Enter description')} placeholder={$i18n.t('Enter description')}
@ -324,7 +324,7 @@
<div class=" text-sm flex-1 py-1 rounded-lg"> <div class=" text-sm flex-1 py-1 rounded-lg">
{$models.find((model) => model.id === modelId)?.name} {$models.find((model) => model.id === modelId)?.name}
</div> </div>
<div class="flex-shrink-0"> <div class="shrink-0">
<button <button
type="button" type="button"
on:click={() => { on:click={() => {
@ -350,7 +350,7 @@
<select <select
class="w-full py-1 text-sm rounded-lg bg-transparent {selectedModelId class="w-full py-1 text-sm rounded-lg bg-transparent {selectedModelId
? '' ? ''
: 'text-gray-500'} placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none" : 'text-gray-500'} placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
bind:value={selectedModelId} bind:value={selectedModelId}
> >
<option value="">{$i18n.t('Select a model')}</option> <option value="">{$i18n.t('Select a model')}</option>

View file

@ -34,7 +34,7 @@
<div class="w-full flex flex-col"> <div class="w-full flex flex-col">
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<div class="flex-shrink-0 line-clamp-1"> <div class="shrink-0 line-clamp-1">
{model.name} {model.name}
</div> </div>
</div> </div>

View file

@ -116,11 +116,11 @@
{#if adminConfig !== null} {#if adminConfig !== null}
<div class=""> <div class="">
<div class="mb-3.5"> <div class="mb-3.5">
<div class=" mb-2 text-base font-medium">{$i18n.t('General')}</div> <div class=" mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
<hr class=" border-gray-50 dark:border-gray-850 my-2" /> <hr class=" border-gray-100 dark:border-gray-850 my-2" />
<div class="mb-2"> <div class="mb-2.5">
<div class=" mb-1 text-xs font-medium flex space-x-2 items-center"> <div class=" mb-1 text-xs font-medium flex space-x-2 items-center">
<div> <div>
{$i18n.t('Version')} {$i18n.t('Version')}
@ -157,7 +157,8 @@
</div> </div>
<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-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-lg font-medium"
type="button"
on:click={() => { on:click={() => {
checkForVersionUpdates(); checkForVersionUpdates();
}} }}
@ -167,19 +168,19 @@
</div> </div>
</div> </div>
<div class=""> <div class="mb-2.5">
<div class="flex w-full justify-between items-center"> <div class="flex w-full justify-between items-center">
<div class="text-xs"> <div class="text-xs pr-2">
<div class=""> <div class="">
{$i18n.t('Help')} {$i18n.t('Help')}
</div> </div>
<div class=" text-xs text-gray-500"> <div class=" text-xs text-gray-500">
Learn how to use Open WebUI and get help from the community. {$i18n.t('Discover how to use Open WebUI and seek support from the community.')}
</div> </div>
</div> </div>
<a <a
class=" text-xs font-medium underline" class="flex-shrink-0 text-xs font-medium underline"
href="https://docs.openwebui.com/" href="https://docs.openwebui.com/"
target="_blank" target="_blank"
> >
@ -212,18 +213,43 @@
</div> </div>
</div> </div>
</div> </div>
<div class="mb-2.5">
<div class="flex w-full justify-between items-center">
<div class="text-xs pr-2">
<div class="">
{$i18n.t('License')}
</div>
<a
class=" text-xs text-gray-500 hover:underline"
href="https://docs.openwebui.com/enterprise"
target="_blank"
>
{$i18n.t(
'Upgrade to a licensed plan for enhanced capabilities, including custom theming and branding, and dedicated support.'
)}
</a>
</div>
<!-- <button
class="flex-shrink-0 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"
>
{$i18n.t('Activate')}
</button> -->
</div>
</div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<div class=" mb-2 text-base font-medium">{$i18n.t('Authentication')}</div> <div class=" mb-2.5 text-base font-medium">{$i18n.t('Authentication')}</div>
<hr class=" border-gray-50 dark:border-gray-850 my-2" /> <hr class=" border-gray-100 dark:border-gray-850 my-2" />
<div class=" mb-2 flex w-full justify-between"> <div class=" mb-2.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Default User Role')}</div> <div class=" self-center text-xs font-medium">{$i18n.t('Default User Role')}</div>
<div class="flex items-center relative"> <div class="flex items-center relative">
<select <select
class="dark:bg-gray-900 w-fit pr-8 rounded px-2 text-xs bg-transparent outline-none text-right" class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 text-xs bg-transparent outline-hidden text-right"
bind:value={adminConfig.DEFAULT_USER_ROLE} bind:value={adminConfig.DEFAULT_USER_ROLE}
placeholder="Select a role" placeholder="Select a role"
> >
@ -234,13 +260,13 @@
</div> </div>
</div> </div>
<div class=" mb-2 flex w-full justify-between pr-2"> <div class=" mb-2.5 flex w-full justify-between pr-2">
<div class=" self-center text-xs font-medium">{$i18n.t('Enable New Sign Ups')}</div> <div class=" self-center text-xs font-medium">{$i18n.t('Enable New Sign Ups')}</div>
<Switch bind:state={adminConfig.ENABLE_SIGNUP} /> <Switch bind:state={adminConfig.ENABLE_SIGNUP} />
</div> </div>
<div class="mb-2 flex w-full items-center justify-between pr-2"> <div class="mb-2.5 flex w-full items-center justify-between pr-2">
<div class=" self-center text-xs font-medium"> <div class=" self-center text-xs font-medium">
{$i18n.t('Show Admin Details in Account Pending Overlay')} {$i18n.t('Show Admin Details in Account Pending Overlay')}
</div> </div>
@ -248,14 +274,14 @@
<Switch bind:state={adminConfig.SHOW_ADMIN_DETAILS} /> <Switch bind:state={adminConfig.SHOW_ADMIN_DETAILS} />
</div> </div>
<div class="mb-2 flex w-full justify-between pr-2"> <div class="mb-2.5 flex w-full justify-between pr-2">
<div class=" self-center text-xs font-medium">{$i18n.t('Enable API Key')}</div> <div class=" self-center text-xs font-medium">{$i18n.t('Enable API Key')}</div>
<Switch bind:state={adminConfig.ENABLE_API_KEY} /> <Switch bind:state={adminConfig.ENABLE_API_KEY} />
</div> </div>
{#if adminConfig?.ENABLE_API_KEY} {#if adminConfig?.ENABLE_API_KEY}
<div class="mb-2 flex w-full justify-between pr-2"> <div class="mb-2.5 flex w-full justify-between pr-2">
<div class=" self-center text-xs font-medium"> <div class=" self-center text-xs font-medium">
{$i18n.t('API Key Endpoint Restrictions')} {$i18n.t('API Key Endpoint Restrictions')}
</div> </div>
@ -270,7 +296,7 @@
</div> </div>
<input <input
class="w-full mt-1 rounded-lg text-sm dark:text-gray-300 bg-transparent outline-none" class="w-full mt-1 rounded-lg text-sm dark:text-gray-300 bg-transparent outline-hidden"
type="text" type="text"
placeholder={`e.g.) /api/v1/messages, /api/v1/channels`} placeholder={`e.g.) /api/v1/messages, /api/v1/channels`}
bind:value={adminConfig.API_KEY_ALLOWED_ENDPOINTS} bind:value={adminConfig.API_KEY_ALLOWED_ENDPOINTS}
@ -290,14 +316,14 @@
{/if} {/if}
{/if} {/if}
<div class=" mb-2 w-full justify-between"> <div class=" mb-2.5 w-full justify-between">
<div class="flex w-full justify-between"> <div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('JWT Expiration')}</div> <div class=" self-center text-xs font-medium">{$i18n.t('JWT Expiration')}</div>
</div> </div>
<div class="flex mt-2 space-x-2"> <div class="flex mt-2 space-x-2">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
type="text" type="text"
placeholder={`e.g.) "30m","1h", "10d". `} placeholder={`e.g.) "30m","1h", "10d". `}
bind:value={adminConfig.JWT_EXPIRES_IN} bind:value={adminConfig.JWT_EXPIRES_IN}
@ -335,7 +361,7 @@
{$i18n.t('Label')} {$i18n.t('Label')}
</div> </div>
<input <input
class="w-full bg-transparent outline-none py-0.5" class="w-full bg-transparent outline-hidden py-0.5"
required required
placeholder={$i18n.t('Enter server label')} placeholder={$i18n.t('Enter server label')}
bind:value={LDAP_SERVER.label} bind:value={LDAP_SERVER.label}
@ -349,7 +375,7 @@
{$i18n.t('Host')} {$i18n.t('Host')}
</div> </div>
<input <input
class="w-full bg-transparent outline-none py-0.5" class="w-full bg-transparent outline-hidden py-0.5"
required required
placeholder={$i18n.t('Enter server host')} placeholder={$i18n.t('Enter server host')}
bind:value={LDAP_SERVER.host} bind:value={LDAP_SERVER.host}
@ -365,7 +391,7 @@
className="w-full" className="w-full"
> >
<input <input
class="w-full bg-transparent outline-none py-0.5" class="w-full bg-transparent outline-hidden py-0.5"
type="number" type="number"
placeholder={$i18n.t('Enter server port')} placeholder={$i18n.t('Enter server port')}
bind:value={LDAP_SERVER.port} bind:value={LDAP_SERVER.port}
@ -383,7 +409,7 @@
placement="top-start" placement="top-start"
> >
<input <input
class="w-full bg-transparent outline-none py-0.5" class="w-full bg-transparent outline-hidden py-0.5"
required required
placeholder={$i18n.t('Enter Application DN')} placeholder={$i18n.t('Enter Application DN')}
bind:value={LDAP_SERVER.app_dn} bind:value={LDAP_SERVER.app_dn}
@ -412,7 +438,7 @@
placement="top-start" placement="top-start"
> >
<input <input
class="w-full bg-transparent outline-none py-0.5" class="w-full bg-transparent outline-hidden py-0.5"
required required
placeholder={$i18n.t('Example: mail')} placeholder={$i18n.t('Example: mail')}
bind:value={LDAP_SERVER.attribute_for_mail} bind:value={LDAP_SERVER.attribute_for_mail}
@ -432,7 +458,7 @@
placement="top-start" placement="top-start"
> >
<input <input
class="w-full bg-transparent outline-none py-0.5" class="w-full bg-transparent outline-hidden py-0.5"
required required
placeholder={$i18n.t( placeholder={$i18n.t(
'Example: sAMAccountName or uid or userPrincipalName' 'Example: sAMAccountName or uid or userPrincipalName'
@ -452,7 +478,7 @@
placement="top-start" placement="top-start"
> >
<input <input
class="w-full bg-transparent outline-none py-0.5" class="w-full bg-transparent outline-hidden py-0.5"
required required
placeholder={$i18n.t('Example: ou=users,dc=foo,dc=example')} placeholder={$i18n.t('Example: ou=users,dc=foo,dc=example')}
bind:value={LDAP_SERVER.search_base} bind:value={LDAP_SERVER.search_base}
@ -466,7 +492,7 @@
{$i18n.t('Search Filters')} {$i18n.t('Search Filters')}
</div> </div>
<input <input
class="w-full bg-transparent outline-none py-0.5" class="w-full bg-transparent outline-hidden py-0.5"
placeholder={$i18n.t('Example: (&(objectClass=inetOrgPerson)(uid=%s))')} placeholder={$i18n.t('Example: (&(objectClass=inetOrgPerson)(uid=%s))')}
bind:value={LDAP_SERVER.search_filters} bind:value={LDAP_SERVER.search_filters}
/> />
@ -496,7 +522,7 @@
{$i18n.t('Certificate Path')} {$i18n.t('Certificate Path')}
</div> </div>
<input <input
class="w-full bg-transparent outline-none py-0.5" class="w-full bg-transparent outline-hidden py-0.5"
required required
placeholder={$i18n.t('Enter certificate path')} placeholder={$i18n.t('Enter certificate path')}
bind:value={LDAP_SERVER.certificate_path} bind:value={LDAP_SERVER.certificate_path}
@ -510,7 +536,7 @@
</div> </div>
<Tooltip content={$i18n.t('Default to ALL')} placement="top-start"> <Tooltip content={$i18n.t('Default to ALL')} placement="top-start">
<input <input
class="w-full bg-transparent outline-none py-0.5" class="w-full bg-transparent outline-hidden py-0.5"
placeholder={$i18n.t('Example: ALL')} placeholder={$i18n.t('Example: ALL')}
bind:value={LDAP_SERVER.ciphers} bind:value={LDAP_SERVER.ciphers}
/> />
@ -527,11 +553,11 @@
</div> </div>
<div class="mb-3"> <div class="mb-3">
<div class=" mb-2 text-base font-medium">{$i18n.t('Features')}</div> <div class=" mb-2.5 text-base font-medium">{$i18n.t('Features')}</div>
<hr class=" border-gray-50 dark:border-gray-850 my-2" /> <hr class=" border-gray-100 dark:border-gray-850 my-2" />
<div class="mb-2 flex w-full items-center justify-between pr-2"> <div class="mb-2.5 flex w-full items-center justify-between pr-2">
<div class=" self-center text-xs font-medium"> <div class=" self-center text-xs font-medium">
{$i18n.t('Enable Community Sharing')} {$i18n.t('Enable Community Sharing')}
</div> </div>
@ -539,13 +565,13 @@
<Switch bind:state={adminConfig.ENABLE_COMMUNITY_SHARING} /> <Switch bind:state={adminConfig.ENABLE_COMMUNITY_SHARING} />
</div> </div>
<div class="mb-2 flex w-full items-center justify-between pr-2"> <div class="mb-2.5 flex w-full items-center justify-between pr-2">
<div class=" self-center text-xs font-medium">{$i18n.t('Enable Message Rating')}</div> <div class=" self-center text-xs font-medium">{$i18n.t('Enable Message Rating')}</div>
<Switch bind:state={adminConfig.ENABLE_MESSAGE_RATING} /> <Switch bind:state={adminConfig.ENABLE_MESSAGE_RATING} />
</div> </div>
<div class="mb-2 flex w-full items-center justify-between pr-2"> <div class="mb-2.5 flex w-full items-center justify-between pr-2">
<div class=" self-center text-xs font-medium"> <div class=" self-center text-xs font-medium">
{$i18n.t('Channels')} ({$i18n.t('Beta')}) {$i18n.t('Channels')} ({$i18n.t('Beta')})
</div> </div>
@ -553,14 +579,14 @@
<Switch bind:state={adminConfig.ENABLE_CHANNELS} /> <Switch bind:state={adminConfig.ENABLE_CHANNELS} />
</div> </div>
<div class="mb-2 w-full justify-between"> <div class="mb-2.5 w-full justify-between">
<div class="flex w-full justify-between"> <div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('WebUI URL')}</div> <div class=" self-center text-xs font-medium">{$i18n.t('WebUI URL')}</div>
</div> </div>
<div class="flex mt-2 space-x-2"> <div class="flex mt-2 space-x-2">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
type="text" type="text"
placeholder={`e.g.) "http://localhost:3000"`} placeholder={`e.g.) "http://localhost:3000"`}
bind:value={adminConfig.WEBUI_URL} bind:value={adminConfig.WEBUI_URL}
@ -581,7 +607,7 @@
<div class="flex mt-2 space-x-2"> <div class="flex mt-2 space-x-2">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
type="text" type="text"
placeholder={`https://example.com/webhook`} placeholder={`https://example.com/webhook`}
bind:value={webhookUrl} bind:value={webhookUrl}

View file

@ -284,7 +284,7 @@
<div class=" self-center text-xs font-medium">{$i18n.t('Image Generation Engine')}</div> <div class=" self-center text-xs font-medium">{$i18n.t('Image Generation Engine')}</div>
<div class="flex items-center relative"> <div class="flex items-center relative">
<select <select
class=" dark:bg-gray-900 w-fit pr-8 cursor-pointer rounded px-2 p-1 text-xs bg-transparent outline-none text-right" class=" dark:bg-gray-900 w-fit pr-8 cursor-pointer rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right"
bind:value={config.engine} bind:value={config.engine}
placeholder={$i18n.t('Select Engine')} placeholder={$i18n.t('Select Engine')}
on:change={async () => { on:change={async () => {
@ -298,7 +298,7 @@
</div> </div>
</div> </div>
</div> </div>
<hr class=" dark:border-gray-850" /> <hr class=" border-gray-100 dark:border-gray-850" />
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
{#if (config?.engine ?? 'automatic1111') === 'automatic1111'} {#if (config?.engine ?? 'automatic1111') === 'automatic1111'}
@ -307,7 +307,7 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
placeholder={$i18n.t('Enter URL (e.g. http://127.0.0.1:7860/)')} placeholder={$i18n.t('Enter URL (e.g. http://127.0.0.1:7860/)')}
bind:value={config.automatic1111.AUTOMATIC1111_BASE_URL} bind:value={config.automatic1111.AUTOMATIC1111_BASE_URL}
/> />
@ -386,7 +386,7 @@
<Tooltip content={$i18n.t('Enter Sampler (e.g. Euler a)')} placement="top-start"> <Tooltip content={$i18n.t('Enter Sampler (e.g. Euler a)')} placement="top-start">
<input <input
list="sampler-list" list="sampler-list"
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
placeholder={$i18n.t('Enter Sampler (e.g. Euler a)')} placeholder={$i18n.t('Enter Sampler (e.g. Euler a)')}
bind:value={config.automatic1111.AUTOMATIC1111_SAMPLER} bind:value={config.automatic1111.AUTOMATIC1111_SAMPLER}
/> />
@ -408,7 +408,7 @@
<Tooltip content={$i18n.t('Enter Scheduler (e.g. Karras)')} placement="top-start"> <Tooltip content={$i18n.t('Enter Scheduler (e.g. Karras)')} placement="top-start">
<input <input
list="scheduler-list" list="scheduler-list"
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
placeholder={$i18n.t('Enter Scheduler (e.g. Karras)')} placeholder={$i18n.t('Enter Scheduler (e.g. Karras)')}
bind:value={config.automatic1111.AUTOMATIC1111_SCHEDULER} bind:value={config.automatic1111.AUTOMATIC1111_SCHEDULER}
/> />
@ -429,7 +429,7 @@
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<Tooltip content={$i18n.t('Enter CFG Scale (e.g. 7.0)')} placement="top-start"> <Tooltip content={$i18n.t('Enter CFG Scale (e.g. 7.0)')} placement="top-start">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
placeholder={$i18n.t('Enter CFG Scale (e.g. 7.0)')} placeholder={$i18n.t('Enter CFG Scale (e.g. 7.0)')}
bind:value={config.automatic1111.AUTOMATIC1111_CFG_SCALE} bind:value={config.automatic1111.AUTOMATIC1111_CFG_SCALE}
/> />
@ -443,7 +443,7 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
placeholder={$i18n.t('Enter URL (e.g. http://127.0.0.1:7860/)')} placeholder={$i18n.t('Enter URL (e.g. http://127.0.0.1:7860/)')}
bind:value={config.comfyui.COMFYUI_BASE_URL} bind:value={config.comfyui.COMFYUI_BASE_URL}
/> />
@ -497,7 +497,7 @@
{#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-none 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}
required required
@ -525,7 +525,7 @@
/> />
<button <button
class="w-full text-sm font-medium py-2 bg-transparent hover:bg-gray-100 border border-dashed dark:border-gray-800 dark:hover:bg-gray-850 text-center rounded-xl" 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"
type="button" type="button"
on:click={() => { on:click={() => {
document.getElementById('upload-comfyui-workflow-input')?.click(); document.getElementById('upload-comfyui-workflow-input')?.click();
@ -548,7 +548,7 @@
<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 border dark:border-gray-850 rounded-lg">
<div class="flex-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 rounded-l-lg bg-green-500/10 text-green-700 dark:text-green-200"
> >
@ -558,7 +558,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-none border-r dark:border-gray-850" class="py-1 px-3 w-24 text-xs text-center bg-transparent outline-hidden border-r dark:border-gray-850"
placeholder="Key" placeholder="Key"
bind:value={node.key} bind:value={node.key}
required required
@ -572,7 +572,7 @@
placement="top-start" placement="top-start"
> >
<input <input
class="w-full py-1 px-4 rounded-r-lg text-xs bg-transparent outline-none" class="w-full py-1 px-4 rounded-r-lg text-xs bg-transparent outline-hidden"
placeholder="Node Ids" placeholder="Node Ids"
bind:value={node.node_ids} bind:value={node.node_ids}
/> />
@ -593,7 +593,7 @@
<div class="flex gap-2 mb-1"> <div class="flex gap-2 mb-1">
<input <input
class="flex-1 w-full text-sm bg-transparent outline-none" class="flex-1 w-full text-sm bg-transparent outline-hidden"
placeholder={$i18n.t('API Base URL')} placeholder={$i18n.t('API Base URL')}
bind:value={config.openai.OPENAI_API_BASE_URL} bind:value={config.openai.OPENAI_API_BASE_URL}
required required
@ -609,7 +609,7 @@
</div> </div>
{#if config?.enabled} {#if config?.enabled}
<hr class=" dark:border-gray-850" /> <hr class=" border-gray-100 dark:border-gray-850" />
<div> <div>
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Default Model')}</div> <div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Default Model')}</div>
@ -620,7 +620,7 @@
<Tooltip content={$i18n.t('Enter Model ID')} placement="top-start"> <Tooltip content={$i18n.t('Enter Model ID')} placement="top-start">
<input <input
list="model-list" list="model-list"
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
bind:value={imageGenerationConfig.MODEL} bind:value={imageGenerationConfig.MODEL}
placeholder="Select a model" placeholder="Select a model"
required required
@ -644,7 +644,7 @@
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<Tooltip content={$i18n.t('Enter Image Size (e.g. 512x512)')} placement="top-start"> <Tooltip content={$i18n.t('Enter Image Size (e.g. 512x512)')} placement="top-start">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
placeholder={$i18n.t('Enter Image Size (e.g. 512x512)')} placeholder={$i18n.t('Enter Image Size (e.g. 512x512)')}
bind:value={imageGenerationConfig.IMAGE_SIZE} bind:value={imageGenerationConfig.IMAGE_SIZE}
required required
@ -660,7 +660,7 @@
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<Tooltip content={$i18n.t('Enter Number of Steps (e.g. 50)')} placement="top-start"> <Tooltip content={$i18n.t('Enter Number of Steps (e.g. 50)')} placement="top-start">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
placeholder={$i18n.t('Enter Number of Steps (e.g. 50)')} placeholder={$i18n.t('Enter Number of Steps (e.g. 50)')}
bind:value={imageGenerationConfig.IMAGE_STEPS} bind:value={imageGenerationConfig.IMAGE_STEPS}
required required

View file

@ -69,9 +69,13 @@
}} }}
> >
<div class=" overflow-y-scroll scrollbar-hidden h-full pr-1.5"> <div class=" overflow-y-scroll scrollbar-hidden h-full pr-1.5">
<div> <div class="mb-3.5">
<div class=" mb-2.5 text-sm font-medium flex items-center"> <div class=" mb-2.5 text-base font-medium">{$i18n.t('Tasks')}</div>
<div class=" mr-1">{$i18n.t('Set Task Model')}</div>
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
<div class=" mb-1 font-medium flex items-center">
<div class=" text-xs mr-1">{$i18n.t('Set Task Model')}</div>
<Tooltip <Tooltip
content={$i18n.t( content={$i18n.t(
'A task model is used when performing tasks such as generating titles for chats and web search queries' 'A task model is used when performing tasks such as generating titles for chats and web search queries'
@ -93,11 +97,12 @@
</svg> </svg>
</Tooltip> </Tooltip>
</div> </div>
<div class="flex w-full gap-2">
<div class=" mb-2.5 flex w-full gap-2">
<div class="flex-1"> <div class="flex-1">
<div class=" text-xs mb-1">{$i18n.t('Local Models')}</div> <div class=" text-xs mb-1">{$i18n.t('Local Models')}</div>
<select <select
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
bind:value={taskConfig.TASK_MODEL} bind:value={taskConfig.TASK_MODEL}
placeholder={$i18n.t('Select a model')} placeholder={$i18n.t('Select a model')}
> >
@ -113,7 +118,7 @@
<div class="flex-1"> <div class="flex-1">
<div class=" text-xs mb-1">{$i18n.t('External Models')}</div> <div class=" text-xs mb-1">{$i18n.t('External Models')}</div>
<select <select
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
bind:value={taskConfig.TASK_MODEL_EXTERNAL} bind:value={taskConfig.TASK_MODEL_EXTERNAL}
placeholder={$i18n.t('Select a model')} placeholder={$i18n.t('Select a model')}
> >
@ -127,9 +132,7 @@
</div> </div>
</div> </div>
<hr class=" border-gray-50 dark:border-gray-850 my-3" /> <div class="mb-2.5 flex w-full items-center justify-between">
<div class="my-3 flex w-full items-center justify-between">
<div class=" self-center text-xs font-medium"> <div class=" self-center text-xs font-medium">
{$i18n.t('Title Generation')} {$i18n.t('Title Generation')}
</div> </div>
@ -138,8 +141,8 @@
</div> </div>
{#if taskConfig.ENABLE_TITLE_GENERATION} {#if taskConfig.ENABLE_TITLE_GENERATION}
<div class="mt-3"> <div class="mb-2.5">
<div class=" mb-2.5 text-xs font-medium">{$i18n.t('Title Generation Prompt')}</div> <div class=" mb-1 text-xs font-medium">{$i18n.t('Title Generation Prompt')}</div>
<Tooltip <Tooltip
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')} content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
@ -155,56 +158,7 @@
</div> </div>
{/if} {/if}
<div class="mt-3"> <div class="mb-2.5 flex w-full items-center justify-between">
<div class=" mb-2.5 text-xs font-medium">{$i18n.t('Image Prompt Generation Prompt')}</div>
<Tooltip
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
placement="top-start"
>
<Textarea
bind:value={taskConfig.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE}
placeholder={$i18n.t(
'Leave empty to use the default prompt, or enter a custom prompt'
)}
/>
</Tooltip>
</div>
<hr class=" border-gray-50 dark:border-gray-850 my-3" />
<div class="my-3 flex w-full items-center justify-between">
<div class=" self-center text-xs font-medium">
{$i18n.t('Autocomplete Generation')}
</div>
<Tooltip content={$i18n.t('Enable autocomplete generation for chat messages')}>
<Switch bind:state={taskConfig.ENABLE_AUTOCOMPLETE_GENERATION} />
</Tooltip>
</div>
{#if taskConfig.ENABLE_AUTOCOMPLETE_GENERATION}
<div class="mt-3">
<div class=" mb-2.5 text-xs font-medium">
{$i18n.t('Autocomplete Generation Input Max Length')}
</div>
<Tooltip
content={$i18n.t('Character limit for autocomplete generation input')}
placement="top-start"
>
<input
class="w-full outline-none bg-transparent"
bind:value={taskConfig.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH}
placeholder={$i18n.t('-1 for no limit, or a positive integer for a specific limit')}
/>
</Tooltip>
</div>
{/if}
<hr class=" border-gray-50 dark:border-gray-850 my-3" />
<div class="my-3 flex w-full items-center justify-between">
<div class=" self-center text-xs font-medium"> <div class=" self-center text-xs font-medium">
{$i18n.t('Tags Generation')} {$i18n.t('Tags Generation')}
</div> </div>
@ -213,8 +167,8 @@
</div> </div>
{#if taskConfig.ENABLE_TAGS_GENERATION} {#if taskConfig.ENABLE_TAGS_GENERATION}
<div class="mt-3"> <div class="mb-2.5">
<div class=" mb-2.5 text-xs font-medium">{$i18n.t('Tags Generation Prompt')}</div> <div class=" mb-1 text-xs font-medium">{$i18n.t('Tags Generation Prompt')}</div>
<Tooltip <Tooltip
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')} content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
@ -230,9 +184,7 @@
</div> </div>
{/if} {/if}
<hr class=" border-gray-50 dark:border-gray-850 my-3" /> <div class="mb-2.5 flex w-full items-center justify-between">
<div class="my-3 flex w-full items-center justify-between">
<div class=" self-center text-xs font-medium"> <div class=" self-center text-xs font-medium">
{$i18n.t('Retrieval Query Generation')} {$i18n.t('Retrieval Query Generation')}
</div> </div>
@ -240,7 +192,7 @@
<Switch bind:state={taskConfig.ENABLE_RETRIEVAL_QUERY_GENERATION} /> <Switch bind:state={taskConfig.ENABLE_RETRIEVAL_QUERY_GENERATION} />
</div> </div>
<div class="my-3 flex w-full items-center justify-between"> <div class="mb-2.5 flex w-full items-center justify-between">
<div class=" self-center text-xs font-medium"> <div class=" self-center text-xs font-medium">
{$i18n.t('Web Search Query Generation')} {$i18n.t('Web Search Query Generation')}
</div> </div>
@ -248,8 +200,8 @@
<Switch bind:state={taskConfig.ENABLE_SEARCH_QUERY_GENERATION} /> <Switch bind:state={taskConfig.ENABLE_SEARCH_QUERY_GENERATION} />
</div> </div>
<div class=""> <div class="mb-2.5">
<div class=" mb-2.5 text-xs font-medium">{$i18n.t('Query Generation Prompt')}</div> <div class=" mb-1 text-xs font-medium">{$i18n.t('Query Generation Prompt')}</div>
<Tooltip <Tooltip
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')} content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
@ -263,10 +215,54 @@
/> />
</Tooltip> </Tooltip>
</div> </div>
<div class="mb-2.5 flex w-full items-center justify-between">
<div class=" self-center text-xs font-medium">
{$i18n.t('Autocomplete Generation')}
</div> </div>
<div class="mt-3"> <Tooltip content={$i18n.t('Enable autocomplete generation for chat messages')}>
<div class=" mb-2.5 text-xs font-medium">{$i18n.t('Tools Function Calling Prompt')}</div> <Switch bind:state={taskConfig.ENABLE_AUTOCOMPLETE_GENERATION} />
</Tooltip>
</div>
{#if taskConfig.ENABLE_AUTOCOMPLETE_GENERATION}
<div class="mb-2.5">
<div class=" mb-1 text-xs font-medium">
{$i18n.t('Autocomplete Generation Input Max Length')}
</div>
<Tooltip
content={$i18n.t('Character limit for autocomplete generation input')}
placement="top-start"
>
<input
class="w-full outline-hidden bg-transparent"
bind:value={taskConfig.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH}
placeholder={$i18n.t('-1 for no limit, or a positive integer for a specific limit')}
/>
</Tooltip>
</div>
{/if}
<div class="mb-2.5">
<div class=" mb-1 text-xs font-medium">{$i18n.t('Image Prompt Generation Prompt')}</div>
<Tooltip
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
placement="top-start"
>
<Textarea
bind:value={taskConfig.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE}
placeholder={$i18n.t(
'Leave empty to use the default prompt, or enter a custom prompt'
)}
/>
</Tooltip>
</div>
<div class="mb-2.5">
<div class=" mb-1 text-xs font-medium">{$i18n.t('Tools Function Calling Prompt')}</div>
<Tooltip <Tooltip
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')} content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
@ -274,21 +270,27 @@
> >
<Textarea <Textarea
bind:value={taskConfig.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE} bind:value={taskConfig.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE}
placeholder={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')} placeholder={$i18n.t(
'Leave empty to use the default prompt, or enter a custom prompt'
)}
/> />
</Tooltip> </Tooltip>
</div> </div>
</div>
<hr class=" border-gray-50 dark:border-gray-850 my-3" /> <div class="mb-3.5">
<div class=" mb-2.5 text-base font-medium">{$i18n.t('UI')}</div>
<div class=" space-y-3 {banners.length > 0 ? ' mb-3' : ''}"> <hr class=" border-gray-100 dark:border-gray-850 my-2" />
<div class="flex w-full justify-between">
<div class=" {banners.length > 0 ? ' mb-3' : ''}">
<div class="mb-2.5 flex w-full justify-between">
<div class=" self-center text-sm font-semibold"> <div class=" self-center text-sm font-semibold">
{$i18n.t('Banners')} {$i18n.t('Banners')}
</div> </div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded-sm transition"
type="button" type="button"
on:click={() => { on:click={() => {
if (banners.length === 0 || banners.at(-1).content !== '') { if (banners.length === 0 || banners.at(-1).content !== '') {
@ -318,12 +320,15 @@
</svg> </svg>
</button> </button>
</div> </div>
<div class="flex flex-col space-y-1">
<div class=" flex flex-col space-y-1">
{#each banners as banner, bannerIdx} {#each banners as banner, bannerIdx}
<div class=" flex justify-between"> <div class=" flex justify-between">
<div class="flex flex-row flex-1 border rounded-xl dark:border-gray-800"> <div
class="flex flex-row flex-1 border rounded-xl border-gray-100 dark:border-gray-850"
>
<select <select
class="w-fit capitalize rounded-xl py-2 px-4 text-xs bg-transparent outline-none" class="w-fit capitalize rounded-xl py-2 px-4 text-xs bg-transparent outline-hidden"
bind:value={banner.type} bind:value={banner.type}
required required
> >
@ -339,7 +344,7 @@
</select> </select>
<input <input
class="pr-5 py-1.5 text-xs w-full bg-transparent outline-none" class="pr-5 py-1.5 text-xs w-full bg-transparent outline-hidden"
placeholder={$i18n.t('Content')} placeholder={$i18n.t('Content')}
bind:value={banner.content} bind:value={banner.content}
/> />
@ -383,7 +388,7 @@
</div> </div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded-sm transition"
type="button" type="button"
on:click={() => { on:click={() => {
if (promptSuggestions.length === 0 || promptSuggestions.at(-1).content !== '') { if (promptSuggestions.length === 0 || promptSuggestions.at(-1).content !== '') {
@ -409,23 +414,25 @@
class=" flex border border-gray-100 dark:border-none dark:bg-gray-850 rounded-xl py-1.5" class=" flex border border-gray-100 dark:border-none dark:bg-gray-850 rounded-xl py-1.5"
> >
<div class="flex flex-col flex-1 pl-1"> <div class="flex flex-col flex-1 pl-1">
<div class="flex border-b border-gray-100 dark:border-gray-800 w-full"> <div class="flex border-b border-gray-100 dark:border-gray-850 w-full">
<input <input
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r border-gray-100 dark:border-gray-800" class="px-3 py-1.5 text-xs w-full bg-transparent outline-hidden border-r border-gray-100 dark:border-gray-850"
placeholder={$i18n.t('Title (e.g. Tell me a fun fact)')} placeholder={$i18n.t('Title (e.g. Tell me a fun fact)')}
bind:value={prompt.title[0]} bind:value={prompt.title[0]}
/> />
<input <input
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r border-gray-100 dark:border-gray-800" class="px-3 py-1.5 text-xs w-full bg-transparent outline-hidden border-r border-gray-100 dark:border-gray-850"
placeholder={$i18n.t('Subtitle (e.g. about the Roman Empire)')} placeholder={$i18n.t('Subtitle (e.g. about the Roman Empire)')}
bind:value={prompt.title[1]} bind:value={prompt.title[1]}
/> />
</div> </div>
<textarea <textarea
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r border-gray-100 dark:border-gray-800 resize-none" class="px-3 py-1.5 text-xs w-full bg-transparent outline-hidden border-r border-gray-100 dark:border-gray-850 resize-none"
placeholder={$i18n.t('Prompt (e.g. Tell me a fun fact about the Roman Empire)')} placeholder={$i18n.t(
'Prompt (e.g. Tell me a fun fact about the Roman Empire)'
)}
rows="3" rows="3"
bind:value={prompt.content} bind:value={prompt.content}
/> />
@ -462,6 +469,7 @@
</div> </div>
{/if} {/if}
</div> </div>
</div>
<div class="flex justify-end text-sm font-medium"> <div class="flex justify-end text-sm font-medium">
<button <button

View file

@ -199,7 +199,7 @@
<Search className="size-3.5" /> <Search className="size-3.5" />
</div> </div>
<input <input
class=" w-full text-sm py-1 rounded-r-xl outline-none bg-transparent" class=" w-full text-sm py-1 rounded-r-xl outline-hidden bg-transparent"
bind:value={searchValue} bind:value={searchValue}
placeholder={$i18n.t('Search Models')} placeholder={$i18n.t('Search Models')}
/> />

View file

@ -165,7 +165,7 @@
<select <select
class="w-full py-1 text-sm rounded-lg bg-transparent {selectedModelId class="w-full py-1 text-sm rounded-lg bg-transparent {selectedModelId
? '' ? ''
: 'text-gray-500'} placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none" : 'text-gray-500'} placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
bind:value={selectedModelId} bind:value={selectedModelId}
> >
<option value="">{$i18n.t('Select a model')}</option> <option value="">{$i18n.t('Select a model')}</option>
@ -186,7 +186,7 @@
<div class=" text-sm flex-1 py-1 rounded-lg"> <div class=" text-sm flex-1 py-1 rounded-lg">
{$models.find((model) => model.id === modelId)?.name} {$models.find((model) => model.id === modelId)?.name}
</div> </div>
<div class="flex-shrink-0"> <div class="shrink-0">
<button <button
type="button" type="button"
on:click={() => { on:click={() => {

View file

@ -12,7 +12,7 @@
{#if ollamaConfig} {#if ollamaConfig}
<div class="flex-1 mb-2.5 pr-1.5 rounded-lg bg-gray-50 dark:text-gray-300 dark:bg-gray-850"> <div class="flex-1 mb-2.5 pr-1.5 rounded-lg bg-gray-50 dark:text-gray-300 dark:bg-gray-850">
<select <select
class="w-full py-2 px-4 text-sm outline-none bg-transparent" class="w-full py-2 px-4 text-sm outline-hidden bg-transparent"
bind:value={selectedUrlIdx} bind:value={selectedUrlIdx}
placeholder={$i18n.t('Select an Ollama instance')} placeholder={$i18n.t('Select an Ollama instance')}
> >

View file

@ -598,7 +598,7 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
placeholder={$i18n.t('Enter model tag (e.g. {{modelTag}})', { placeholder={$i18n.t('Enter model tag (e.g. {{modelTag}})', {
modelTag: 'mistral:7b' modelTag: 'mistral:7b'
})} })}
@ -740,7 +740,7 @@
class="flex-1 mr-2 pr-1.5 rounded-lg bg-gray-50 dark:text-gray-300 dark:bg-gray-850" class="flex-1 mr-2 pr-1.5 rounded-lg bg-gray-50 dark:text-gray-300 dark:bg-gray-850"
> >
<select <select
class="w-full py-2 px-4 text-sm outline-none bg-transparent" class="w-full py-2 px-4 text-sm outline-hidden bg-transparent"
bind:value={deleteModelTag} bind:value={deleteModelTag}
placeholder={$i18n.t('Select a model')} placeholder={$i18n.t('Select a model')}
> >
@ -781,7 +781,7 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1 mr-2 flex flex-col gap-2"> <div class="flex-1 mr-2 flex flex-col gap-2">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
placeholder={$i18n.t('Enter model tag (e.g. {{modelTag}})', { placeholder={$i18n.t('Enter model tag (e.g. {{modelTag}})', {
modelTag: 'my-modelfile' modelTag: 'my-modelfile'
})} })}
@ -791,7 +791,7 @@
<textarea <textarea
bind:value={createModelObject} bind:value={createModelObject}
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-100 dark:bg-gray-850 outline-none resize-none scrollbar-hidden" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-100 dark:bg-gray-850 outline-hidden resize-none scrollbar-hidden"
rows="6" rows="6"
placeholder={`e.g. {"model": "my-modelfile", "from": "ollama:7b"})`} placeholder={`e.g. {"model": "my-modelfile", "from": "ollama:7b"})`}
disabled={createModelLoading} disabled={createModelLoading}
@ -870,7 +870,7 @@
<div class=" text-sm font-medium">{$i18n.t('Upload a GGUF model')}</div> <div class=" text-sm font-medium">{$i18n.t('Upload a GGUF model')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded-sm transition"
on:click={() => { on:click={() => {
if (modelUploadMode === 'file') { if (modelUploadMode === 'file') {
modelUploadMode = 'url'; modelUploadMode = 'url';
@ -922,7 +922,7 @@
{:else} {:else}
<div class="flex-1 {modelFileUrl !== '' ? 'mr-2' : ''}"> <div class="flex-1 {modelFileUrl !== '' ? 'mr-2' : ''}">
<input <input
class="w-full rounded-lg text-left py-2 px-4 bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none {modelFileUrl !== class="w-full rounded-lg text-left py-2 px-4 bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden {modelFileUrl !==
'' ''
? 'mr-2' ? 'mr-2'
: ''}" : ''}"
@ -998,7 +998,7 @@
</div> </div>
<textarea <textarea
bind:value={modelFileContent} bind:value={modelFileContent}
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-100 dark:bg-gray-850 outline-none resize-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-100 dark:bg-gray-850 outline-hidden resize-none"
rows="6" rows="6"
/> />
</div> </div>

View file

@ -234,7 +234,7 @@
<div class="flex gap-2"> <div class="flex gap-2">
<div class="flex-1"> <div class="flex-1">
<select <select
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
bind:value={selectedPipelinesUrlIdx} bind:value={selectedPipelinesUrlIdx}
placeholder={$i18n.t('Select a pipeline url')} placeholder={$i18n.t('Select a pipeline url')}
on:change={async () => { on:change={async () => {
@ -271,7 +271,7 @@
/> />
<button <button
class="w-full text-sm font-medium py-2 bg-transparent hover:bg-gray-100 border border-dashed dark:border-gray-800 dark:hover:bg-gray-850 text-center rounded-xl" 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"
type="button" type="button"
on:click={() => { on:click={() => {
document.getElementById('pipelines-upload-input')?.click(); document.getElementById('pipelines-upload-input')?.click();
@ -348,7 +348,7 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
placeholder={$i18n.t('Enter Github Raw URL')} placeholder={$i18n.t('Enter Github Raw URL')}
bind:value={pipelineDownloadUrl} bind:value={pipelineDownloadUrl}
/> />
@ -418,7 +418,7 @@
</div> </div>
</div> </div>
<hr class=" dark:border-gray-800 my-3 w-full" /> <hr class="border-gray-100 dark:border-gray-850 my-3 w-full" />
{#if pipelines !== null} {#if pipelines !== null}
{#if pipelines.length > 0} {#if pipelines.length > 0}
@ -432,7 +432,7 @@
<div class="flex gap-2"> <div class="flex gap-2">
<div class="flex-1"> <div class="flex-1">
<select <select
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
bind:value={selectedPipelineIdx} bind:value={selectedPipelineIdx}
placeholder={$i18n.t('Select a pipeline')} placeholder={$i18n.t('Select a pipeline')}
on:change={async () => { on:change={async () => {
@ -482,7 +482,7 @@
</div> </div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded-sm transition"
type="button" type="button"
on:click={() => { on:click={() => {
valves[property] = (valves[property] ?? null) === null ? '' : null; valves[property] = (valves[property] ?? null) === null ? '' : null;
@ -502,7 +502,7 @@
<div class=" flex-1"> <div class=" flex-1">
{#if valves_spec.properties[property]?.enum ?? null} {#if valves_spec.properties[property]?.enum ?? null}
<select <select
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
bind:value={valves[property]} bind:value={valves[property]}
> >
{#each valves_spec.properties[property].enum as option} {#each valves_spec.properties[property].enum as option}
@ -523,7 +523,7 @@
</div> </div>
{:else} {:else}
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
type="text" type="text"
placeholder={valves_spec.properties[property].title} placeholder={valves_spec.properties[property].title}
bind:value={valves[property]} bind:value={valves[property]}

View file

@ -6,6 +6,7 @@
import { onMount, getContext } from 'svelte'; import { onMount, getContext } from 'svelte';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte'; import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
@ -103,7 +104,7 @@
<div class=" self-center text-xs font-medium">{$i18n.t('Web Search Engine')}</div> <div class=" self-center text-xs font-medium">{$i18n.t('Web Search Engine')}</div>
<div class="flex items-center relative"> <div class="flex items-center relative">
<select <select
class="dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right" class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right"
bind:value={webConfig.search.engine} bind:value={webConfig.search.engine}
placeholder={$i18n.t('Select a engine')} placeholder={$i18n.t('Select a engine')}
required required
@ -116,6 +117,19 @@
</div> </div>
</div> </div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Full Context Mode')}</div>
<div class="flex items-center relative">
<Tooltip
content={webConfig.RAG_WEB_SEARCH_FULL_CONTEXT
? 'Inject the entire web results as context for comprehensive processing, this is recommended for complex queries.'
: 'Default to segmented retrieval for focused and relevant content extraction, this is recommended for most cases.'}
>
<Switch bind:state={webConfig.RAG_WEB_SEARCH_FULL_CONTEXT} />
</Tooltip>
</div>
</div>
{#if webConfig.search.engine !== ''} {#if webConfig.search.engine !== ''}
<div class="mt-1.5"> <div class="mt-1.5">
{#if webConfig.search.engine === 'searxng'} {#if webConfig.search.engine === 'searxng'}
@ -127,7 +141,7 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
type="text" type="text"
placeholder={$i18n.t('Enter Searxng Query URL')} placeholder={$i18n.t('Enter Searxng Query URL')}
bind:value={webConfig.search.searxng_query_url} bind:value={webConfig.search.searxng_query_url}
@ -155,7 +169,7 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
type="text" type="text"
placeholder={$i18n.t('Enter Google PSE Engine Id')} placeholder={$i18n.t('Enter Google PSE Engine Id')}
bind:value={webConfig.search.google_pse_engine_id} bind:value={webConfig.search.google_pse_engine_id}
@ -260,7 +274,7 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
type="text" type="text"
placeholder={$i18n.t('Enter SearchApi Engine')} placeholder={$i18n.t('Enter SearchApi Engine')}
bind:value={webConfig.search.searchapi_engine} bind:value={webConfig.search.searchapi_engine}
@ -288,7 +302,7 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
type="text" type="text"
placeholder={$i18n.t('Enter SerpApi Engine')} placeholder={$i18n.t('Enter SerpApi Engine')}
bind:value={webConfig.search.serpapi_engine} bind:value={webConfig.search.serpapi_engine}
@ -339,7 +353,7 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
type="text" type="text"
placeholder={$i18n.t('Enter Bing Search V7 Endpoint')} placeholder={$i18n.t('Enter Bing Search V7 Endpoint')}
bind:value={webConfig.search.bing_search_v7_endpoint} bind:value={webConfig.search.bing_search_v7_endpoint}
@ -371,7 +385,7 @@
</div> </div>
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
placeholder={$i18n.t('Search Result Count')} placeholder={$i18n.t('Search Result Count')}
bind:value={webConfig.search.result_count} bind:value={webConfig.search.result_count}
required required
@ -384,7 +398,7 @@
</div> </div>
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
placeholder={$i18n.t('Concurrent Requests')} placeholder={$i18n.t('Concurrent Requests')}
bind:value={webConfig.search.concurrent_requests} bind:value={webConfig.search.concurrent_requests}
required required
@ -398,7 +412,7 @@
</div> </div>
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
placeholder={$i18n.t( placeholder={$i18n.t(
'Enter domains separated by commas (e.g., example.com,site.org)' 'Enter domains separated by commas (e.g., example.com,site.org)'
)} )}
@ -408,7 +422,7 @@
{/if} {/if}
</div> </div>
<hr class=" dark:border-gray-850 my-2" /> <hr class="border-gray-100 dark:border-gray-850 my-2" />
<div> <div>
<div class=" mb-1 text-sm font-medium"> <div class=" mb-1 text-sm font-medium">
@ -422,14 +436,15 @@
</div> </div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded-sm transition"
on:click={() => { on:click={() => {
webConfig.web_loader_ssl_verification = !webConfig.web_loader_ssl_verification; webConfig.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION =
!webConfig.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION;
submitHandler(); submitHandler();
}} }}
type="button" type="button"
> >
{#if webConfig.web_loader_ssl_verification === false} {#if webConfig.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION === false}
<span class="ml-2 self-center">{$i18n.t('On')}</span> <span class="ml-2 self-center">{$i18n.t('On')}</span>
{:else} {:else}
<span class="ml-2 self-center">{$i18n.t('Off')}</span> <span class="ml-2 self-center">{$i18n.t('Off')}</span>
@ -447,7 +462,7 @@
<div class=" w-20 text-xs font-medium self-center">{$i18n.t('Language')}</div> <div class=" w-20 text-xs font-medium self-center">{$i18n.t('Language')}</div>
<div class=" flex-1 self-center"> <div class=" flex-1 self-center">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
type="text" type="text"
placeholder={$i18n.t('Enter language codes')} placeholder={$i18n.t('Enter language codes')}
bind:value={youtubeLanguage} bind:value={youtubeLanguage}
@ -462,7 +477,7 @@
<div class=" w-20 text-xs font-medium self-center">{$i18n.t('Proxy URL')}</div> <div class=" w-20 text-xs font-medium self-center">{$i18n.t('Proxy URL')}</div>
<div class=" flex-1 self-center"> <div class=" flex-1 self-center">
<input <input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
type="text" type="text"
placeholder={$i18n.t('Enter proxy URL (e.g. https://user:password@host:port)')} placeholder={$i18n.t('Enter proxy URL (e.g. https://user:password@host:port)')}
bind:value={youtubeProxyUrl} bind:value={youtubeProxyUrl}

View file

@ -140,7 +140,7 @@
</svg> </svg>
</div> </div>
<input <input
class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-none bg-transparent" class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-hidden bg-transparent"
bind:value={search} bind:value={search}
placeholder={$i18n.t('Search')} placeholder={$i18n.t('Search')}
/> />
@ -195,7 +195,7 @@
<div class="w-full"></div> <div class="w-full"></div>
</div> </div>
<hr class="mt-1.5 border-gray-50 dark:border-gray-850" /> <hr class="mt-1.5 border-gray-100 dark:border-gray-850" />
{#each filteredGroups as group} {#each filteredGroups as group}
<div class="my-2"> <div class="my-2">
@ -205,7 +205,7 @@
</div> </div>
{/if} {/if}
<hr class="mb-2 border-gray-50 dark:border-gray-850" /> <hr class="mb-2 border-gray-100 dark:border-gray-850" />
<GroupModal <GroupModal
bind:show={showDefaultPermissionsModal} bind:show={showDefaultPermissionsModal}

View file

@ -78,7 +78,7 @@
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none" class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
type="text" type="text"
bind:value={name} bind:value={name}
placeholder={$i18n.t('Group Name')} placeholder={$i18n.t('Group Name')}
@ -94,7 +94,7 @@
<div class="flex-1"> <div class="flex-1">
<Textarea <Textarea
className="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none resize-none" className="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden resize-none"
rows={2} rows={2}
bind:value={description} bind:value={description}
placeholder={$i18n.t('Group Description')} placeholder={$i18n.t('Group Description')}

View file

@ -16,7 +16,7 @@
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none" class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
type="text" type="text"
bind:value={name} bind:value={name}
placeholder={$i18n.t('Group Name')} placeholder={$i18n.t('Group Name')}
@ -36,7 +36,7 @@
<div class="text-gray-500">#</div> <div class="text-gray-500">#</div>
<input <input
class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none" class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
type="text" type="text"
bind:value={color} bind:value={color}
placeholder={$i18n.t('Hex Color')} placeholder={$i18n.t('Hex Color')}
@ -52,7 +52,7 @@
<div class="flex-1"> <div class="flex-1">
<Textarea <Textarea
className="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none resize-none" className="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden resize-none"
rows={4} rows={4}
bind:value={description} bind:value={description}
placeholder={$i18n.t('Group Description')} placeholder={$i18n.t('Group Description')}

View file

@ -76,7 +76,7 @@
<div class=" text-sm flex-1 rounded-lg"> <div class=" text-sm flex-1 rounded-lg">
{modelId} {modelId}
</div> </div>
<div class="flex-shrink-0"> <div class="shrink-0">
<button <button
type="button" type="button"
on:click={() => { on:click={() => {
@ -102,7 +102,7 @@
<select <select
class="w-full py-1 text-sm rounded-lg bg-transparent {selectedModelId class="w-full py-1 text-sm rounded-lg bg-transparent {selectedModelId
? '' ? ''
: 'text-gray-500'} placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none" : 'text-gray-500'} placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
bind:value={selectedModelId} bind:value={selectedModelId}
> >
<option value="">{$i18n.t('Select a model')}</option> <option value="">{$i18n.t('Select a model')}</option>
@ -137,7 +137,7 @@
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<select <select
class="w-full bg-transparent outline-none py-0.5 text-sm" class="w-full bg-transparent outline-hidden py-0.5 text-sm"
bind:value={permissions.model.default_id} bind:value={permissions.model.default_id}
placeholder="Select a model" placeholder="Select a model"
> >
@ -150,7 +150,7 @@
</div> </div>
</div> </div>
<hr class=" border-gray-50 dark:border-gray-850 my-2" /> --> <hr class=" border-gray-100 dark:border-gray-850 my-2" /> -->
<div> <div>
<div class=" mb-2 text-sm font-medium">{$i18n.t('Workspace Permissions')}</div> <div class=" mb-2 text-sm font-medium">{$i18n.t('Workspace Permissions')}</div>
@ -192,7 +192,7 @@
</div> </div>
</div> </div>
<hr class=" border-gray-50 dark:border-gray-850 my-2" /> <hr class=" border-gray-100 dark:border-gray-850 my-2" />
<div> <div>
<div class=" mb-2 text-sm font-medium">{$i18n.t('Chat Permissions')}</div> <div class=" mb-2 text-sm font-medium">{$i18n.t('Chat Permissions')}</div>
@ -238,7 +238,7 @@
</div> </div>
</div> </div>
<hr class=" border-gray-50 dark:border-gray-850 my-2" /> <hr class=" border-gray-100 dark:border-gray-850 my-2" />
<div> <div>
<div class=" mb-2 text-sm font-medium">{$i18n.t('Features Permissions')}</div> <div class=" mb-2 text-sm font-medium">{$i18n.t('Features Permissions')}</div>

View file

@ -64,7 +64,7 @@
</svg> </svg>
</div> </div>
<input <input
class=" w-full text-sm pr-4 rounded-r-xl outline-none bg-transparent" class=" w-full text-sm pr-4 rounded-r-xl outline-hidden bg-transparent"
bind:value={query} bind:value={query}
placeholder={$i18n.t('Search')} placeholder={$i18n.t('Search')}
/> />

View file

@ -149,7 +149,7 @@
</svg> </svg>
</div> </div>
<input <input
class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-none bg-transparent" class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-hidden bg-transparent"
bind:value={search} bind:value={search}
placeholder={$i18n.t('Search')} placeholder={$i18n.t('Search')}
/> />
@ -171,9 +171,11 @@
</div> </div>
</div> </div>
<div class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full rounded pt-0.5"> <div
class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full rounded-sm pt-0.5"
>
<table <table
class="w-full text-sm text-left text-gray-500 dark:text-gray-400 table-auto max-w-full rounded" class="w-full text-sm text-left text-gray-500 dark:text-gray-400 table-auto max-w-full rounded-sm"
> >
<thead <thead
class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-850 dark:text-gray-400 -translate-y-0.5" class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-850 dark:text-gray-400 -translate-y-0.5"

View file

@ -181,7 +181,7 @@
<div class="flex-1"> <div class="flex-1">
<select <select
class="w-full capitalize rounded-lg text-sm bg-transparent dark:disabled:text-gray-500 outline-none" class="w-full capitalize rounded-lg text-sm bg-transparent dark:disabled:text-gray-500 outline-hidden"
bind:value={_user.role} bind:value={_user.role}
placeholder={$i18n.t('Enter Your Role')} placeholder={$i18n.t('Enter Your Role')}
required required
@ -198,7 +198,7 @@
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full text-sm bg-transparent disabled:text-gray-500 dark:disabled:text-gray-500 outline-none" class="w-full text-sm bg-transparent disabled:text-gray-500 dark:disabled:text-gray-500 outline-hidden"
type="text" type="text"
bind:value={_user.name} bind:value={_user.name}
placeholder={$i18n.t('Enter Your Full Name')} placeholder={$i18n.t('Enter Your Full Name')}
@ -208,14 +208,14 @@
</div> </div>
</div> </div>
<hr class=" border-gray-50 dark:border-gray-850 my-2.5 w-full" /> <hr class=" border-gray-100 dark:border-gray-850 my-2.5 w-full" />
<div class="flex flex-col w-full"> <div class="flex flex-col w-full">
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Email')}</div> <div class=" mb-1 text-xs text-gray-500">{$i18n.t('Email')}</div>
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full text-sm bg-transparent disabled:text-gray-500 dark:disabled:text-gray-500 outline-none" class="w-full text-sm bg-transparent disabled:text-gray-500 dark:disabled:text-gray-500 outline-hidden"
type="email" type="email"
bind:value={_user.email} bind:value={_user.email}
placeholder={$i18n.t('Enter Your Email')} placeholder={$i18n.t('Enter Your Email')}
@ -229,7 +229,7 @@
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full text-sm bg-transparent disabled:text-gray-500 dark:disabled:text-gray-500 outline-none" class="w-full text-sm bg-transparent disabled:text-gray-500 dark:disabled:text-gray-500 outline-hidden"
type="password" type="password"
bind:value={_user.password} bind:value={_user.password}
placeholder={$i18n.t('Enter Your Password')} placeholder={$i18n.t('Enter Your Password')}
@ -249,7 +249,7 @@
/> />
<button <button
class="w-full text-sm font-medium py-3 bg-transparent hover:bg-gray-100 border border-dashed dark:border-gray-800 dark:hover:bg-gray-850 text-center rounded-xl" class="w-full text-sm font-medium py-3 bg-transparent hover:bg-gray-100 border border-dashed dark:border-gray-850 dark:hover:bg-gray-850 text-center rounded-xl"
type="button" type="button"
on:click={() => { on:click={() => {
document.getElementById('upload-user-csv-input')?.click(); document.getElementById('upload-user-csv-input')?.click();

View file

@ -65,7 +65,7 @@
</svg> </svg>
</button> </button>
</div> </div>
<hr class=" dark:border-gray-800" /> <hr class="border-gray-100 dark:border-gray-850" />
<div class="flex flex-col md:flex-row w-full p-5 md:space-x-4 dark:text-gray-200"> <div class="flex flex-col md:flex-row w-full p-5 md:space-x-4 dark:text-gray-200">
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6"> <div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
@ -94,7 +94,7 @@
</div> </div>
</div> </div>
<hr class=" dark:border-gray-800 my-3 w-full" /> <hr class="border-gray-100 dark:border-gray-850 my-3 w-full" />
<div class=" flex flex-col space-y-1.5"> <div class=" flex flex-col space-y-1.5">
<div class="flex flex-col w-full"> <div class="flex flex-col w-full">
@ -102,7 +102,7 @@
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 disabled:text-gray-500 dark:disabled:text-gray-500 outline-none" class="w-full rounded-sm py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 disabled:text-gray-500 dark:disabled:text-gray-500 outline-hidden"
type="email" type="email"
bind:value={_user.email} bind:value={_user.email}
autocomplete="off" autocomplete="off"
@ -117,7 +117,7 @@
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none" class="w-full rounded-sm py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-hidden"
type="text" type="text"
bind:value={_user.name} bind:value={_user.name}
autocomplete="off" autocomplete="off"
@ -131,7 +131,7 @@
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none" class="w-full rounded-sm py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-hidden"
type="password" type="password"
bind:value={_user.password} bind:value={_user.password}
autocomplete="new-password" autocomplete="new-password"

View file

@ -82,7 +82,7 @@
<div class="relative overflow-x-auto"> <div class="relative overflow-x-auto">
<table class="w-full text-sm text-left text-gray-600 dark:text-gray-400 table-auto"> <table class="w-full text-sm text-left text-gray-600 dark:text-gray-400 table-auto">
<thead <thead
class="text-xs text-gray-700 uppercase bg-transparent dark:text-gray-200 border-b-2 dark:border-gray-800" class="text-xs text-gray-700 uppercase bg-transparent dark:text-gray-200 border-b-2 dark:border-gray-850"
> >
<tr> <tr>
<th <th

View file

@ -281,7 +281,7 @@
<PaneResizer <PaneResizer
class="relative flex w-[3px] items-center justify-center bg-background group bg-gray-50 dark:bg-gray-850" class="relative flex w-[3px] items-center justify-center bg-background group bg-gray-50 dark:bg-gray-850"
> >
<div class="z-10 flex h-7 w-5 items-center justify-center rounded-sm"> <div class="z-10 flex h-7 w-5 items-center justify-center rounded-xs">
<EllipsisVertical className="size-4 invisible group-hover:visible" /> <EllipsisVertical className="size-4 invisible group-hover:visible" />
</div> </div>
</PaneResizer> </PaneResizer>

View file

@ -103,7 +103,9 @@
return; return;
} }
if (['image/gif', 'image/webp', 'image/jpeg', 'image/png'].includes(file['type'])) { if (
['image/gif', 'image/webp', 'image/jpeg', 'image/png', 'image/avif'].includes(file['type'])
) {
let reader = new FileReader(); let reader = new FileReader();
reader.onload = async (event) => { reader.onload = async (event) => {
@ -455,7 +457,7 @@
<div class="px-2.5"> <div class="px-2.5">
<div <div
class="scrollbar-hidden font-primary text-left bg-transparent dark:text-gray-100 outline-none w-full pt-3 px-1 rounded-xl resize-none h-fit max-h-80 overflow-auto" class="scrollbar-hidden font-primary text-left bg-transparent dark:text-gray-100 outline-hidden w-full pt-3 px-1 rounded-xl resize-none h-fit max-h-80 overflow-auto"
> >
<RichTextInput <RichTextInput
bind:value={content} bind:value={content}
@ -513,7 +515,7 @@
}} }}
> >
<button <button
class="bg-transparent hover:bg-white/80 text-gray-800 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5 outline-none focus:outline-none" class="bg-transparent hover:bg-white/80 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="More"
> >

View file

@ -44,7 +44,7 @@
<div slot="content"> <div slot="content">
<DropdownMenu.Content <DropdownMenu.Content
class="w-full max-w-[200px] rounded-xl px-1 py-1 border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow" class="w-full max-w-[200px] rounded-xl px-1 py-1 border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-sm"
sideOffset={15} sideOffset={15}
alignOffset={-8} alignOffset={-8}
side="top" side="top"

View file

@ -72,7 +72,7 @@
class=" absolute {showButtons ? '' : 'invisible group-hover:visible'} right-1 -top-2 z-10" class=" absolute {showButtons ? '' : 'invisible group-hover:visible'} right-1 -top-2 z-10"
> >
<div <div
class="flex gap-1 rounded-lg bg-white dark:bg-gray-850 shadow-md p-0.5 border border-gray-100 dark:border-gray-800" class="flex gap-1 rounded-lg bg-white dark:bg-gray-850 shadow-md p-0.5 border border-gray-100 dark:border-gray-850"
> >
<ReactionPicker <ReactionPicker
onClose={() => (showButtons = false)} onClose={() => (showButtons = false)}
@ -138,7 +138,7 @@
dir={$settings.chatDirection} dir={$settings.chatDirection}
> >
<div <div
class={`flex-shrink-0 ${($settings?.chatDirection ?? 'LTR') === 'LTR' ? 'mr-3' : 'ml-3'} w-9`} class={`shrink-0 ${($settings?.chatDirection ?? 'LTR') === 'LTR' ? 'mr-3' : 'ml-3'} w-9`}
> >
{#if showUserProfile} {#if showUserProfile}
<ProfilePreview user={message.user}> <ProfilePreview user={message.user}>
@ -153,7 +153,7 @@
{#if message.created_at} {#if message.created_at}
<div <div
class="mt-1.5 flex flex-shrink-0 items-center text-xs self-center invisible group-hover:visible text-gray-500 font-medium first-letter:capitalize" class="mt-1.5 flex shrink-0 items-center text-xs self-center invisible group-hover:visible text-gray-500 font-medium first-letter:capitalize"
> >
<Tooltip content={dayjs(message.created_at / 1000000).format('LLLL')}> <Tooltip content={dayjs(message.created_at / 1000000).format('LLLL')}>
{dayjs(message.created_at / 1000000).format('HH:mm')} {dayjs(message.created_at / 1000000).format('HH:mm')}
@ -206,7 +206,7 @@
{#if edit} {#if edit}
<div class="py-2"> <div class="py-2">
<Textarea <Textarea
className=" bg-transparent outline-none w-full resize-none" className=" bg-transparent outline-hidden w-full resize-none"
bind:value={editedContent} bind:value={editedContent}
onKeydown={(e) => { onKeydown={(e) => {
if (e.key === 'Escape') { if (e.key === 'Escape') {

View file

@ -29,7 +29,7 @@
<slot name="content"> <slot name="content">
<DropdownMenu.Content <DropdownMenu.Content
class="max-w-full w-[240px] rounded-lg z-[9999] bg-white dark:bg-black dark:text-white shadow-lg" class="max-w-full w-[240px] rounded-lg z-9999 bg-white dark:bg-black dark:text-white shadow-lg"
sideOffset={8} sideOffset={8}
{side} {side}
{align} {align}

View file

@ -107,7 +107,7 @@
<slot /> <slot />
</DropdownMenu.Trigger> </DropdownMenu.Trigger>
<DropdownMenu.Content <DropdownMenu.Content
class="max-w-full w-80 bg-gray-50 dark:bg-gray-850 rounded-lg z-[9999] shadow-lg dark:text-white" class="max-w-full w-80 bg-gray-50 dark:bg-gray-850 rounded-lg z-9999 shadow-lg dark:text-white"
sideOffset={8} sideOffset={8}
{side} {side}
{align} {align}
@ -116,7 +116,7 @@
<div class="mb-1 px-3 pt-2 pb-2"> <div class="mb-1 px-3 pt-2 pb-2">
<input <input
type="text" type="text"
class="w-full text-sm bg-transparent outline-none" class="w-full text-sm bg-transparent outline-hidden"
placeholder="Search all emojis" placeholder="Search all emojis"
bind:value={search} bind:value={search}
/> />

View file

@ -18,7 +18,7 @@
<nav class="sticky top-0 z-30 w-full px-1.5 py-1.5 -mb-8 flex items-center drag-region"> <nav class="sticky top-0 z-30 w-full px-1.5 py-1.5 -mb-8 flex items-center drag-region">
<div <div
class=" bg-gradient-to-b via-50% from-white via-white to-transparent dark:from-gray-900 dark:via-gray-900 dark:to-transparent pointer-events-none absolute inset-0 -bottom-7 z-[-1] blur" class=" bg-linear-to-b via-50% from-white via-white to-transparent dark:from-gray-900 dark:via-gray-900 dark:to-transparent pointer-events-none absolute inset-0 -bottom-7 z-[-1]"
></div> ></div>
<div class=" flex max-w-full w-full mx-auto px-1 pt-0.5 bg-transparent"> <div class=" flex max-w-full w-full mx-auto px-1 pt-0.5 bg-transparent">

View file

@ -1241,7 +1241,7 @@
// Response not done // Response not done
return; return;
} }
if (messages.length != 0 && messages.at(-1).error) { if (messages.length != 0 && messages.at(-1).error && !messages.at(-1).content) {
// Error in response // Error in response
toast.error($i18n.t(`Oops! There was an error in the previous response.`)); toast.error($i18n.t(`Oops! There was an error in the previous response.`));
return; return;
@ -1896,7 +1896,7 @@
/> />
<div <div
class="absolute top-0 left-0 w-full h-full bg-gradient-to-t from-white to-white/85 dark:from-gray-900 dark:to-[#171717]/90 z-0" class="absolute top-0 left-0 w-full h-full bg-linear-to-t from-white to-white/85 dark:from-gray-900 dark:to-gray-900/90 z-0"
/> />
{/if} {/if}

View file

@ -195,7 +195,7 @@
{#if $showControls} {#if $showControls}
<PaneResizer class="relative flex w-2 items-center justify-center bg-background group"> <PaneResizer class="relative flex w-2 items-center justify-center bg-background group">
<div class="z-10 flex h-7 w-5 items-center justify-center rounded-sm"> <div class="z-10 flex h-7 w-5 items-center justify-center rounded-xs">
<EllipsisVertical className="size-4 invisible group-hover:visible" /> <EllipsisVertical className="size-4 invisible group-hover:visible" />
</div> </div>
</PaneResizer> </PaneResizer>
@ -230,7 +230,7 @@
<div <div
class="w-full {($showOverview || $showArtifacts) && !$showCallOverlay class="w-full {($showOverview || $showArtifacts) && !$showCallOverlay
? ' ' ? ' '
: 'px-4 py-4 bg-white dark:shadow-lg dark:bg-gray-850 border border-gray-50 dark:border-gray-850'} rounded-xl z-40 pointer-events-auto overflow-y-auto scrollbar-hidden" : 'px-4 py-4 bg-white dark:shadow-lg dark:bg-gray-850 border border-gray-100 dark:border-gray-850'} rounded-xl z-40 pointer-events-auto overflow-y-auto scrollbar-hidden"
> >
{#if $showCallOverlay} {#if $showCallOverlay}
<div class="w-full h-full flex justify-center"> <div class="w-full h-full flex justify-center">

View file

@ -221,7 +221,7 @@
<div <div
id={`floating-buttons-${id}`} id={`floating-buttons-${id}`}
class="absolute rounded-lg mt-1 text-xs z-[9999]" class="absolute rounded-lg mt-1 text-xs z-9999"
style="display: none" style="display: none"
> >
{#if responseContent === null} {#if responseContent === null}
@ -230,7 +230,7 @@
class="flex flex-row gap-0.5 shrink-0 p-1 bg-white dark:bg-gray-850 dark:text-gray-100 text-medium rounded-lg shadow-xl" class="flex flex-row gap-0.5 shrink-0 p-1 bg-white dark:bg-gray-850 dark:text-gray-100 text-medium rounded-lg shadow-xl"
> >
<button <button
class="px-1 hover:bg-gray-50 dark:hover:bg-gray-800 rounded flex items-center gap-1 min-w-fit" class="px-1 hover:bg-gray-50 dark:hover:bg-gray-800 rounded-sm flex items-center gap-1 min-w-fit"
on:click={async () => { on:click={async () => {
selectedText = window.getSelection().toString(); selectedText = window.getSelection().toString();
floatingInput = true; floatingInput = true;
@ -249,7 +249,7 @@
<div class="shrink-0">Ask</div> <div class="shrink-0">Ask</div>
</button> </button>
<button <button
class="px-1 hover:bg-gray-50 dark:hover:bg-gray-800 rounded flex items-center gap-1 min-w-fit" class="px-1 hover:bg-gray-50 dark:hover:bg-gray-800 rounded-sm flex items-center gap-1 min-w-fit"
on:click={() => { on:click={() => {
selectedText = window.getSelection().toString(); selectedText = window.getSelection().toString();
explainHandler(); explainHandler();
@ -262,12 +262,12 @@
</div> </div>
{:else} {:else}
<div <div
class="py-1 flex dark:text-gray-100 bg-gray-50 dark:bg-gray-800 border dark:border-gray-800 w-72 rounded-full shadow-xl" class="py-1 flex dark:text-gray-100 bg-gray-50 dark:bg-gray-800 border dark:border-gray-850 w-72 rounded-full shadow-xl"
> >
<input <input
type="text" type="text"
id="floating-message-input" id="floating-message-input"
class="ml-5 bg-transparent outline-none w-full flex-1 text-sm" class="ml-5 bg-transparent outline-hidden w-full flex-1 text-sm"
placeholder={$i18n.t('Ask a question')} placeholder={$i18n.t('Ask a question')}
bind:value={floatingInputValue} bind:value={floatingInputValue}
on:keydown={(e) => { on:keydown={(e) => {

View file

@ -74,7 +74,7 @@
<div class="" slot="content"> <div class="" slot="content">
<textarea <textarea
bind:value={params.system} bind:value={params.system}
class="w-full text-xs py-1.5 bg-transparent outline-none resize-none" class="w-full text-xs py-1.5 bg-transparent outline-hidden resize-none"
rows="4" rows="4"
placeholder={$i18n.t('Enter system prompt')} placeholder={$i18n.t('Enter system prompt')}
/> />

View file

@ -148,7 +148,7 @@
<div class="flex gap-2"> <div class="flex gap-2">
<div class="flex-1"> <div class="flex-1">
<select <select
class=" w-full rounded text-xs py-2 px-1 bg-transparent outline-none" class=" w-full rounded-sm text-xs py-2 px-1 bg-transparent outline-hidden"
bind:value={tab} bind:value={tab}
placeholder="Select" placeholder="Select"
> >
@ -161,7 +161,7 @@
<div class="flex-1"> <div class="flex-1">
<select <select
class="w-full rounded py-2 px-1 text-xs bg-transparent outline-none" class="w-full rounded-sm py-2 px-1 text-xs bg-transparent outline-hidden"
bind:value={selectedId} bind:value={selectedId}
on:change={async () => { on:change={async () => {
await tick(); await tick();

View file

@ -249,7 +249,9 @@
return; return;
} }
if (['image/gif', 'image/webp', 'image/jpeg', 'image/png'].includes(file['type'])) { if (
['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;
@ -394,7 +396,7 @@
<div class="w-full relative"> <div class="w-full relative">
{#if atSelectedModel !== undefined || selectedToolIds.length > 0 || webSearchEnabled || ($settings?.webSearch ?? false) === 'always' || imageGenerationEnabled || codeInterpreterEnabled} {#if atSelectedModel !== undefined || selectedToolIds.length > 0 || webSearchEnabled || ($settings?.webSearch ?? false) === 'always' || imageGenerationEnabled || codeInterpreterEnabled}
<div <div
class="px-3 pb-0.5 pt-1.5 text-left w-full flex flex-col absolute bottom-0 left-0 right-0 bg-gradient-to-t from-white dark:from-gray-900 z-10" class="px-3 pb-0.5 pt-1.5 text-left w-full flex flex-col absolute bottom-0 left-0 right-0 bg-linear-to-t from-white dark:from-gray-900 z-10"
> >
{#if selectedToolIds.length > 0} {#if selectedToolIds.length > 0}
<div class="flex items-center justify-between w-full"> <div class="flex items-center justify-between w-full">
@ -413,7 +415,7 @@
}) as tool, toolIdx (toolIdx)} }) as tool, toolIdx (toolIdx)}
<Tooltip <Tooltip
content={tool?.meta?.description ?? ''} content={tool?.meta?.description ?? ''}
className=" {toolIdx !== 0 ? 'pl-0.5' : ''} flex-shrink-0" className=" {toolIdx !== 0 ? 'pl-0.5' : ''} shrink-0"
placement="top" placement="top"
> >
{tool.name} {tool.name}
@ -682,7 +684,7 @@
<div class="px-2.5"> <div class="px-2.5">
{#if $settings?.richTextInput ?? true} {#if $settings?.richTextInput ?? true}
<div <div
class="scrollbar-hidden text-left bg-transparent dark:text-gray-100 outline-none w-full pt-3 px-1 resize-none h-fit max-h-80 overflow-auto" class="scrollbar-hidden text-left bg-transparent dark:text-gray-100 outline-hidden w-full pt-3 px-1 resize-none h-fit max-h-80 overflow-auto"
> >
<RichTextInput <RichTextInput
bind:this={chatInputElement} bind:this={chatInputElement}
@ -886,7 +888,7 @@
<textarea <textarea
id="chat-input" id="chat-input"
bind:this={chatInputElement} bind:this={chatInputElement}
class="scrollbar-hidden bg-transparent dark:text-gray-100 outline-none w-full pt-3 px-1 resize-none" class="scrollbar-hidden bg-transparent dark:text-gray-100 outline-hidden w-full pt-3 px-1 resize-none"
placeholder={placeholder ? placeholder : $i18n.t('Send a Message')} placeholder={placeholder ? placeholder : $i18n.t('Send a Message')}
bind:value={prompt} bind:value={prompt}
on:keypress={(e) => { on:keypress={(e) => {
@ -1114,7 +1116,7 @@
}} }}
> >
<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-none focus:outline-none" 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="More"
> >
@ -1138,10 +1140,10 @@
<button <button
on:click|preventDefault={() => (webSearchEnabled = !webSearchEnabled)} on:click|preventDefault={() => (webSearchEnabled = !webSearchEnabled)}
type="button" type="button"
class="px-1.5 @sm:px-2.5 py-1.5 flex gap-1.5 items-center text-sm rounded-full font-medium transition-colors duration-300 focus:outline-none max-w-full overflow-hidden {webSearchEnabled || class="px-1.5 @sm:px-2.5 py-1.5 flex gap-1.5 items-center text-sm rounded-full font-medium transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden {webSearchEnabled ||
($settings?.webSearch ?? false) === 'always' ($settings?.webSearch ?? false) === 'always'
? 'bg-blue-100 dark:bg-blue-500/20 text-blue-500 dark:text-blue-400' ? 'bg-blue-100 dark:bg-blue-500/20 text-blue-500 dark:text-blue-400'
: 'bg-transparent text-gray-600 dark:text-gray-400 border-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800'}" : 'bg-transparent text-gray-600 dark:text-gray-300 border-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800'}"
> >
<GlobeAlt className="size-5" strokeWidth="1.75" /> <GlobeAlt className="size-5" strokeWidth="1.75" />
<span <span
@ -1158,7 +1160,7 @@
on:click|preventDefault={() => on:click|preventDefault={() =>
(imageGenerationEnabled = !imageGenerationEnabled)} (imageGenerationEnabled = !imageGenerationEnabled)}
type="button" type="button"
class="px-1.5 @sm:px-2.5 py-1.5 flex gap-1.5 items-center text-sm rounded-full font-medium transition-colors duration-300 focus:outline-none max-w-full overflow-hidden {imageGenerationEnabled class="px-1.5 @sm:px-2.5 py-1.5 flex gap-1.5 items-center text-sm rounded-full font-medium transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden {imageGenerationEnabled
? 'bg-gray-100 dark:bg-gray-500/20 text-gray-600 dark:text-gray-400' ? 'bg-gray-100 dark:bg-gray-500/20 text-gray-600 dark:text-gray-400'
: 'bg-transparent text-gray-600 dark:text-gray-300 border-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800 '}" : 'bg-transparent text-gray-600 dark:text-gray-300 border-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800 '}"
> >
@ -1177,7 +1179,7 @@
on:click|preventDefault={() => on:click|preventDefault={() =>
(codeInterpreterEnabled = !codeInterpreterEnabled)} (codeInterpreterEnabled = !codeInterpreterEnabled)}
type="button" type="button"
class="px-1.5 @sm:px-2.5 py-1.5 flex gap-1.5 items-center text-sm rounded-full font-medium transition-colors duration-300 focus:outline-none max-w-full overflow-hidden {codeInterpreterEnabled class="px-1.5 @sm:px-2.5 py-1.5 flex gap-1.5 items-center text-sm rounded-full font-medium transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden {codeInterpreterEnabled
? 'bg-gray-100 dark:bg-gray-500/20 text-gray-600 dark:text-gray-400' ? 'bg-gray-100 dark:bg-gray-500/20 text-gray-600 dark:text-gray-400'
: 'bg-transparent text-gray-600 dark:text-gray-300 border-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800 '}" : 'bg-transparent text-gray-600 dark:text-gray-300 border-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800 '}"
> >
@ -1193,7 +1195,7 @@
</div> </div>
</div> </div>
<div class="self-end flex space-x-1 mr-1 flex-shrink-0"> <div class="self-end flex space-x-1 mr-1 shrink-0">
{#if !history?.currentId || history.messages[history.currentId]?.done == true} {#if !history?.currentId || history.messages[history.currentId]?.done == true}
<Tooltip content={$i18n.t('Record voice')}> <Tooltip content={$i18n.t('Record voice')}>
<button <button

View file

@ -26,7 +26,7 @@
<div slot="content"> <div slot="content">
<DropdownMenu.Content <DropdownMenu.Content
class="w-full max-w-[180px] rounded-lg px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-[9999] bg-white dark:bg-gray-900 dark:text-white shadow-sm" class="w-full max-w-[180px] rounded-lg px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-9999 bg-white dark:bg-gray-900 dark:text-white shadow-xs"
sideOffset={6} sideOffset={6}
side="top" side="top"
align="start" align="start"

View file

@ -112,7 +112,7 @@
id="commands-container" id="commands-container"
class="px-2 mb-2 text-left w-full absolute bottom-0 left-0 right-0 z-10" class="px-2 mb-2 text-left w-full absolute bottom-0 left-0 right-0 z-10"
> >
<div class="flex w-full rounded-xl border border-gray-50 dark:border-gray-850"> <div class="flex w-full rounded-xl border border-gray-100 dark:border-gray-850">
<div <div
class="max-h-60 flex flex-col w-full rounded-xl bg-white dark:bg-gray-900 dark:text-gray-100" class="max-h-60 flex flex-col w-full rounded-xl bg-white dark:bg-gray-900 dark:text-gray-100"
> >

View file

@ -161,7 +161,7 @@
id="commands-container" id="commands-container"
class="px-2 mb-2 text-left w-full absolute bottom-0 left-0 right-0 z-10" class="px-2 mb-2 text-left w-full absolute bottom-0 left-0 right-0 z-10"
> >
<div class="flex w-full rounded-xl border border-gray-50 dark:border-gray-850"> <div class="flex w-full rounded-xl border border-gray-100 dark:border-gray-850">
<div <div
class="max-h-60 flex flex-col w-full rounded-xl bg-white dark:bg-gray-900 dark:text-gray-100" class="max-h-60 flex flex-col w-full rounded-xl bg-white dark:bg-gray-900 dark:text-gray-100"
> >
@ -185,25 +185,25 @@
<div class=" font-medium text-black dark:text-gray-100 flex items-center gap-1"> <div class=" font-medium text-black dark:text-gray-100 flex items-center gap-1">
{#if item.legacy} {#if item.legacy}
<div <div
class="bg-gray-500/20 text-gray-700 dark:text-gray-200 rounded uppercase text-xs font-bold px-1 flex-shrink-0" class="bg-gray-500/20 text-gray-700 dark:text-gray-200 rounded-sm uppercase text-xs font-bold px-1 shrink-0"
> >
Legacy Legacy
</div> </div>
{:else if item?.meta?.document} {:else if item?.meta?.document}
<div <div
class="bg-gray-500/20 text-gray-700 dark:text-gray-200 rounded uppercase text-xs font-bold px-1 flex-shrink-0" class="bg-gray-500/20 text-gray-700 dark:text-gray-200 rounded-sm uppercase text-xs font-bold px-1 shrink-0"
> >
Document Document
</div> </div>
{:else if item?.type === 'file'} {:else if item?.type === 'file'}
<div <div
class="bg-gray-500/20 text-gray-700 dark:text-gray-200 rounded uppercase text-xs font-bold px-1 flex-shrink-0" class="bg-gray-500/20 text-gray-700 dark:text-gray-200 rounded-sm uppercase text-xs font-bold px-1 shrink-0"
> >
File File
</div> </div>
{:else} {:else}
<div <div
class="bg-green-500/20 text-green-700 dark:text-green-200 rounded uppercase text-xs font-bold px-1 flex-shrink-0" class="bg-green-500/20 text-green-700 dark:text-green-200 rounded-sm uppercase text-xs font-bold px-1 shrink-0"
> >
Collection Collection
</div> </div>
@ -238,7 +238,7 @@
class=" font-medium text-black dark:text-gray-100 flex items-center gap-1" class=" font-medium text-black dark:text-gray-100 flex items-center gap-1"
> >
<div <div
class="bg-gray-500/20 text-gray-700 dark:text-gray-200 rounded uppercase text-xs font-bold px-1 flex-shrink-0" class="bg-gray-500/20 text-gray-700 dark:text-gray-200 rounded-sm uppercase text-xs font-bold px-1 shrink-0"
> >
File File
</div> </div>

View file

@ -70,7 +70,7 @@
id="commands-container" id="commands-container"
class="px-2 mb-2 text-left w-full absolute bottom-0 left-0 right-0 z-10" class="px-2 mb-2 text-left w-full absolute bottom-0 left-0 right-0 z-10"
> >
<div class="flex w-full rounded-xl border border-gray-50 dark:border-gray-850"> <div class="flex w-full rounded-xl border border-gray-100 dark:border-gray-850">
<div <div
class="max-h-60 flex flex-col w-full rounded-xl bg-white dark:bg-gray-900 dark:text-gray-100" class="max-h-60 flex flex-col w-full rounded-xl bg-white dark:bg-gray-900 dark:text-gray-100"
> >

View file

@ -139,7 +139,7 @@
id="commands-container" id="commands-container"
class="px-2 mb-2 text-left w-full absolute bottom-0 left-0 right-0 z-10" class="px-2 mb-2 text-left w-full absolute bottom-0 left-0 right-0 z-10"
> >
<div class="flex w-full rounded-xl border border-gray-50 dark:border-gray-850"> <div class="flex w-full rounded-xl border border-gray-100 dark:border-gray-850">
<div <div
class="max-h-60 flex flex-col w-full rounded-xl bg-white dark:bg-gray-900 dark:text-gray-100" class="max-h-60 flex flex-col w-full rounded-xl bg-white dark:bg-gray-900 dark:text-gray-100"
> >

View file

@ -19,12 +19,12 @@
bind:this={overlayElement} bind:this={overlayElement}
class="fixed {$showSidebar class="fixed {$showSidebar
? 'left-0 md:left-[260px] md:w-[calc(100%-260px)]' ? 'left-0 md:left-[260px] md:w-[calc(100%-260px)]'
: 'left-0'} fixed top-0 right-0 bottom-0 w-full h-full flex z-[9999] touch-none pointer-events-none" : 'left-0'} fixed top-0 right-0 bottom-0 w-full h-full flex z-9999 touch-none pointer-events-none"
id="dropzone" id="dropzone"
role="region" role="region"
aria-label="Drag and Drop Container" aria-label="Drag and Drop Container"
> >
<div class="absolute w-full h-full backdrop-blur bg-gray-800/40 flex justify-center"> <div class="absolute w-full h-full backdrop-blur-sm bg-gray-800/40 flex justify-center">
<div class="m-auto pt-64 flex flex-col justify-center"> <div class="m-auto pt-64 flex flex-col justify-center">
<div class="max-w-md"> <div class="max-w-md">
<AddFilesPlaceholder /> <AddFilesPlaceholder />

View file

@ -92,7 +92,7 @@
<div slot="content"> <div slot="content">
<DropdownMenu.Content <DropdownMenu.Content
class="w-full max-w-[220px] rounded-xl px-1 py-1 border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow" class="w-full max-w-[220px] rounded-xl px-1 py-1 border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-sm"
sideOffset={15} sideOffset={15}
alignOffset={-8} alignOffset={-8}
side="top" side="top"
@ -114,7 +114,7 @@
placement="top-start" placement="top-start"
className="flex flex-1 gap-2 items-center" className="flex flex-1 gap-2 items-center"
> >
<div class="flex-shrink-0"> <div class="shrink-0">
<WrenchSolid /> <WrenchSolid />
</div> </div>
@ -122,7 +122,7 @@
</Tooltip> </Tooltip>
</div> </div>
<div class=" flex-shrink-0"> <div class=" shrink-0">
<Switch <Switch
state={tools[toolId].enabled} state={tools[toolId].enabled}
on:change={async (e) => { on:change={async (e) => {

View file

@ -362,7 +362,7 @@
{#each visualizerData.slice().reverse() as rms} {#each visualizerData.slice().reverse() as rms}
<div class="flex items-center h-full"> <div class="flex items-center h-full">
<div <div
class="w-[2px] flex-shrink-0 class="w-[2px] shrink-0
{loading {loading
? ' bg-gray-500 dark:bg-gray-400 ' ? ' bg-gray-500 dark:bg-gray-400 '

View file

@ -1,6 +1,14 @@
<script lang="ts"> <script lang="ts">
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { chats, config, settings, user as _user, mobile, currentChatPage } from '$lib/stores'; import {
chats,
config,
settings,
user as _user,
mobile,
currentChatPage,
temporaryChatEnabled
} from '$lib/stores';
import { tick, getContext, onMount, createEventDispatcher } from 'svelte'; import { tick, getContext, onMount, createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
@ -85,6 +93,7 @@
}; };
const updateChat = async () => { const updateChat = async () => {
if (!$temporaryChatEnabled) {
history = history; history = history;
await tick(); await tick();
await updateChatById(localStorage.token, chatId, { await updateChatById(localStorage.token, chatId, {
@ -94,6 +103,7 @@
currentChatPage.set(1); currentChatPage.set(1);
await chats.set(await getChatList(localStorage.token, $currentChatPage)); await chats.set(await getChatList(localStorage.token, $currentChatPage));
}
}; };
const showPreviousMessage = async (message) => { const showPreviousMessage = async (message) => {

View file

@ -101,7 +101,7 @@
{#each citations as citation, idx} {#each citations as citation, idx}
<button <button
id={`source-${idx}`} id={`source-${idx}`}
class="no-toggle outline-none flex dark:text-gray-300 p-1 bg-white dark:bg-gray-900 rounded-xl max-w-96" class="no-toggle outline-hidden flex dark:text-gray-300 p-1 bg-white dark:bg-gray-900 rounded-xl max-w-96"
on:click={() => { on:click={() => {
showCitationModal = true; showCitationModal = true;
selectedCitation = citation; selectedCitation = citation;
@ -133,14 +133,14 @@
<div <div
class="flex-1 flex items-center gap-1 overflow-auto scrollbar-none w-full max-w-full" class="flex-1 flex items-center gap-1 overflow-auto scrollbar-none w-full max-w-full"
> >
<span class="whitespace-nowrap hidden sm:inline flex-shrink-0" <span class="whitespace-nowrap hidden sm:inline shrink-0"
>{$i18n.t('References from')}</span >{$i18n.t('References from')}</span
> >
<div class="flex items-center overflow-auto scrollbar-none w-full max-w-full flex-1"> <div class="flex items-center overflow-auto scrollbar-none w-full max-w-full flex-1">
<div class="flex text-xs font-medium items-center"> <div class="flex text-xs font-medium items-center">
{#each citations.slice(0, 2) as citation, idx} {#each citations.slice(0, 2) as citation, idx}
<button <button
class="no-toggle outline-none flex dark:text-gray-300 p-1 bg-gray-50 hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 transition rounded-xl max-w-96" class="no-toggle outline-hidden flex dark:text-gray-300 p-1 bg-gray-50 hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 transition rounded-xl max-w-96"
on:click={() => { on:click={() => {
showCitationModal = true; showCitationModal = true;
selectedCitation = citation; selectedCitation = citation;
@ -161,13 +161,13 @@
{/each} {/each}
</div> </div>
</div> </div>
<div class="flex items-center gap-1 whitespace-nowrap flex-shrink-0"> <div class="flex items-center gap-1 whitespace-nowrap shrink-0">
<span class="hidden sm:inline">{$i18n.t('and')}</span> <span class="hidden sm:inline">{$i18n.t('and')}</span>
{citations.length - 2} {citations.length - 2}
<span>{$i18n.t('more')}</span> <span>{$i18n.t('more')}</span>
</div> </div>
</div> </div>
<div class="flex-shrink-0"> <div class="shrink-0">
{#if isCollapsibleOpen} {#if isCollapsibleOpen}
<ChevronUp strokeWidth="3.5" className="size-3.5" /> <ChevronUp strokeWidth="3.5" className="size-3.5" />
{:else} {:else}
@ -180,7 +180,7 @@
{#each citations as citation, idx} {#each citations as citation, idx}
<button <button
id={`source-${idx}`} id={`source-${idx}`}
class="no-toggle outline-none flex dark:text-gray-300 p-1 bg-gray-50 hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 transition rounded-xl max-w-96" class="no-toggle outline-hidden flex dark:text-gray-300 p-1 bg-gray-50 hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 transition rounded-xl max-w-96"
on:click={() => { on:click={() => {
showCitationModal = true; showCitationModal = true;
selectedCitation = citation; selectedCitation = citation;

View file

@ -90,7 +90,7 @@
> >
<div class="text-sm dark:text-gray-400 flex items-center gap-2 w-fit"> <div class="text-sm dark:text-gray-400 flex items-center gap-2 w-fit">
<a <a
class="hover:text-gray-500 hover:dark:text-gray-100 underline flex-grow" class="hover:text-gray-500 dark:hover:text-gray-100 underline grow"
href={document?.metadata?.file_id href={document?.metadata?.file_id
? `${WEBUI_API_BASE_URL}/files/${document?.metadata?.file_id}/content${document?.metadata?.page !== undefined ? `#page=${document.metadata.page + 1}` : ''}` ? `${WEBUI_API_BASE_URL}/files/${document?.metadata?.file_id}/content${document?.metadata?.page !== undefined ? `#page=${document.metadata.page + 1}` : ''}`
: document.source?.url?.includes('http') : document.source?.url?.includes('http')
@ -122,7 +122,9 @@
<div class="text-sm my-1 dark:text-gray-400 flex items-center gap-2 w-fit"> <div class="text-sm my-1 dark:text-gray-400 flex items-center gap-2 w-fit">
{#if showPercentage} {#if showPercentage}
{@const percentage = calculatePercentage(document.distance)} {@const percentage = calculatePercentage(document.distance)}
<span class={`px-1 rounded font-medium ${getRelevanceColor(percentage)}`}> <span
class={`px-1 rounded-sm font-medium ${getRelevanceColor(percentage)}`}
>
{percentage.toFixed(2)}% {percentage.toFixed(2)}%
</span> </span>
<span class="text-gray-500 dark:text-gray-500"> <span class="text-gray-500 dark:text-gray-500">
@ -166,7 +168,7 @@
</div> </div>
{#if documentIdx !== mergedDocuments.length - 1} {#if documentIdx !== mergedDocuments.length - 1}
<hr class=" dark:border-gray-850 my-3" /> <hr class="border-gray-100 dark:border-gray-850 my-3" />
{/if} {/if}
{/each} {/each}
</div> </div>

View file

@ -20,6 +20,9 @@
import PyodideWorker from '$lib/workers/pyodide.worker?worker'; import PyodideWorker from '$lib/workers/pyodide.worker?worker';
import CodeEditor from '$lib/components/common/CodeEditor.svelte'; import CodeEditor from '$lib/components/common/CodeEditor.svelte';
import SvgPanZoom from '$lib/components/common/SVGPanZoom.svelte'; import SvgPanZoom from '$lib/components/common/SVGPanZoom.svelte';
import { config } from '$lib/stores';
import { executeCode } from '$lib/apis/utils';
import { toast } from 'svelte-sonner';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
@ -120,7 +123,20 @@
}; };
const executePython = async (code) => { const executePython = async (code) => {
if ($config?.code?.engine === 'jupyter') {
const output = await executeCode(localStorage.token, code).catch((error) => {
toast.error(`${error}`);
return null;
});
if (output) {
stdout = output.stdout;
stderr = output.stderr;
result = output.result;
}
} else {
executePythonAsWorker(code); executePythonAsWorker(code);
}
}; };
const executePythonAsWorker = async (code) => { const executePythonAsWorker = async (code) => {
@ -302,7 +318,7 @@
{#if lang === 'mermaid'} {#if lang === 'mermaid'}
{#if mermaidHtml} {#if mermaidHtml}
<SvgPanZoom <SvgPanZoom
className=" border border-gray-50 dark:border-gray-850 rounded-lg max-h-fit overflow-hidden" className=" border border-gray-100 dark:border-gray-850 rounded-lg max-h-fit overflow-hidden"
svg={mermaidHtml} svg={mermaidHtml}
content={_token.text} content={_token.text}
/> />
@ -377,7 +393,7 @@
{#if executing || stdout || stderr || result} {#if executing || stdout || stderr || result}
<div <div
class="bg-gray-50 dark:bg-[#202123] dark:text-white !rounded-b-lg py-4 px-4 flex flex-col gap-2" class="bg-gray-50 dark:bg-[#202123] dark:text-white rounded-b-lg! py-4 px-4 flex flex-col gap-2"
> >
{#if executing} {#if executing}
<div class=" "> <div class=" ">

View file

@ -99,7 +99,7 @@
{/if} {/if}
{#if codeExecution?.result?.files && codeExecution?.result?.files.length > 0} {#if codeExecution?.result?.files && codeExecution?.result?.files.length > 0}
<div class="flex flex-col w-full"> <div class="flex flex-col w-full">
<hr class=" dark:border-gray-850 my-2" /> <hr class="border-gray-100 dark:border-gray-850 my-2" />
<div class=" text-sm font-medium dark:text-gray-300"> <div class=" text-sm font-medium dark:text-gray-300">
{$i18n.t('Files')} {$i18n.t('Files')}
</div> </div>

View file

@ -74,7 +74,7 @@
<!-- {JSON.stringify(tokens)} --> <!-- {JSON.stringify(tokens)} -->
{#each tokens as token, tokenIdx (tokenIdx)} {#each tokens as token, tokenIdx (tokenIdx)}
{#if token.type === 'hr'} {#if token.type === 'hr'}
<hr class=" border-gray-50 dark:border-gray-850" /> <hr class=" border-gray-100 dark:border-gray-850" />
{:else if token.type === 'heading'} {:else if token.type === 'heading'}
<svelte:element this={headerComponent(token.depth)}> <svelte:element this={headerComponent(token.depth)}>
<MarkdownInlineTokens id={`${id}-${tokenIdx}-h`} tokens={token.tokens} {onSourceClick} /> <MarkdownInlineTokens id={`${id}-${tokenIdx}-h`} tokens={token.tokens} {onSourceClick} />
@ -115,11 +115,11 @@
{#each token.header as header, headerIdx} {#each token.header as header, headerIdx}
<th <th
scope="col" scope="col"
class="!px-3 !py-1.5 cursor-pointer border border-gray-50 dark:border-gray-850" class="px-3! py-1.5! cursor-pointer border border-gray-100 dark:border-gray-850"
style={token.align[headerIdx] ? '' : `text-align: ${token.align[headerIdx]}`} style={token.align[headerIdx] ? '' : `text-align: ${token.align[headerIdx]}`}
> >
<div class="flex flex-col gap-1.5 text-left"> <div class="flex flex-col gap-1.5 text-left">
<div class="flex-shrink-0 break-normal"> <div class="shrink-0 break-normal">
<MarkdownInlineTokens <MarkdownInlineTokens
id={`${id}-${tokenIdx}-header-${headerIdx}`} id={`${id}-${tokenIdx}-header-${headerIdx}`}
tokens={header.tokens} tokens={header.tokens}
@ -136,7 +136,7 @@
<tr class="bg-white dark:bg-gray-900 dark:border-gray-850 text-xs"> <tr class="bg-white dark:bg-gray-900 dark:border-gray-850 text-xs">
{#each row ?? [] as cell, cellIdx} {#each row ?? [] as cell, cellIdx}
<td <td
class="!px-3 !py-1.5 text-gray-900 dark:text-white w-max border border-gray-50 dark:border-gray-850" class="px-3! py-1.5! text-gray-900 dark:text-white w-max border border-gray-100 dark:border-gray-850"
style={token.align[cellIdx] ? '' : `text-align: ${token.align[cellIdx]}`} style={token.align[cellIdx] ? '' : `text-align: ${token.align[cellIdx]}`}
> >
<div class="flex flex-col break-normal"> <div class="flex flex-col break-normal">
@ -169,14 +169,14 @@
</div> </div>
</div> </div>
{:else if token.type === 'blockquote'} {:else if token.type === 'blockquote'}
<blockquote> <blockquote dir="auto">
<svelte:self id={`${id}-${tokenIdx}`} tokens={token.tokens} {onTaskClick} {onSourceClick} /> <svelte:self id={`${id}-${tokenIdx}`} tokens={token.tokens} {onTaskClick} {onSourceClick} />
</blockquote> </blockquote>
{:else if token.type === 'list'} {:else if token.type === 'list'}
{#if token.ordered} {#if token.ordered}
<ol start={token.start || 1}> <ol start={token.start || 1}>
{#each token.items as item, itemIdx} {#each token.items as item, itemIdx}
<li> <li dir="auto">
{#if item?.task} {#if item?.task}
<input <input
class=" translate-y-[1px] -translate-x-1" class=" translate-y-[1px] -translate-x-1"
@ -208,7 +208,7 @@
{:else} {:else}
<ul> <ul>
{#each token.items as item, itemIdx} {#each token.items as item, itemIdx}
<li> <li dir="auto">
{#if item?.task} {#if item?.task}
<input <input
class=" translate-y-[1px] -translate-x-1" class=" translate-y-[1px] -translate-x-1"
@ -268,7 +268,7 @@
onload="this.style.height=(this.contentWindow.document.body.scrollHeight+20)+'px';" onload="this.style.height=(this.contentWindow.document.body.scrollHeight+20)+'px';"
></iframe> ></iframe>
{:else if token.type === 'paragraph'} {:else if token.type === 'paragraph'}
<p> <p dir="auto">
<MarkdownInlineTokens <MarkdownInlineTokens
id={`${id}-${tokenIdx}-p`} id={`${id}-${tokenIdx}-p`}
tokens={token.tokens ?? []} tokens={token.tokens ?? []}
@ -277,7 +277,7 @@
</p> </p>
{:else if token.type === 'text'} {:else if token.type === 'text'}
{#if top} {#if top}
<p> <p dir="auto">
{#if token.tokens} {#if token.tokens}
<MarkdownInlineTokens id={`${id}-${tokenIdx}-t`} tokens={token.tokens} {onSourceClick} /> <MarkdownInlineTokens id={`${id}-${tokenIdx}-t`} tokens={token.tokens} {onSourceClick} />
{:else} {:else}

View file

@ -194,10 +194,10 @@
<div <div
class=" snap-center w-full max-w-full m-1 border {history.messages[messageId] class=" snap-center w-full max-w-full m-1 border {history.messages[messageId]
?.modelIdx == modelIdx ?.modelIdx == modelIdx
? `border-gray-100 dark:border-gray-800 border-[1.5px] ${ ? `border-gray-100 dark:border-gray-850 border-[1.5px] ${
$mobile ? 'min-w-full' : 'min-w-80' $mobile ? 'min-w-full' : 'min-w-80'
}` }`
: `border-gray-50 dark:border-gray-850 border-dashed ${ : `border-gray-100 dark:border-gray-850 border-dashed ${
$mobile ? 'min-w-full' : 'min-w-80' $mobile ? 'min-w-full' : 'min-w-80'
}`} transition-all p-5 rounded-2xl" }`} transition-all p-5 rounded-2xl"
on:click={async () => { on:click={async () => {
@ -286,7 +286,7 @@
</div> </div>
{#if isLastMessage} {#if isLastMessage}
<div class=" flex-shrink-0 text-gray-600 dark:text-gray-500 mt-1"> <div class=" shrink-0 text-gray-600 dark:text-gray-500 mt-1">
<Tooltip content={$i18n.t('Merge Responses')} placement="bottom"> <Tooltip content={$i18n.t('Merge Responses')} placement="bottom">
<button <button
type="button" type="button"

View file

@ -110,7 +110,7 @@
{/if} {/if}
<div <div
class=" my-2.5 rounded-xl px-4 py-3 border border-gray-50 dark:border-gray-850" class=" my-2.5 rounded-xl px-4 py-3 border border-gray-100 dark:border-gray-850"
id="message-feedback-{message.id}" id="message-feedback-{message.id}"
> >
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
@ -142,10 +142,10 @@
<!-- 1-10 scale --> <!-- 1-10 scale -->
{#each Array.from({ length: 10 }).map((_, i) => i + 1) as rating} {#each Array.from({ length: 10 }).map((_, i) => i + 1) as rating}
<button <button
class="size-7 text-sm border border-gray-50 dark:border-gray-850 hover:bg-gray-50 dark:hover:bg-gray-850 {detailedRating === class="size-7 text-sm border border-gray-100 dark:border-gray-850 hover:bg-gray-50 dark:hover:bg-gray-850 {detailedRating ===
rating rating
? 'bg-gray-100 dark:bg-gray-800' ? 'bg-gray-100 dark:bg-gray-800'
: ''} transition rounded-full disabled:cursor-not-allowed disabled:text-gray-500 disabled:bg-white disabled:dark:bg-gray-900" : ''} transition rounded-full disabled:cursor-not-allowed disabled:text-gray-500 disabled:bg-white dark:disabled:bg-gray-900"
on:click={() => { on:click={() => {
detailedRating = rating; detailedRating = rating;
}} }}
@ -175,7 +175,7 @@
<div class="flex flex-wrap gap-1.5 text-sm mt-1.5"> <div class="flex flex-wrap gap-1.5 text-sm mt-1.5">
{#each reasons as reason} {#each reasons as reason}
<button <button
class="px-3 py-0.5 border border-gray-50 dark:border-gray-850 hover:bg-gray-50 dark:hover:bg-gray-850 {selectedReason === class="px-3 py-0.5 border border-gray-100 dark:border-gray-850 hover:bg-gray-50 dark:hover:bg-gray-850 {selectedReason ===
reason reason
? 'bg-gray-100 dark:bg-gray-800' ? 'bg-gray-100 dark:bg-gray-800'
: ''} transition rounded-xl" : ''} transition rounded-xl"
@ -223,7 +223,7 @@
<div class="mt-2"> <div class="mt-2">
<textarea <textarea
bind:value={comment} bind:value={comment}
class="w-full text-sm px-1 py-2 bg-transparent outline-none resize-none rounded-xl" class="w-full text-sm px-1 py-2 bg-transparent outline-hidden resize-none rounded-xl"
placeholder={$i18n.t('Feel free to add specific details')} placeholder={$i18n.t('Feel free to add specific details')}
rows="3" rows="3"
/> />

View file

@ -37,6 +37,9 @@
import Spinner from '$lib/components/common/Spinner.svelte'; import Spinner from '$lib/components/common/Spinner.svelte';
import WebSearchResults from './ResponseMessage/WebSearchResults.svelte'; import WebSearchResults from './ResponseMessage/WebSearchResults.svelte';
import Sparkles from '$lib/components/icons/Sparkles.svelte'; import Sparkles from '$lib/components/icons/Sparkles.svelte';
import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
import Error from './Error.svelte'; import Error from './Error.svelte';
import Citations from './Citations.svelte'; import Citations from './Citations.svelte';
import CodeExecutions from './CodeExecutions.svelte'; import CodeExecutions from './CodeExecutions.svelte';
@ -126,6 +129,8 @@
export let isLastMessage = true; export let isLastMessage = true;
export let readOnly = false; export let readOnly = false;
let showDeleteConfirm = false;
let model = null; let model = null;
$: model = $models.find((m) => m.id === message.model); $: model = $models.find((m) => m.id === message.model);
@ -516,13 +521,21 @@
}); });
</script> </script>
<DeleteConfirmDialog
bind:show={showDeleteConfirm}
title={$i18n.t('Delete message?')}
on:confirm={() => {
deleteMessageHandler();
}}
/>
{#key message.id} {#key message.id}
<div <div
class=" flex w-full message-{message.id}" class=" flex w-full message-{message.id}"
id="message-{message.id}" id="message-{message.id}"
dir={$settings.chatDirection} dir={$settings.chatDirection}
> >
<div class={`flex-shrink-0 ${($settings?.chatDirection ?? 'LTR') === 'LTR' ? 'mr-3' : 'ml-3'}`}> <div class={`shrink-0 ${($settings?.chatDirection ?? 'LTR') === 'LTR' ? 'mr-3' : 'ml-3'}`}>
<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`)}
@ -645,7 +658,7 @@
<textarea <textarea
id="message-edit-{message.id}" id="message-edit-{message.id}"
bind:this={editTextAreaElement} bind:this={editTextAreaElement}
class=" bg-transparent outline-none w-full resize-none" class=" bg-transparent outline-hidden w-full resize-none"
bind:value={editedContent} bind:value={editedContent}
on:input={(e) => { on:input={(e) => {
e.target.style.height = ''; e.target.style.height = '';
@ -1247,7 +1260,7 @@
? 'visible' ? 'visible'
: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition regenerate-response-button" : 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition regenerate-response-button"
on:click={() => { on:click={() => {
deleteMessageHandler(); showDeleteConfirm = true;
}} }}
> >
<svg <svg

View file

@ -5,31 +5,41 @@
<div class="w-full mt-2 mb-2"> <div class="w-full mt-2 mb-2">
<div class="animate-pulse flex w-full"> <div class="animate-pulse flex w-full">
<div class="{size === 'md' ? 'space-y-2' : 'space-y-1.5'} w-full"> <div class="{size === 'md' ? 'space-y-2' : 'space-y-1.5'} w-full">
<div class="{size === 'md' ? 'h-2' : 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded mr-14" /> <div
class="{size === 'md' ? 'h-2' : 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded-sm mr-14"
/>
<div class="grid grid-cols-3 gap-4"> <div class="grid grid-cols-3 gap-4">
<div <div
class="{size === 'md' ? 'h-2' : 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded col-span-2" class="{size === 'md'
/> ? 'h-2'
<div : 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded-sm col-span-2"
class="{size === 'md' ? 'h-2' : 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded 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 col-span-1"
/>
<div
class="{size === 'md' ? 'h-2' : 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded col-span-2"
/> />
<div <div
class="{size === 'md' class="{size === 'md'
? 'h-2' ? 'h-2'
: 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded col-span-1 mr-4" : '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>
<div class="{size === 'md' ? 'h-2' : 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded" /> <div class="{size === 'md' ? 'h-2' : 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded-sm" />
</div> </div>
</div> </div>
</div> </div>

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