Merge branch 'dev' into dev-i8n-french

This commit is contained in:
flefevre 2025-05-01 20:17:50 +02:00 committed by GitHub
commit fb8a7e8549
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
160 changed files with 6395 additions and 2969 deletions

View File

@ -73,4 +73,4 @@
### Contributor License Agreement
By submitting this pull request, I confirm that I have read and fully agree to the [CONTRIBUTOR_LICENSE_AGREEMENT](CONTRIBUTOR_LICENSE_AGREEMENT), and I am providing my contributions under its terms.
By submitting this pull request, I confirm that I have read and fully agree to the [Contributor License Agreement (CLA)](/CONTRIBUTOR_LICENSE_AGREEMENT), and I am providing my contributions under its terms.

View File

@ -4,7 +4,7 @@
ARG USE_CUDA=false
ARG USE_OLLAMA=false
# Tested with cu117 for CUDA 11 and cu121 for CUDA 12 (default)
ARG USE_CUDA_VER=cu121
ARG USE_CUDA_VER=cu128
# any sentence transformer model; models to use can be found at https://huggingface.co/models?library=sentence-transformers
# Leaderboard: https://huggingface.co/spaces/mteb/leaderboard
# for better performance and multilangauge support use "intfloat/multilingual-e5-large" (~2.5GB) or "intfloat/multilingual-e5-base" (~1.5GB)

View File

@ -1,4 +1,4 @@
Copyright (c) 2023-2025 Timothy Jaeryang Baek
Copyright (c) 2023-2025 Timothy Jaeryang Baek (Open WebUI)
All rights reserved.
Redistribution and use in source and binary forms, with or without
@ -15,6 +15,12 @@ modification, are permitted provided that the following conditions are met:
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
4. Notwithstanding any other provision of this License, and as a material condition of the rights granted herein, licensees are strictly prohibited from altering, removing, obscuring, or replacing any "Open WebUI" branding, including but not limited to the name, logo, or any visual, textual, or symbolic identifiers that distinguish the software and its interfaces, in any deployment or distribution, regardless of the number of users, except as explicitly set forth in Clauses 5 and 6 below.
5. The branding restriction enumerated in Clause 4 shall not apply in the following limited circumstances: (i) deployments or distributions where the total number of end users (defined as individual natural persons with direct access to the application) does not exceed fifty (50) within any rolling thirty (30) day period; (ii) cases in which the licensee is an official contributor to the codebase—with a substantive code change successfully merged into the main branch of the official codebase maintained by the copyright holder—who has obtained specific prior written permission for branding adjustment from the copyright holder; or (iii) where the licensee has obtained a duly executed enterprise license expressly permitting such modification. For all other cases, any removal or alteration of the "Open WebUI" branding shall constitute a material breach of license.
6. All code, modifications, or derivative works incorporated into this project prior to the incorporation of this branding clause remain licensed under the BSD 3-Clause License, and prior contributors retain all BSD-3 rights therein; if any such contributor requests the removal of their BSD-3-licensed code, the copyright holder will do so, and any replacement code will be licensed under the project's primary license then in effect. By contributing after this clause's adoption, you agree to the project's Contributor License Agreement (CLA) and to these updated terms for all new contributions.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE

View File

@ -7,7 +7,6 @@
![GitHub language count](https://img.shields.io/github/languages/count/open-webui/open-webui)
![GitHub top language](https://img.shields.io/github/languages/top/open-webui/open-webui)
![GitHub last commit](https://img.shields.io/github/last-commit/open-webui/open-webui?color=red)
![Hits](https://hits.seeyoufarm.com/api/count/incr/badge.svg?url=https%3A%2F%2Fgithub.com%2Follama-webui%2Follama-wbui&count_bg=%2379C83D&title_bg=%23555555&icon=&icon_color=%23E7E7E7&title=hits&edge_flat=false)
[![Discord](https://img.shields.io/badge/Discord-Open_WebUI-blue?logo=discord&logoColor=white)](https://discord.gg/5rJgQTnV4s)
[![](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/tjbck)
@ -206,7 +205,7 @@ Discover upcoming features on our roadmap in the [Open WebUI Documentation](http
## License 📜
This project is licensed under the [BSD-3-Clause License](LICENSE) - see the [LICENSE](LICENSE) file for details. 📄
This project is licensed under the [Open WebUI License](LICENSE), a revised BSD-3-Clause license. You receive all the same rights as the classic BSD-3 license: you can use, modify, and distribute the software, including in proprietary and commercial products, with minimal restrictions. The only additional requirement is to preserve the "Open WebUI" branding, as detailed in the LICENSE file. For full terms, see the [LICENSE](LICENSE) document. 📄
## Support 💬

View File

@ -76,7 +76,7 @@ def serve(
from open_webui.env import UVICORN_WORKERS # Import the workers setting
uvicorn.run(
open_webui.main.app,
"open_webui.main:app",
host=host,
port=port,
forwarded_allow_ips="*",

View File

@ -509,6 +509,12 @@ ENABLE_OAUTH_GROUP_MANAGEMENT = PersistentConfig(
os.environ.get("ENABLE_OAUTH_GROUP_MANAGEMENT", "False").lower() == "true",
)
ENABLE_OAUTH_GROUP_CREATION = PersistentConfig(
"ENABLE_OAUTH_GROUP_CREATION",
"oauth.enable_group_creation",
os.environ.get("ENABLE_OAUTH_GROUP_CREATION", "False").lower() == "true",
)
OAUTH_ROLES_CLAIM = PersistentConfig(
"OAUTH_ROLES_CLAIM",
"oauth.roles_claim",
@ -952,10 +958,15 @@ DEFAULT_MODELS = PersistentConfig(
"DEFAULT_MODELS", "ui.default_models", os.environ.get("DEFAULT_MODELS", None)
)
DEFAULT_PROMPT_SUGGESTIONS = PersistentConfig(
"DEFAULT_PROMPT_SUGGESTIONS",
"ui.prompt_suggestions",
[
try:
default_prompt_suggestions = json.loads(
os.environ.get("DEFAULT_PROMPT_SUGGESTIONS", "[]")
)
except Exception as e:
log.exception(f"Error loading DEFAULT_PROMPT_SUGGESTIONS: {e}")
default_prompt_suggestions = []
if default_prompt_suggestions == []:
default_prompt_suggestions = [
{
"title": ["Help me study", "vocabulary for a college entrance exam"],
"content": "Help me study vocabulary: write a sentence for me to fill in the blank, and I'll try to pick the correct option.",
@ -983,7 +994,11 @@ DEFAULT_PROMPT_SUGGESTIONS = PersistentConfig(
"title": ["Overcome procrastination", "give me tips"],
"content": "Could you start by asking me about instances when I procrastinate the most and then give me some suggestions to overcome it?",
},
],
]
DEFAULT_PROMPT_SUGGESTIONS = PersistentConfig(
"DEFAULT_PROMPT_SUGGESTIONS",
"ui.prompt_suggestions",
default_prompt_suggestions,
)
MODEL_ORDER_LIST = PersistentConfig(
@ -1062,6 +1077,14 @@ USER_PERMISSIONS_CHAT_EDIT = (
os.environ.get("USER_PERMISSIONS_CHAT_EDIT", "True").lower() == "true"
)
USER_PERMISSIONS_CHAT_SHARE = (
os.environ.get("USER_PERMISSIONS_CHAT_SHARE", "True").lower() == "true"
)
USER_PERMISSIONS_CHAT_EXPORT = (
os.environ.get("USER_PERMISSIONS_CHAT_EXPORT", "True").lower() == "true"
)
USER_PERMISSIONS_CHAT_STT = (
os.environ.get("USER_PERMISSIONS_CHAT_STT", "True").lower() == "true"
)
@ -1126,6 +1149,8 @@ DEFAULT_USER_PERMISSIONS = {
"file_upload": USER_PERMISSIONS_CHAT_FILE_UPLOAD,
"delete": USER_PERMISSIONS_CHAT_DELETE,
"edit": USER_PERMISSIONS_CHAT_EDIT,
"share": USER_PERMISSIONS_CHAT_SHARE,
"export": USER_PERMISSIONS_CHAT_EXPORT,
"stt": USER_PERMISSIONS_CHAT_STT,
"tts": USER_PERMISSIONS_CHAT_TTS,
"call": USER_PERMISSIONS_CHAT_CALL,
@ -1153,6 +1178,11 @@ ENABLE_CHANNELS = PersistentConfig(
os.environ.get("ENABLE_CHANNELS", "False").lower() == "true",
)
ENABLE_NOTES = PersistentConfig(
"ENABLE_NOTES",
"notes.enable",
os.environ.get("ENABLE_NOTES", "True").lower() == "true",
)
ENABLE_EVALUATION_ARENA_MODELS = PersistentConfig(
"ENABLE_EVALUATION_ARENA_MODELS",
@ -1203,6 +1233,9 @@ ENABLE_USER_WEBHOOKS = PersistentConfig(
os.environ.get("ENABLE_USER_WEBHOOKS", "True").lower() == "true",
)
# FastAPI / AnyIO settings
THREAD_POOL_SIZE = int(os.getenv("THREAD_POOL_SIZE", "0"))
def validate_cors_origins(origins):
for origin in origins:
@ -1229,7 +1262,9 @@ def validate_cors_origin(origin):
# To test CORS_ALLOW_ORIGIN locally, you can set something like
# CORS_ALLOW_ORIGIN=http://localhost:5173;http://localhost:8080
# in your .env file depending on your frontend port, 5173 in this case.
CORS_ALLOW_ORIGIN = os.environ.get("CORS_ALLOW_ORIGIN", "*").split(";")
CORS_ALLOW_ORIGIN = os.environ.get(
"CORS_ALLOW_ORIGIN", "*;http://localhost:5173;http://localhost:8080"
).split(";")
if "*" in CORS_ALLOW_ORIGIN:
log.warning(
@ -1693,6 +1728,9 @@ MILVUS_TOKEN = os.environ.get("MILVUS_TOKEN", None)
# Qdrant
QDRANT_URI = os.environ.get("QDRANT_URI", None)
QDRANT_API_KEY = os.environ.get("QDRANT_API_KEY", None)
QDRANT_ON_DISK = os.environ.get("QDRANT_ON_DISK", "false").lower() == "true"
QDRANT_PREFER_GRPC = os.environ.get("QDRANT_PREFER_GRPC", "False").lower() == "true"
QDRANT_GRPC_PORT = int(os.environ.get("QDRANT_GRPC_PORT", "6334"))
# OpenSearch
OPENSEARCH_URI = os.environ.get("OPENSEARCH_URI", "https://localhost:9200")
@ -1724,6 +1762,14 @@ PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH = int(
os.environ.get("PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH", "1536")
)
# Pinecone
PINECONE_API_KEY = os.environ.get("PINECONE_API_KEY", None)
PINECONE_ENVIRONMENT = os.environ.get("PINECONE_ENVIRONMENT", None)
PINECONE_INDEX_NAME = os.getenv("PINECONE_INDEX_NAME", "open-webui-index")
PINECONE_DIMENSION = int(os.getenv("PINECONE_DIMENSION", 1536)) # or 3072, 1024, 768
PINECONE_METRIC = os.getenv("PINECONE_METRIC", "cosine")
PINECONE_CLOUD = os.getenv("PINECONE_CLOUD", "aws") # or "gcp" or "azure"
####################################
# Information Retrieval (RAG)
####################################
@ -1760,6 +1806,13 @@ ONEDRIVE_CLIENT_ID = PersistentConfig(
os.environ.get("ONEDRIVE_CLIENT_ID", ""),
)
ONEDRIVE_SHAREPOINT_URL = PersistentConfig(
"ONEDRIVE_SHAREPOINT_URL",
"onedrive.sharepoint_url",
os.environ.get("ONEDRIVE_SHAREPOINT_URL", ""),
)
# RAG Content Extraction
CONTENT_EXTRACTION_ENGINE = PersistentConfig(
"CONTENT_EXTRACTION_ENGINE",
@ -2087,6 +2140,24 @@ SEARXNG_QUERY_URL = PersistentConfig(
os.getenv("SEARXNG_QUERY_URL", ""),
)
YACY_QUERY_URL = PersistentConfig(
"YACY_QUERY_URL",
"rag.web.search.yacy_query_url",
os.getenv("YACY_QUERY_URL", ""),
)
YACY_USERNAME = PersistentConfig(
"YACY_USERNAME",
"rag.web.search.yacy_username",
os.getenv("YACY_USERNAME", ""),
)
YACY_PASSWORD = PersistentConfig(
"YACY_PASSWORD",
"rag.web.search.yacy_password",
os.getenv("YACY_PASSWORD", ""),
)
GOOGLE_PSE_API_KEY = PersistentConfig(
"GOOGLE_PSE_API_KEY",
"rag.web.search.google_pse_api_key",
@ -2251,6 +2322,29 @@ FIRECRAWL_API_BASE_URL = PersistentConfig(
os.environ.get("FIRECRAWL_API_BASE_URL", "https://api.firecrawl.dev"),
)
EXTERNAL_WEB_SEARCH_URL = PersistentConfig(
"EXTERNAL_WEB_SEARCH_URL",
"rag.web.search.external_web_search_url",
os.environ.get("EXTERNAL_WEB_SEARCH_URL", ""),
)
EXTERNAL_WEB_SEARCH_API_KEY = PersistentConfig(
"EXTERNAL_WEB_SEARCH_API_KEY",
"rag.web.search.external_web_search_api_key",
os.environ.get("EXTERNAL_WEB_SEARCH_API_KEY", ""),
)
EXTERNAL_WEB_LOADER_URL = PersistentConfig(
"EXTERNAL_WEB_LOADER_URL",
"rag.web.loader.external_web_loader_url",
os.environ.get("EXTERNAL_WEB_LOADER_URL", ""),
)
EXTERNAL_WEB_LOADER_API_KEY = PersistentConfig(
"EXTERNAL_WEB_LOADER_API_KEY",
"rag.web.loader.external_web_loader_api_key",
os.environ.get("EXTERNAL_WEB_LOADER_API_KEY", ""),
)
####################################
# Images
@ -2561,6 +2655,18 @@ AUDIO_STT_AZURE_LOCALES = PersistentConfig(
os.getenv("AUDIO_STT_AZURE_LOCALES", ""),
)
AUDIO_STT_AZURE_BASE_URL = PersistentConfig(
"AUDIO_STT_AZURE_BASE_URL",
"audio.stt.azure.base_url",
os.getenv("AUDIO_STT_AZURE_BASE_URL", ""),
)
AUDIO_STT_AZURE_MAX_SPEAKERS = PersistentConfig(
"AUDIO_STT_AZURE_MAX_SPEAKERS",
"audio.stt.azure.max_speakers",
os.getenv("AUDIO_STT_AZURE_MAX_SPEAKERS", "3"),
)
AUDIO_TTS_OPENAI_API_BASE_URL = PersistentConfig(
"AUDIO_TTS_OPENAI_API_BASE_URL",
"audio.tts.openai.api_base_url",

View File

@ -354,6 +354,10 @@ BYPASS_MODEL_ACCESS_CONTROL = (
os.environ.get("BYPASS_MODEL_ACCESS_CONTROL", "False").lower() == "true"
)
WEBUI_AUTH_SIGNOUT_REDIRECT_URL = os.environ.get(
"WEBUI_AUTH_SIGNOUT_REDIRECT_URL", None
)
####################################
# WEBUI_SECRET_KEY
####################################
@ -409,6 +413,11 @@ else:
except Exception:
AIOHTTP_CLIENT_TIMEOUT = 300
AIOHTTP_CLIENT_SESSION_SSL = (
os.environ.get("AIOHTTP_CLIENT_SESSION_SSL", "True").lower() == "true"
)
AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST = os.environ.get(
"AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST",
os.environ.get("AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST", "10"),
@ -437,6 +446,56 @@ else:
except Exception:
AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA = 10
AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL = (
os.environ.get("AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL", "True").lower() == "true"
)
####################################
# SENTENCE TRANSFORMERS
####################################
SENTENCE_TRANSFORMERS_BACKEND = os.environ.get("SENTENCE_TRANSFORMERS_BACKEND", "")
if SENTENCE_TRANSFORMERS_BACKEND == "":
SENTENCE_TRANSFORMERS_BACKEND = "torch"
SENTENCE_TRANSFORMERS_MODEL_KWARGS = os.environ.get(
"SENTENCE_TRANSFORMERS_MODEL_KWARGS", ""
)
if SENTENCE_TRANSFORMERS_MODEL_KWARGS == "":
SENTENCE_TRANSFORMERS_MODEL_KWARGS = None
else:
try:
SENTENCE_TRANSFORMERS_MODEL_KWARGS = json.loads(
SENTENCE_TRANSFORMERS_MODEL_KWARGS
)
except Exception:
SENTENCE_TRANSFORMERS_MODEL_KWARGS = None
SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND = os.environ.get(
"SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND", ""
)
if SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND == "":
SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND = "torch"
SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS = os.environ.get(
"SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS", ""
)
if SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS == "":
SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS = None
else:
try:
SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS = json.loads(
SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS
)
except Exception:
SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS = None
####################################
# OFFLINE_MODE
####################################
@ -446,6 +505,7 @@ OFFLINE_MODE = os.environ.get("OFFLINE_MODE", "false").lower() == "true"
if OFFLINE_MODE:
os.environ["HF_HUB_OFFLINE"] = "1"
####################################
# AUDIT LOGGING
####################################
@ -467,6 +527,7 @@ AUDIT_EXCLUDED_PATHS = os.getenv("AUDIT_EXCLUDED_PATHS", "/chats,/chat,/folders"
AUDIT_EXCLUDED_PATHS = [path.strip() for path in AUDIT_EXCLUDED_PATHS]
AUDIT_EXCLUDED_PATHS = [path.lstrip("/") for path in AUDIT_EXCLUDED_PATHS]
####################################
# OPENTELEMETRY
####################################

View File

@ -17,6 +17,7 @@ from sqlalchemy import text
from typing import Optional
from aiocache import cached
import aiohttp
import anyio.to_thread
import requests
@ -100,11 +101,14 @@ from open_webui.config import (
# OpenAI
ENABLE_OPENAI_API,
ONEDRIVE_CLIENT_ID,
ONEDRIVE_SHAREPOINT_URL,
OPENAI_API_BASE_URLS,
OPENAI_API_KEYS,
OPENAI_API_CONFIGS,
# Direct Connections
ENABLE_DIRECT_CONNECTIONS,
# Thread pool size for FastAPI/AnyIO
THREAD_POOL_SIZE,
# Tool Server Configs
TOOL_SERVER_CONNECTIONS,
# Code Execution
@ -151,6 +155,8 @@ from open_webui.config import (
AUDIO_STT_AZURE_API_KEY,
AUDIO_STT_AZURE_REGION,
AUDIO_STT_AZURE_LOCALES,
AUDIO_STT_AZURE_BASE_URL,
AUDIO_STT_AZURE_MAX_SPEAKERS,
AUDIO_TTS_API_KEY,
AUDIO_TTS_ENGINE,
AUDIO_TTS_MODEL,
@ -219,6 +225,9 @@ from open_webui.config import (
SERPAPI_API_KEY,
SERPAPI_ENGINE,
SEARXNG_QUERY_URL,
YACY_QUERY_URL,
YACY_USERNAME,
YACY_PASSWORD,
SERPER_API_KEY,
SERPLY_API_KEY,
SERPSTACK_API_KEY,
@ -240,12 +249,17 @@ from open_webui.config import (
GOOGLE_DRIVE_CLIENT_ID,
GOOGLE_DRIVE_API_KEY,
ONEDRIVE_CLIENT_ID,
ONEDRIVE_SHAREPOINT_URL,
ENABLE_RAG_HYBRID_SEARCH,
ENABLE_RAG_LOCAL_WEB_FETCH,
ENABLE_WEB_LOADER_SSL_VERIFICATION,
ENABLE_GOOGLE_DRIVE_INTEGRATION,
ENABLE_ONEDRIVE_INTEGRATION,
UPLOAD_DIR,
EXTERNAL_WEB_SEARCH_URL,
EXTERNAL_WEB_SEARCH_API_KEY,
EXTERNAL_WEB_LOADER_URL,
EXTERNAL_WEB_LOADER_API_KEY,
# WebUI
WEBUI_AUTH,
WEBUI_NAME,
@ -260,6 +274,7 @@ from open_webui.config import (
ENABLE_API_KEY_ENDPOINT_RESTRICTIONS,
API_KEY_ALLOWED_ENDPOINTS,
ENABLE_CHANNELS,
ENABLE_NOTES,
ENABLE_COMMUNITY_SHARING,
ENABLE_MESSAGE_RATING,
ENABLE_USER_WEBHOOKS,
@ -341,6 +356,7 @@ from open_webui.env import (
WEBUI_SESSION_COOKIE_SECURE,
WEBUI_AUTH_TRUSTED_EMAIL_HEADER,
WEBUI_AUTH_TRUSTED_NAME_HEADER,
WEBUI_AUTH_SIGNOUT_REDIRECT_URL,
ENABLE_WEBSOCKET_SUPPORT,
BYPASS_MODEL_ACCESS_CONTROL,
RESET_CONFIG_ON_START,
@ -370,6 +386,7 @@ from open_webui.utils.auth import (
get_admin_user,
get_verified_user,
)
from open_webui.utils.plugin import install_tool_and_function_dependencies
from open_webui.utils.oauth import OAuthManager
from open_webui.utils.security_headers import SecurityHeadersMiddleware
@ -432,7 +449,18 @@ async def lifespan(app: FastAPI):
if LICENSE_KEY:
get_license_data(app, LICENSE_KEY)
# This should be blocking (sync) so functions are not deactivated on first /get_models calls
# when the first user lands on the / route.
log.info("Installing external dependencies of functions and tools...")
install_tool_and_function_dependencies()
pool_size = THREAD_POOL_SIZE
if pool_size and pool_size > 0:
limiter = anyio.to_thread.current_default_thread_limiter()
limiter.total_tokens = pool_size
asyncio.create_task(periodic_usage_pool_cleanup())
yield
@ -543,6 +571,7 @@ app.state.config.MODEL_ORDER_LIST = MODEL_ORDER_LIST
app.state.config.ENABLE_CHANNELS = ENABLE_CHANNELS
app.state.config.ENABLE_NOTES = ENABLE_NOTES
app.state.config.ENABLE_COMMUNITY_SHARING = ENABLE_COMMUNITY_SHARING
app.state.config.ENABLE_MESSAGE_RATING = ENABLE_MESSAGE_RATING
app.state.config.ENABLE_USER_WEBHOOKS = ENABLE_USER_WEBHOOKS
@ -576,6 +605,7 @@ app.state.config.LDAP_CIPHERS = LDAP_CIPHERS
app.state.AUTH_TRUSTED_EMAIL_HEADER = WEBUI_AUTH_TRUSTED_EMAIL_HEADER
app.state.AUTH_TRUSTED_NAME_HEADER = WEBUI_AUTH_TRUSTED_NAME_HEADER
app.state.WEBUI_AUTH_SIGNOUT_REDIRECT_URL = WEBUI_AUTH_SIGNOUT_REDIRECT_URL
app.state.EXTERNAL_PWA_MANIFEST_URL = EXTERNAL_PWA_MANIFEST_URL
app.state.USER_COUNT = None
@ -646,6 +676,9 @@ app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL = (
app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION = ENABLE_GOOGLE_DRIVE_INTEGRATION
app.state.config.ENABLE_ONEDRIVE_INTEGRATION = ENABLE_ONEDRIVE_INTEGRATION
app.state.config.SEARXNG_QUERY_URL = SEARXNG_QUERY_URL
app.state.config.YACY_QUERY_URL = YACY_QUERY_URL
app.state.config.YACY_USERNAME = YACY_USERNAME
app.state.config.YACY_PASSWORD = YACY_PASSWORD
app.state.config.GOOGLE_PSE_API_KEY = GOOGLE_PSE_API_KEY
app.state.config.GOOGLE_PSE_ENGINE_ID = GOOGLE_PSE_ENGINE_ID
app.state.config.BRAVE_SEARCH_API_KEY = BRAVE_SEARCH_API_KEY
@ -668,6 +701,10 @@ app.state.config.EXA_API_KEY = EXA_API_KEY
app.state.config.PERPLEXITY_API_KEY = PERPLEXITY_API_KEY
app.state.config.SOUGOU_API_SID = SOUGOU_API_SID
app.state.config.SOUGOU_API_SK = SOUGOU_API_SK
app.state.config.EXTERNAL_WEB_SEARCH_URL = EXTERNAL_WEB_SEARCH_URL
app.state.config.EXTERNAL_WEB_SEARCH_API_KEY = EXTERNAL_WEB_SEARCH_API_KEY
app.state.config.EXTERNAL_WEB_LOADER_URL = EXTERNAL_WEB_LOADER_URL
app.state.config.EXTERNAL_WEB_LOADER_API_KEY = EXTERNAL_WEB_LOADER_API_KEY
app.state.config.PLAYWRIGHT_WS_URL = PLAYWRIGHT_WS_URL
@ -796,6 +833,8 @@ app.state.config.DEEPGRAM_API_KEY = DEEPGRAM_API_KEY
app.state.config.AUDIO_STT_AZURE_API_KEY = AUDIO_STT_AZURE_API_KEY
app.state.config.AUDIO_STT_AZURE_REGION = AUDIO_STT_AZURE_REGION
app.state.config.AUDIO_STT_AZURE_LOCALES = AUDIO_STT_AZURE_LOCALES
app.state.config.AUDIO_STT_AZURE_BASE_URL = AUDIO_STT_AZURE_BASE_URL
app.state.config.AUDIO_STT_AZURE_MAX_SPEAKERS = AUDIO_STT_AZURE_MAX_SPEAKERS
app.state.config.TTS_OPENAI_API_BASE_URL = AUDIO_TTS_OPENAI_API_BASE_URL
app.state.config.TTS_OPENAI_API_KEY = AUDIO_TTS_OPENAI_API_KEY
@ -869,7 +908,8 @@ class RedirectMiddleware(BaseHTTPMiddleware):
# Check for the specific watch path and the presence of 'v' parameter
if path.endswith("/watch") and "v" in query_params:
video_id = query_params["v"][0] # Extract the first 'v' parameter
# Extract the first 'v' parameter
video_id = query_params["v"][0]
encoded_video_id = urlencode({"youtube": video_id})
redirect_url = f"/?{encoded_video_id}"
return RedirectResponse(url=redirect_url)
@ -1283,6 +1323,7 @@ async def get_app_config(request: Request):
{
"enable_direct_connections": app.state.config.ENABLE_DIRECT_CONNECTIONS,
"enable_channels": app.state.config.ENABLE_CHANNELS,
"enable_notes": app.state.config.ENABLE_NOTES,
"enable_web_search": app.state.config.ENABLE_WEB_SEARCH,
"enable_code_execution": app.state.config.ENABLE_CODE_EXECUTION,
"enable_code_interpreter": app.state.config.ENABLE_CODE_INTERPRETER,
@ -1327,7 +1368,10 @@ async def get_app_config(request: Request):
"client_id": GOOGLE_DRIVE_CLIENT_ID.value,
"api_key": GOOGLE_DRIVE_API_KEY.value,
},
"onedrive": {"client_id": ONEDRIVE_CLIENT_ID.value},
"onedrive": {
"client_id": ONEDRIVE_CLIENT_ID.value,
"sharepoint_url": ONEDRIVE_SHAREPOINT_URL.value,
},
"license_metadata": app.state.LICENSE_METADATA,
**(
{
@ -1439,7 +1483,7 @@ async def get_manifest_json():
"start_url": "/",
"display": "standalone",
"background_color": "#343541",
"orientation": "natural",
"orientation": "any",
"icons": [
{
"src": "/static/logo.png",

View File

@ -10,6 +10,8 @@ from open_webui.models.groups import Groups
from pydantic import BaseModel, ConfigDict
from sqlalchemy import BigInteger, Column, String, Text
from sqlalchemy import or_
####################
# User DB Schema
@ -67,6 +69,11 @@ class UserModel(BaseModel):
####################
class UserListResponse(BaseModel):
users: list[UserModel]
total: int
class UserResponse(BaseModel):
id: str
name: str
@ -160,11 +167,63 @@ class UsersTable:
return None
def get_users(
self, skip: Optional[int] = None, limit: Optional[int] = None
) -> list[UserModel]:
self,
filter: Optional[dict] = None,
skip: Optional[int] = None,
limit: Optional[int] = None,
) -> UserListResponse:
with get_db() as db:
query = db.query(User)
query = db.query(User).order_by(User.created_at.desc())
if filter:
query_key = filter.get("query")
if query_key:
query = query.filter(
or_(
User.name.ilike(f"%{query_key}%"),
User.email.ilike(f"%{query_key}%"),
)
)
order_by = filter.get("order_by")
direction = filter.get("direction")
if order_by == "name":
if direction == "asc":
query = query.order_by(User.name.asc())
else:
query = query.order_by(User.name.desc())
elif order_by == "email":
if direction == "asc":
query = query.order_by(User.email.asc())
else:
query = query.order_by(User.email.desc())
elif order_by == "created_at":
if direction == "asc":
query = query.order_by(User.created_at.asc())
else:
query = query.order_by(User.created_at.desc())
elif order_by == "last_active_at":
if direction == "asc":
query = query.order_by(User.last_active_at.asc())
else:
query = query.order_by(User.last_active_at.desc())
elif order_by == "updated_at":
if direction == "asc":
query = query.order_by(User.updated_at.asc())
else:
query = query.order_by(User.updated_at.desc())
elif order_by == "role":
if direction == "asc":
query = query.order_by(User.role.asc())
else:
query = query.order_by(User.role.desc())
else:
query = query.order_by(User.created_at.desc())
if skip:
query = query.offset(skip)
@ -172,8 +231,10 @@ class UsersTable:
query = query.limit(limit)
users = query.all()
return [UserModel.model_validate(user) for user in users]
return {
"users": [UserModel.model_validate(user) for user in users],
"total": db.query(User).count(),
}
def get_users_by_user_ids(self, user_ids: list[str]) -> list[UserModel]:
with get_db() as db:

View File

@ -0,0 +1,53 @@
import requests
import logging
from typing import Iterator, List, Union
from langchain_core.document_loaders import BaseLoader
from langchain_core.documents import Document
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
class ExternalLoader(BaseLoader):
def __init__(
self,
web_paths: Union[str, List[str]],
external_url: str,
external_api_key: str,
continue_on_failure: bool = True,
**kwargs,
) -> None:
self.external_url = external_url
self.external_api_key = external_api_key
self.urls = web_paths if isinstance(web_paths, list) else [web_paths]
self.continue_on_failure = continue_on_failure
def lazy_load(self) -> Iterator[Document]:
batch_size = 20
for i in range(0, len(self.urls), batch_size):
urls = self.urls[i : i + batch_size]
try:
response = requests.post(
self.external_url,
headers={
"User-Agent": "Open WebUI (https://github.com/open-webui/open-webui) RAG Bot",
"Authorization": f"Bearer {self.external_api_key}",
},
json={
"urls": urls,
},
)
response.raise_for_status()
results = response.json()
for result in results:
yield Document(
page_content=result.get("page_content", ""),
metadata=result.get("metadata", {}),
)
except Exception as e:
if self.continue_on_failure:
log.error(f"Error extracting content from batch {urls}: {e}")
else:
raise e

View File

@ -207,7 +207,7 @@ def merge_and_sort_query_results(query_results: list[dict], k: int) -> dict:
for distance, document, metadata in zip(distances, documents, metadatas):
if isinstance(document, str):
doc_hash = hashlib.md5(
doc_hash = hashlib.sha256(
document.encode()
).hexdigest() # Compute a hash for uniqueness
@ -260,23 +260,47 @@ def query_collection(
k: int,
) -> dict:
results = []
for query in queries:
log.debug(f"query_collection:query {query}")
query_embedding = embedding_function(query, prefix=RAG_EMBEDDING_QUERY_PREFIX)
for collection_name in collection_names:
error = False
def process_query_collection(collection_name, query_embedding):
try:
if collection_name:
try:
result = query_doc(
collection_name=collection_name,
k=k,
query_embedding=query_embedding,
)
if result is not None:
results.append(result.model_dump())
except Exception as e:
log.exception(f"Error when querying the collection: {e}")
else:
pass
result = query_doc(
collection_name=collection_name,
k=k,
query_embedding=query_embedding,
)
if result is not None:
return result.model_dump(), None
return None, None
except Exception as e:
log.exception(f"Error when querying the collection: {e}")
return None, e
# Generate all query embeddings (in one call)
query_embeddings = embedding_function(queries, prefix=RAG_EMBEDDING_QUERY_PREFIX)
log.debug(
f"query_collection: processing {len(queries)} queries across {len(collection_names)} collections"
)
with ThreadPoolExecutor() as executor:
future_results = []
for query_embedding in query_embeddings:
for collection_name in collection_names:
result = executor.submit(
process_query_collection, collection_name, query_embedding
)
future_results.append(result)
task_results = [future.result() for future in future_results]
for result, err in task_results:
if err is not None:
error = True
elif result is not None:
results.append(result)
if error and not results:
log.warning("All collection queries failed. No results returned.")
return merge_and_sort_query_results(results, k=k)

View File

@ -20,6 +20,10 @@ elif VECTOR_DB == "elasticsearch":
from open_webui.retrieval.vector.dbs.elasticsearch import ElasticsearchClient
VECTOR_DB_CLIENT = ElasticsearchClient()
elif VECTOR_DB == "pinecone":
from open_webui.retrieval.vector.dbs.pinecone import PineconeClient
VECTOR_DB_CLIENT = PineconeClient()
else:
from open_webui.retrieval.vector.dbs.chroma import ChromaClient

View File

@ -5,7 +5,12 @@ from chromadb.utils.batch_utils import create_batches
from typing import Optional
from open_webui.retrieval.vector.main import VectorItem, SearchResult, GetResult
from open_webui.retrieval.vector.main import (
VectorDBBase,
VectorItem,
SearchResult,
GetResult,
)
from open_webui.config import (
CHROMA_DATA_PATH,
CHROMA_HTTP_HOST,
@ -23,7 +28,7 @@ log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
class ChromaClient:
class ChromaClient(VectorDBBase):
def __init__(self):
settings_dict = {
"allow_reset": True,

View File

@ -2,7 +2,12 @@ from elasticsearch import Elasticsearch, BadRequestError
from typing import Optional
import ssl
from elasticsearch.helpers import bulk, scan
from open_webui.retrieval.vector.main import VectorItem, SearchResult, GetResult
from open_webui.retrieval.vector.main import (
VectorDBBase,
VectorItem,
SearchResult,
GetResult,
)
from open_webui.config import (
ELASTICSEARCH_URL,
ELASTICSEARCH_CA_CERTS,
@ -15,7 +20,7 @@ from open_webui.config import (
)
class ElasticsearchClient:
class ElasticsearchClient(VectorDBBase):
"""
Important:
in order to reduce the number of indexes and since the embedding vector length is fixed, we avoid creating

View File

@ -4,7 +4,12 @@ import json
import logging
from typing import Optional
from open_webui.retrieval.vector.main import VectorItem, SearchResult, GetResult
from open_webui.retrieval.vector.main import (
VectorDBBase,
VectorItem,
SearchResult,
GetResult,
)
from open_webui.config import (
MILVUS_URI,
MILVUS_DB,
@ -16,7 +21,7 @@ log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
class MilvusClient:
class MilvusClient(VectorDBBase):
def __init__(self):
self.collection_prefix = "open_webui"
if MILVUS_TOKEN is None:

View File

@ -2,7 +2,12 @@ from opensearchpy import OpenSearch
from opensearchpy.helpers import bulk
from typing import Optional
from open_webui.retrieval.vector.main import VectorItem, SearchResult, GetResult
from open_webui.retrieval.vector.main import (
VectorDBBase,
VectorItem,
SearchResult,
GetResult,
)
from open_webui.config import (
OPENSEARCH_URI,
OPENSEARCH_SSL,
@ -12,7 +17,7 @@ from open_webui.config import (
)
class OpenSearchClient:
class OpenSearchClient(VectorDBBase):
def __init__(self):
self.index_prefix = "open_webui"
self.client = OpenSearch(

View File

@ -22,7 +22,12 @@ from pgvector.sqlalchemy import Vector
from sqlalchemy.ext.mutable import MutableDict
from sqlalchemy.exc import NoSuchTableError
from open_webui.retrieval.vector.main import VectorItem, SearchResult, GetResult
from open_webui.retrieval.vector.main import (
VectorDBBase,
VectorItem,
SearchResult,
GetResult,
)
from open_webui.config import PGVECTOR_DB_URL, PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH
from open_webui.env import SRC_LOG_LEVELS
@ -44,7 +49,7 @@ class DocumentChunk(Base):
vmetadata = Column(MutableDict.as_mutable(JSONB), nullable=True)
class PgvectorClient:
class PgvectorClient(VectorDBBase):
def __init__(self) -> None:
# if no pgvector uri, use the existing database connection
@ -136,9 +141,8 @@ class PgvectorClient:
# Pad the vector with zeros
vector += [0.0] * (VECTOR_LENGTH - current_length)
elif current_length > VECTOR_LENGTH:
raise Exception(
f"Vector length {current_length} not supported. Max length must be <= {VECTOR_LENGTH}"
)
# Truncate the vector to VECTOR_LENGTH
vector = vector[:VECTOR_LENGTH]
return vector
def insert(self, collection_name: str, items: List[VectorItem]) -> None:

View File

@ -0,0 +1,412 @@
from typing import Optional, List, Dict, Any, Union
import logging
from pinecone import Pinecone, ServerlessSpec
from open_webui.retrieval.vector.main import (
VectorDBBase,
VectorItem,
SearchResult,
GetResult,
)
from open_webui.config import (
PINECONE_API_KEY,
PINECONE_ENVIRONMENT,
PINECONE_INDEX_NAME,
PINECONE_DIMENSION,
PINECONE_METRIC,
PINECONE_CLOUD,
)
from open_webui.env import SRC_LOG_LEVELS
NO_LIMIT = 10000 # Reasonable limit to avoid overwhelming the system
BATCH_SIZE = 100 # Recommended batch size for Pinecone operations
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
class PineconeClient(VectorDBBase):
def __init__(self):
self.collection_prefix = "open-webui"
# Validate required configuration
self._validate_config()
# Store configuration values
self.api_key = PINECONE_API_KEY
self.environment = PINECONE_ENVIRONMENT
self.index_name = PINECONE_INDEX_NAME
self.dimension = PINECONE_DIMENSION
self.metric = PINECONE_METRIC
self.cloud = PINECONE_CLOUD
# Initialize Pinecone client
self.client = Pinecone(api_key=self.api_key)
# Create index if it doesn't exist
self._initialize_index()
def _validate_config(self) -> None:
"""Validate that all required configuration variables are set."""
missing_vars = []
if not PINECONE_API_KEY:
missing_vars.append("PINECONE_API_KEY")
if not PINECONE_ENVIRONMENT:
missing_vars.append("PINECONE_ENVIRONMENT")
if not PINECONE_INDEX_NAME:
missing_vars.append("PINECONE_INDEX_NAME")
if not PINECONE_DIMENSION:
missing_vars.append("PINECONE_DIMENSION")
if not PINECONE_CLOUD:
missing_vars.append("PINECONE_CLOUD")
if missing_vars:
raise ValueError(
f"Required configuration missing: {', '.join(missing_vars)}"
)
def _initialize_index(self) -> None:
"""Initialize the Pinecone index."""
try:
# Check if index exists
if self.index_name not in self.client.list_indexes().names():
log.info(f"Creating Pinecone index '{self.index_name}'...")
self.client.create_index(
name=self.index_name,
dimension=self.dimension,
metric=self.metric,
spec=ServerlessSpec(cloud=self.cloud, region=self.environment),
)
log.info(f"Successfully created Pinecone index '{self.index_name}'")
else:
log.info(f"Using existing Pinecone index '{self.index_name}'")
# Connect to the index
self.index = self.client.Index(self.index_name)
except Exception as e:
log.error(f"Failed to initialize Pinecone index: {e}")
raise RuntimeError(f"Failed to initialize Pinecone index: {e}")
def _create_points(
self, items: List[VectorItem], collection_name_with_prefix: str
) -> List[Dict[str, Any]]:
"""Convert VectorItem objects to Pinecone point format."""
points = []
for item in items:
# Start with any existing metadata or an empty dict
metadata = item.get("metadata", {}).copy() if item.get("metadata") else {}
# Add text to metadata if available
if "text" in item:
metadata["text"] = item["text"]
# Always add collection_name to metadata for filtering
metadata["collection_name"] = collection_name_with_prefix
point = {
"id": item["id"],
"values": item["vector"],
"metadata": metadata,
}
points.append(point)
return points
def _get_collection_name_with_prefix(self, collection_name: str) -> str:
"""Get the collection name with prefix."""
return f"{self.collection_prefix}_{collection_name}"
def _normalize_distance(self, score: float) -> float:
"""Normalize distance score based on the metric used."""
if self.metric.lower() == "cosine":
# Cosine similarity ranges from -1 to 1, normalize to 0 to 1
return (score + 1.0) / 2.0
elif self.metric.lower() in ["euclidean", "dotproduct"]:
# These are already suitable for ranking (smaller is better for Euclidean)
return score
else:
# For other metrics, use as is
return score
def _result_to_get_result(self, matches: list) -> GetResult:
"""Convert Pinecone matches to GetResult format."""
ids = []
documents = []
metadatas = []
for match in matches:
metadata = match.get("metadata", {})
ids.append(match["id"])
documents.append(metadata.get("text", ""))
metadatas.append(metadata)
return GetResult(
**{
"ids": [ids],
"documents": [documents],
"metadatas": [metadatas],
}
)
def has_collection(self, collection_name: str) -> bool:
"""Check if a collection exists by searching for at least one item."""
collection_name_with_prefix = self._get_collection_name_with_prefix(
collection_name
)
try:
# Search for at least 1 item with this collection name in metadata
response = self.index.query(
vector=[0.0] * self.dimension, # dummy vector
top_k=1,
filter={"collection_name": collection_name_with_prefix},
include_metadata=False,
)
return len(response.matches) > 0
except Exception as e:
log.exception(
f"Error checking collection '{collection_name_with_prefix}': {e}"
)
return False
def delete_collection(self, collection_name: str) -> None:
"""Delete a collection by removing all vectors with the collection name in metadata."""
collection_name_with_prefix = self._get_collection_name_with_prefix(
collection_name
)
try:
self.index.delete(filter={"collection_name": collection_name_with_prefix})
log.info(
f"Collection '{collection_name_with_prefix}' deleted (all vectors removed)."
)
except Exception as e:
log.warning(
f"Failed to delete collection '{collection_name_with_prefix}': {e}"
)
raise
def insert(self, collection_name: str, items: List[VectorItem]) -> None:
"""Insert vectors into a collection."""
if not items:
log.warning("No items to insert")
return
collection_name_with_prefix = self._get_collection_name_with_prefix(
collection_name
)
points = self._create_points(items, collection_name_with_prefix)
# Insert in batches for better performance and reliability
for i in range(0, len(points), BATCH_SIZE):
batch = points[i : i + BATCH_SIZE]
try:
self.index.upsert(vectors=batch)
log.debug(
f"Inserted batch of {len(batch)} vectors into '{collection_name_with_prefix}'"
)
except Exception as e:
log.error(
f"Error inserting batch into '{collection_name_with_prefix}': {e}"
)
raise
log.info(
f"Successfully inserted {len(items)} vectors into '{collection_name_with_prefix}'"
)
def upsert(self, collection_name: str, items: List[VectorItem]) -> None:
"""Upsert (insert or update) vectors into a collection."""
if not items:
log.warning("No items to upsert")
return
collection_name_with_prefix = self._get_collection_name_with_prefix(
collection_name
)
points = self._create_points(items, collection_name_with_prefix)
# Upsert in batches
for i in range(0, len(points), BATCH_SIZE):
batch = points[i : i + BATCH_SIZE]
try:
self.index.upsert(vectors=batch)
log.debug(
f"Upserted batch of {len(batch)} vectors into '{collection_name_with_prefix}'"
)
except Exception as e:
log.error(
f"Error upserting batch into '{collection_name_with_prefix}': {e}"
)
raise
log.info(
f"Successfully upserted {len(items)} vectors into '{collection_name_with_prefix}'"
)
def search(
self, collection_name: str, vectors: List[List[Union[float, int]]], limit: int
) -> Optional[SearchResult]:
"""Search for similar vectors in a collection."""
if not vectors or not vectors[0]:
log.warning("No vectors provided for search")
return None
collection_name_with_prefix = self._get_collection_name_with_prefix(
collection_name
)
if limit is None or limit <= 0:
limit = NO_LIMIT
try:
# Search using the first vector (assuming this is the intended behavior)
query_vector = vectors[0]
# Perform the search
query_response = self.index.query(
vector=query_vector,
top_k=limit,
include_metadata=True,
filter={"collection_name": collection_name_with_prefix},
)
if not query_response.matches:
# Return empty result if no matches
return SearchResult(
ids=[[]],
documents=[[]],
metadatas=[[]],
distances=[[]],
)
# Convert to GetResult format
get_result = self._result_to_get_result(query_response.matches)
# Calculate normalized distances based on metric
distances = [
[
self._normalize_distance(match.score)
for match in query_response.matches
]
]
return SearchResult(
ids=get_result.ids,
documents=get_result.documents,
metadatas=get_result.metadatas,
distances=distances,
)
except Exception as e:
log.error(f"Error searching in '{collection_name_with_prefix}': {e}")
return None
def query(
self, collection_name: str, filter: Dict, limit: Optional[int] = None
) -> Optional[GetResult]:
"""Query vectors by metadata filter."""
collection_name_with_prefix = self._get_collection_name_with_prefix(
collection_name
)
if limit is None or limit <= 0:
limit = NO_LIMIT
try:
# Create a zero vector for the dimension as Pinecone requires a vector
zero_vector = [0.0] * self.dimension
# Combine user filter with collection_name
pinecone_filter = {"collection_name": collection_name_with_prefix}
if filter:
pinecone_filter.update(filter)
# Perform metadata-only query
query_response = self.index.query(
vector=zero_vector,
filter=pinecone_filter,
top_k=limit,
include_metadata=True,
)
return self._result_to_get_result(query_response.matches)
except Exception as e:
log.error(f"Error querying collection '{collection_name}': {e}")
return None
def get(self, collection_name: str) -> Optional[GetResult]:
"""Get all vectors in a collection."""
collection_name_with_prefix = self._get_collection_name_with_prefix(
collection_name
)
try:
# Use a zero vector for fetching all entries
zero_vector = [0.0] * self.dimension
# Add filter to only get vectors for this collection
query_response = self.index.query(
vector=zero_vector,
top_k=NO_LIMIT,
include_metadata=True,
filter={"collection_name": collection_name_with_prefix},
)
return self._result_to_get_result(query_response.matches)
except Exception as e:
log.error(f"Error getting collection '{collection_name}': {e}")
return None
def delete(
self,
collection_name: str,
ids: Optional[List[str]] = None,
filter: Optional[Dict] = None,
) -> None:
"""Delete vectors by IDs or filter."""
collection_name_with_prefix = self._get_collection_name_with_prefix(
collection_name
)
try:
if ids:
# Delete by IDs (in batches for large deletions)
for i in range(0, len(ids), BATCH_SIZE):
batch_ids = ids[i : i + BATCH_SIZE]
# Note: When deleting by ID, we can't filter by collection_name
# This is a limitation of Pinecone - be careful with ID uniqueness
self.index.delete(ids=batch_ids)
log.debug(
f"Deleted batch of {len(batch_ids)} vectors by ID from '{collection_name_with_prefix}'"
)
log.info(
f"Successfully deleted {len(ids)} vectors by ID from '{collection_name_with_prefix}'"
)
elif filter:
# Combine user filter with collection_name
pinecone_filter = {"collection_name": collection_name_with_prefix}
if filter:
pinecone_filter.update(filter)
# Delete by metadata filter
self.index.delete(filter=pinecone_filter)
log.info(
f"Successfully deleted vectors by filter from '{collection_name_with_prefix}'"
)
else:
log.warning("No ids or filter provided for delete operation")
except Exception as e:
log.error(f"Error deleting from collection '{collection_name}': {e}")
raise
def reset(self) -> None:
"""Reset the database by deleting all collections."""
try:
self.index.delete(delete_all=True)
log.info("All vectors successfully deleted from the index.")
except Exception as e:
log.error(f"Failed to reset Pinecone index: {e}")
raise

View File

@ -1,12 +1,24 @@
from typing import Optional
import logging
from urllib.parse import urlparse
from qdrant_client import QdrantClient as Qclient
from qdrant_client.http.models import PointStruct
from qdrant_client.models import models
from open_webui.retrieval.vector.main import VectorItem, SearchResult, GetResult
from open_webui.config import QDRANT_URI, QDRANT_API_KEY
from open_webui.retrieval.vector.main import (
VectorDBBase,
VectorItem,
SearchResult,
GetResult,
)
from open_webui.config import (
QDRANT_URI,
QDRANT_API_KEY,
QDRANT_ON_DISK,
QDRANT_GRPC_PORT,
QDRANT_PREFER_GRPC,
)
from open_webui.env import SRC_LOG_LEVELS
NO_LIMIT = 999999999
@ -15,16 +27,34 @@ log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
class QdrantClient:
class QdrantClient(VectorDBBase):
def __init__(self):
self.collection_prefix = "open-webui"
self.QDRANT_URI = QDRANT_URI
self.QDRANT_API_KEY = QDRANT_API_KEY
self.client = (
Qclient(url=self.QDRANT_URI, api_key=self.QDRANT_API_KEY)
if self.QDRANT_URI
else None
)
self.QDRANT_ON_DISK = QDRANT_ON_DISK
self.PREFER_GRPC = QDRANT_PREFER_GRPC
self.GRPC_PORT = QDRANT_GRPC_PORT
if not self.QDRANT_URI:
self.client = None
return
# Unified handling for either scheme
parsed = urlparse(self.QDRANT_URI)
host = parsed.hostname or self.QDRANT_URI
http_port = parsed.port or 6333 # default REST port
if self.PREFER_GRPC:
self.client = Qclient(
host=host,
port=http_port,
grpc_port=self.GRPC_PORT,
prefer_grpc=self.PREFER_GRPC,
api_key=self.QDRANT_API_KEY,
)
else:
self.client = Qclient(url=self.QDRANT_URI, api_key=self.QDRANT_API_KEY)
def _result_to_get_result(self, points) -> GetResult:
ids = []
@ -50,7 +80,9 @@ class QdrantClient:
self.client.create_collection(
collection_name=collection_name_with_prefix,
vectors_config=models.VectorParams(
size=dimension, distance=models.Distance.COSINE
size=dimension,
distance=models.Distance.COSINE,
on_disk=self.QDRANT_ON_DISK,
),
)

View File

@ -1,5 +1,6 @@
from pydantic import BaseModel
from typing import Optional, List, Any
from abc import ABC, abstractmethod
from typing import Any, Dict, List, Optional, Union
class VectorItem(BaseModel):
@ -17,3 +18,69 @@ class GetResult(BaseModel):
class SearchResult(GetResult):
distances: Optional[List[List[float | int]]]
class VectorDBBase(ABC):
"""
Abstract base class for all vector database backends.
Implementations of this class provide methods for collection management,
vector insertion, deletion, similarity search, and metadata filtering.
Any custom vector database integration must inherit from this class and
implement all abstract methods.
"""
@abstractmethod
def has_collection(self, collection_name: str) -> bool:
"""Check if the collection exists in the vector DB."""
pass
@abstractmethod
def delete_collection(self, collection_name: str) -> None:
"""Delete a collection from the vector DB."""
pass
@abstractmethod
def insert(self, collection_name: str, items: List[VectorItem]) -> None:
"""Insert a list of vector items into a collection."""
pass
@abstractmethod
def upsert(self, collection_name: str, items: List[VectorItem]) -> None:
"""Insert or update vector items in a collection."""
pass
@abstractmethod
def search(
self, collection_name: str, vectors: List[List[Union[float, int]]], limit: int
) -> Optional[SearchResult]:
"""Search for similar vectors in a collection."""
pass
@abstractmethod
def query(
self, collection_name: str, filter: Dict, limit: Optional[int] = None
) -> Optional[GetResult]:
"""Query vectors from a collection using metadata filter."""
pass
@abstractmethod
def get(self, collection_name: str) -> Optional[GetResult]:
"""Retrieve all vectors from a collection."""
pass
@abstractmethod
def delete(
self,
collection_name: str,
ids: Optional[List[str]] = None,
filter: Optional[Dict] = None,
) -> None:
"""Delete vectors by ID or filter from a collection."""
pass
@abstractmethod
def reset(self) -> None:
"""Reset the vector database by removing all collections or those matching a condition."""
pass

View File

@ -0,0 +1,47 @@
import logging
from typing import Optional, List
import requests
from open_webui.retrieval.web.main import SearchResult, get_filtered_results
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
def search_external(
external_url: str,
external_api_key: str,
query: str,
count: int,
filter_list: Optional[List[str]] = None,
) -> List[SearchResult]:
try:
response = requests.post(
external_url,
headers={
"User-Agent": "Open WebUI (https://github.com/open-webui/open-webui) RAG Bot",
"Authorization": f"Bearer {external_api_key}",
},
json={
"query": query,
"count": count,
},
)
response.raise_for_status()
results = response.json()
if filter_list:
results = get_filtered_results(results, filter_list)
results = [
SearchResult(
link=result.get("link"),
title=result.get("title"),
snippet=result.get("snippet"),
)
for result in results[:count]
]
log.info(f"External search results: {results}")
return results
except Exception as e:
log.error(f"Error in External search: {e}")
return []

View File

@ -0,0 +1,49 @@
import logging
from typing import Optional, List
from urllib.parse import urljoin
import requests
from open_webui.retrieval.web.main import SearchResult, get_filtered_results
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
def search_firecrawl(
firecrawl_url: str,
firecrawl_api_key: str,
query: str,
count: int,
filter_list: Optional[List[str]] = None,
) -> List[SearchResult]:
try:
firecrawl_search_url = urljoin(firecrawl_url, "/v1/search")
response = requests.post(
firecrawl_search_url,
headers={
"User-Agent": "Open WebUI (https://github.com/open-webui/open-webui) RAG Bot",
"Authorization": f"Bearer {firecrawl_api_key}",
},
json={
"query": query,
"limit": count,
},
)
response.raise_for_status()
results = response.json().get("data", [])
if filter_list:
results = get_filtered_results(results, filter_list)
results = [
SearchResult(
link=result.get("url"),
title=result.get("title"),
snippet=result.get("description"),
)
for result in results[:count]
]
log.info(f"External search results: {results}")
return results
except Exception as e:
log.error(f"Error in External search: {e}")
return []

View File

@ -2,7 +2,7 @@ import logging
from typing import Optional
import requests
from open_webui.retrieval.web.main import SearchResult
from open_webui.retrieval.web.main import SearchResult, get_filtered_results
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
@ -21,18 +21,25 @@ def search_tavily(
Args:
api_key (str): A Tavily Search API key
query (str): The query to search for
count (int): The maximum number of results to return
Returns:
list[SearchResult]: A list of search results
"""
url = "https://api.tavily.com/search"
data = {"query": query, "api_key": api_key}
response = requests.post(url, json=data)
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {api_key}",
}
data = {"query": query, "max_results": count}
response = requests.post(url, headers=headers, json=data)
response.raise_for_status()
json_response = response.json()
raw_search_results = json_response.get("results", [])
results = json_response.get("results", [])
if filter_list:
results = get_filtered_results(results, filter_list)
return [
SearchResult(
@ -40,5 +47,5 @@ def search_tavily(
title=result.get("title", ""),
snippet=result.get("content"),
)
for result in raw_search_results[:count]
for result in results
]

View File

@ -25,6 +25,7 @@ from langchain_community.document_loaders.firecrawl import FireCrawlLoader
from langchain_community.document_loaders.base import BaseLoader
from langchain_core.documents import Document
from open_webui.retrieval.loaders.tavily import TavilyLoader
from open_webui.retrieval.loaders.external import ExternalLoader
from open_webui.constants import ERROR_MESSAGES
from open_webui.config import (
ENABLE_RAG_LOCAL_WEB_FETCH,
@ -35,6 +36,8 @@ from open_webui.config import (
FIRECRAWL_API_KEY,
TAVILY_API_KEY,
TAVILY_EXTRACT_DEPTH,
EXTERNAL_WEB_LOADER_URL,
EXTERNAL_WEB_LOADER_API_KEY,
)
from open_webui.env import SRC_LOG_LEVELS
@ -167,7 +170,7 @@ class SafeFireCrawlLoader(BaseLoader, RateLimitMixin, URLProcessingMixin):
continue_on_failure: bool = True,
api_key: Optional[str] = None,
api_url: Optional[str] = None,
mode: Literal["crawl", "scrape", "map"] = "crawl",
mode: Literal["crawl", "scrape", "map"] = "scrape",
proxy: Optional[Dict[str, str]] = None,
params: Optional[Dict] = None,
):
@ -225,7 +228,10 @@ class SafeFireCrawlLoader(BaseLoader, RateLimitMixin, URLProcessingMixin):
mode=self.mode,
params=self.params,
)
yield from loader.lazy_load()
for document in loader.lazy_load():
if not document.metadata.get("source"):
document.metadata["source"] = document.metadata.get("sourceURL")
yield document
except Exception as e:
if self.continue_on_failure:
log.exception(f"Error loading {url}: {e}")
@ -245,6 +251,8 @@ class SafeFireCrawlLoader(BaseLoader, RateLimitMixin, URLProcessingMixin):
params=self.params,
)
async for document in loader.alazy_load():
if not document.metadata.get("source"):
document.metadata["source"] = document.metadata.get("sourceURL")
yield document
except Exception as e:
if self.continue_on_failure:
@ -619,6 +627,11 @@ def get_web_loader(
web_loader_args["api_key"] = TAVILY_API_KEY.value
web_loader_args["extract_depth"] = TAVILY_EXTRACT_DEPTH.value
if WEB_LOADER_ENGINE.value == "external":
WebLoaderClass = ExternalLoader
web_loader_args["external_url"] = EXTERNAL_WEB_LOADER_URL.value
web_loader_args["external_api_key"] = EXTERNAL_WEB_LOADER_API_KEY.value
if WebLoaderClass:
web_loader = WebLoaderClass(**web_loader_args)

View File

@ -0,0 +1,85 @@
import logging
from typing import Optional
import requests
from requests.auth import HTTPDigestAuth
from open_webui.retrieval.web.main import SearchResult, get_filtered_results
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
def search_yacy(
query_url: str,
username: Optional[str],
password: Optional[str],
query: str,
count: int,
filter_list: Optional[list[str]] = None,
) -> list[SearchResult]:
"""
Search a Yacy instance for a given query and return the results as a list of SearchResult objects.
The function accepts username and password for authenticating to Yacy.
Args:
query_url (str): The base URL of the Yacy server.
username (str): Optional YaCy username.
password (str): Optional YaCy password.
query (str): The search term or question to find in the Yacy database.
count (int): The maximum number of results to retrieve from the search.
Returns:
list[SearchResult]: A list of SearchResults sorted by relevance score in descending order.
Raise:
requests.exceptions.RequestException: If a request error occurs during the search process.
"""
# Use authentication if either username or password is set
yacy_auth = None
if username or password:
yacy_auth = HTTPDigestAuth(username, password)
params = {
"query": query,
"contentdom": "text",
"resource": "global",
"maximumRecords": count,
"nav": "none",
}
# Check if provided a json API URL
if not query_url.endswith("yacysearch.json"):
# Strip all query parameters from the URL
query_url = query_url.rstrip('/') + "/yacysearch.json"
log.debug(f"searching {query_url}")
response = requests.get(
query_url,
auth=yacy_auth,
headers={
"User-Agent": "Open WebUI (https://github.com/open-webui/open-webui) RAG Bot",
"Accept": "text/html",
"Accept-Encoding": "gzip, deflate",
"Accept-Language": "en-US,en;q=0.5",
"Connection": "keep-alive",
},
params=params,
)
response.raise_for_status() # Raise an exception for HTTP errors.
json_response = response.json()
results = json_response.get("channels", [{}])[0].get("items", [])
sorted_results = sorted(results, key=lambda x: x.get("ranking", 0), reverse=True)
if filter_list:
sorted_results = get_filtered_results(sorted_results, filter_list)
return [
SearchResult(
link=result["link"], title=result.get("title"), snippet=result.get("description")
)
for result in sorted_results[:count]
]

View File

@ -150,7 +150,8 @@ class STTConfigForm(BaseModel):
AZURE_API_KEY: str
AZURE_REGION: str
AZURE_LOCALES: str
AZURE_BASE_URL: str
AZURE_MAX_SPEAKERS: str
class AudioConfigUpdateForm(BaseModel):
tts: TTSConfigForm
@ -181,6 +182,8 @@ async def get_audio_config(request: Request, user=Depends(get_admin_user)):
"AZURE_API_KEY": request.app.state.config.AUDIO_STT_AZURE_API_KEY,
"AZURE_REGION": request.app.state.config.AUDIO_STT_AZURE_REGION,
"AZURE_LOCALES": request.app.state.config.AUDIO_STT_AZURE_LOCALES,
"AZURE_BASE_URL": request.app.state.config.AUDIO_STT_AZURE_BASE_URL,
"AZURE_MAX_SPEAKERS": request.app.state.config.AUDIO_STT_AZURE_MAX_SPEAKERS,
},
}
@ -210,6 +213,8 @@ async def update_audio_config(
request.app.state.config.AUDIO_STT_AZURE_API_KEY = form_data.stt.AZURE_API_KEY
request.app.state.config.AUDIO_STT_AZURE_REGION = form_data.stt.AZURE_REGION
request.app.state.config.AUDIO_STT_AZURE_LOCALES = form_data.stt.AZURE_LOCALES
request.app.state.config.AUDIO_STT_AZURE_BASE_URL = form_data.stt.AZURE_BASE_URL
request.app.state.config.AUDIO_STT_AZURE_MAX_SPEAKERS = form_data.stt.AZURE_MAX_SPEAKERS
if request.app.state.config.STT_ENGINE == "":
request.app.state.faster_whisper_model = set_faster_whisper_model(
@ -238,6 +243,8 @@ async def update_audio_config(
"AZURE_API_KEY": request.app.state.config.AUDIO_STT_AZURE_API_KEY,
"AZURE_REGION": request.app.state.config.AUDIO_STT_AZURE_REGION,
"AZURE_LOCALES": request.app.state.config.AUDIO_STT_AZURE_LOCALES,
"AZURE_BASE_URL": request.app.state.config.AUDIO_STT_AZURE_BASE_URL,
"AZURE_MAX_SPEAKERS": request.app.state.config.AUDIO_STT_AZURE_MAX_SPEAKERS,
},
}
@ -641,6 +648,8 @@ def transcribe(request: Request, file_path):
api_key = request.app.state.config.AUDIO_STT_AZURE_API_KEY
region = request.app.state.config.AUDIO_STT_AZURE_REGION
locales = request.app.state.config.AUDIO_STT_AZURE_LOCALES
base_url = request.app.state.config.AUDIO_STT_AZURE_BASE_URL
max_speakers = request.app.state.config.AUDIO_STT_AZURE_MAX_SPEAKERS
# IF NO LOCALES, USE DEFAULTS
if len(locales) < 2:
@ -664,7 +673,13 @@ def transcribe(request: Request, file_path):
if not api_key or not region:
raise HTTPException(
status_code=400,
detail="Azure API key and region are required for Azure STT",
detail="Azure API key is required for Azure STT",
)
if not base_url and not region:
raise HTTPException(
status_code=400,
detail="Azure region or base url is required for Azure STT",
)
r = None
@ -674,13 +689,14 @@ def transcribe(request: Request, file_path):
"definition": json.dumps(
{
"locales": locales.split(","),
"diarization": {"maxSpeakers": 3, "enabled": True},
"diarization": {"maxSpeakers": max_speakers, "enabled": True},
}
if locales
else {}
)
}
url = f"https://{region}.api.cognitive.microsoft.com/speechtotext/transcriptions:transcribe?api-version=2024-11-15"
url = base_url or f"https://{region}.api.cognitive.microsoft.com/speechtotext/transcriptions:transcribe?api-version=2024-11-15"
# Use context manager to ensure file is properly closed
with open(file_path, "rb") as audio_file:

View File

@ -27,20 +27,24 @@ from open_webui.env import (
WEBUI_AUTH_TRUSTED_NAME_HEADER,
WEBUI_AUTH_COOKIE_SAME_SITE,
WEBUI_AUTH_COOKIE_SECURE,
WEBUI_AUTH_SIGNOUT_REDIRECT_URL,
SRC_LOG_LEVELS,
)
from fastapi import APIRouter, Depends, HTTPException, Request, status
from fastapi.responses import RedirectResponse, Response
from open_webui.config import OPENID_PROVIDER_URL, ENABLE_OAUTH_SIGNUP, ENABLE_LDAP
from pydantic import BaseModel
from open_webui.utils.misc import parse_duration, validate_email_format
from open_webui.utils.auth import (
decode_token,
create_api_key,
create_token,
get_admin_user,
get_verified_user,
get_current_user,
get_password_hash,
get_http_authorization_cred,
)
from open_webui.utils.webhook import post_webhook
from open_webui.utils.access_control import get_permissions
@ -72,27 +76,29 @@ class SessionUserResponse(Token, UserResponse):
async def get_session_user(
request: Request, response: Response, user=Depends(get_current_user)
):
expires_delta = parse_duration(request.app.state.config.JWT_EXPIRES_IN)
expires_at = None
if expires_delta:
expires_at = int(time.time()) + int(expires_delta.total_seconds())
token = create_token(
data={"id": user.id},
expires_delta=expires_delta,
)
auth_header = request.headers.get("Authorization")
auth_token = get_http_authorization_cred(auth_header)
token = auth_token.credentials
data = decode_token(token)
datetime_expires_at = (
datetime.datetime.fromtimestamp(expires_at, datetime.timezone.utc)
if expires_at
else None
)
expires_at = data.get("exp")
if (expires_at is not None) and int(time.time()) > expires_at:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.INVALID_TOKEN,
)
# Set the cookie token
response.set_cookie(
key="token",
value=token,
expires=datetime_expires_at,
expires=(
datetime.datetime.fromtimestamp(expires_at, datetime.timezone.utc)
if expires_at
else None
),
httponly=True, # Ensures the cookie is not accessible via JavaScript
samesite=WEBUI_AUTH_COOKIE_SAME_SITE,
secure=WEBUI_AUTH_COOKIE_SECURE,
@ -288,18 +294,30 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
user = Auths.authenticate_user_by_trusted_header(email)
if user:
expires_delta = parse_duration(request.app.state.config.JWT_EXPIRES_IN)
expires_at = None
if expires_delta:
expires_at = int(time.time()) + int(expires_delta.total_seconds())
token = create_token(
data={"id": user.id},
expires_delta=parse_duration(
request.app.state.config.JWT_EXPIRES_IN
),
expires_delta=expires_delta,
)
# Set the cookie token
response.set_cookie(
key="token",
value=token,
expires=(
datetime.datetime.fromtimestamp(
expires_at, datetime.timezone.utc
)
if expires_at
else None
),
httponly=True, # Ensures the cookie is not accessible via JavaScript
samesite=WEBUI_AUTH_COOKIE_SAME_SITE,
secure=WEBUI_AUTH_COOKIE_SECURE,
)
user_permissions = get_permissions(
@ -309,6 +327,7 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
return {
"token": token,
"token_type": "Bearer",
"expires_at": expires_at,
"id": user.id,
"email": user.email,
"name": user.name,
@ -566,6 +585,12 @@ async def signout(request: Request, response: Response):
detail="Failed to sign out from the OpenID provider.",
)
if WEBUI_AUTH_SIGNOUT_REDIRECT_URL:
return RedirectResponse(
headers=response.headers,
url=WEBUI_AUTH_SIGNOUT_REDIRECT_URL,
)
return {"status": True}
@ -664,6 +689,7 @@ async def get_admin_config(request: Request, user=Depends(get_admin_user)):
"ENABLE_COMMUNITY_SHARING": request.app.state.config.ENABLE_COMMUNITY_SHARING,
"ENABLE_MESSAGE_RATING": request.app.state.config.ENABLE_MESSAGE_RATING,
"ENABLE_CHANNELS": request.app.state.config.ENABLE_CHANNELS,
"ENABLE_NOTES": request.app.state.config.ENABLE_NOTES,
"ENABLE_USER_WEBHOOKS": request.app.state.config.ENABLE_USER_WEBHOOKS,
}
@ -680,6 +706,7 @@ class AdminConfig(BaseModel):
ENABLE_COMMUNITY_SHARING: bool
ENABLE_MESSAGE_RATING: bool
ENABLE_CHANNELS: bool
ENABLE_NOTES: bool
ENABLE_USER_WEBHOOKS: bool
@ -700,6 +727,7 @@ async def update_admin_config(
)
request.app.state.config.ENABLE_CHANNELS = form_data.ENABLE_CHANNELS
request.app.state.config.ENABLE_NOTES = form_data.ENABLE_NOTES
if form_data.DEFAULT_USER_ROLE in ["pending", "user", "admin"]:
request.app.state.config.DEFAULT_USER_ROLE = form_data.DEFAULT_USER_ROLE
@ -724,11 +752,12 @@ async def update_admin_config(
"ENABLE_API_KEY": request.app.state.config.ENABLE_API_KEY,
"ENABLE_API_KEY_ENDPOINT_RESTRICTIONS": request.app.state.config.ENABLE_API_KEY_ENDPOINT_RESTRICTIONS,
"API_KEY_ALLOWED_ENDPOINTS": request.app.state.config.API_KEY_ALLOWED_ENDPOINTS,
"ENABLE_CHANNELS": request.app.state.config.ENABLE_CHANNELS,
"DEFAULT_USER_ROLE": request.app.state.config.DEFAULT_USER_ROLE,
"JWT_EXPIRES_IN": request.app.state.config.JWT_EXPIRES_IN,
"ENABLE_COMMUNITY_SHARING": request.app.state.config.ENABLE_COMMUNITY_SHARING,
"ENABLE_MESSAGE_RATING": request.app.state.config.ENABLE_MESSAGE_RATING,
"ENABLE_CHANNELS": request.app.state.config.ENABLE_CHANNELS,
"ENABLE_NOTES": request.app.state.config.ENABLE_NOTES,
"ENABLE_USER_WEBHOOKS": request.app.state.config.ENABLE_USER_WEBHOOKS,
}

View File

@ -638,8 +638,17 @@ async def archive_chat_by_id(id: str, user=Depends(get_verified_user)):
@router.post("/{id}/share", response_model=Optional[ChatResponse])
async def share_chat_by_id(id: str, user=Depends(get_verified_user)):
async def share_chat_by_id(request: Request, id: str, user=Depends(get_verified_user)):
if not has_permission(
user.id, "chat.share", request.app.state.config.USER_PERMISSIONS
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
chat = Chats.get_chat_by_id_and_user_id(id, user.id)
if chat:
if chat.share_id:
shared_chat = Chats.update_shared_chat_by_chat_id(chat.id)

View File

@ -19,6 +19,8 @@ from fastapi import (
from fastapi.responses import FileResponse, StreamingResponse
from open_webui.constants import ERROR_MESSAGES
from open_webui.env import SRC_LOG_LEVELS
from open_webui.models.users import Users
from open_webui.models.files import (
FileForm,
FileModel,
@ -83,10 +85,12 @@ def upload_file(
request: Request,
file: UploadFile = File(...),
user=Depends(get_verified_user),
file_metadata: dict = {},
file_metadata: dict = None,
process: bool = Query(True),
):
log.info(f"file.content_type: {file.content_type}")
file_metadata = file_metadata if file_metadata else {}
try:
unsanitized_filename = file.filename
filename = os.path.basename(unsanitized_filename)
@ -95,7 +99,13 @@ def upload_file(
id = str(uuid.uuid4())
name = filename
filename = f"{id}_{filename}"
contents, file_path = Storage.upload_file(file.file, filename)
tags = {
"OpenWebUI-User-Email": user.email,
"OpenWebUI-User-Id": user.id,
"OpenWebUI-User-Name": user.name,
"OpenWebUI-File-Id": id,
}
contents, file_path = Storage.upload_file(file.file, filename, tags)
file_item = Files.insert_new_file(
user.id,
@ -129,7 +139,15 @@ def upload_file(
ProcessFileForm(file_id=id, content=result.get("text", "")),
user=user,
)
elif file.content_type not in ["image/png", "image/jpeg", "image/gif"]:
elif file.content_type not in [
"image/png",
"image/jpeg",
"image/gif",
"video/mp4",
"video/ogg",
"video/quicktime",
"video/webm",
]:
process_file(request, ProcessFileForm(file_id=id), user=user)
file_item = Files.get_file_by_id(id=id)
@ -173,7 +191,8 @@ async def list_files(user=Depends(get_verified_user), content: bool = Query(True
if not content:
for file in files:
del file.data["content"]
if "content" in file.data:
del file.data["content"]
return files
@ -214,7 +233,8 @@ async def search_files(
if not content:
for file in matching_files:
del file.data["content"]
if "content" in file.data:
del file.data["content"]
return matching_files
@ -431,6 +451,13 @@ async def get_html_file_content_by_id(id: str, user=Depends(get_verified_user)):
detail=ERROR_MESSAGES.NOT_FOUND,
)
file_user = Users.get_user_by_id(file.user_id)
if not file_user.role == "admin":
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
)
if (
file.user_id == user.id
or user.role == "admin"

View File

@ -500,7 +500,11 @@ async def image_generations(
if form_data.size
else request.app.state.config.IMAGE_SIZE
),
"response_format": "b64_json",
**(
{"response_format": "b64_json"}
if "gpt-image-1" in request.app.state.config.IMAGE_GENERATION_MODEL
else {}
),
}
# Use asyncio.to_thread for the requests.post call

View File

@ -9,7 +9,7 @@ from open_webui.models.knowledge import (
KnowledgeResponse,
KnowledgeUserResponse,
)
from open_webui.models.files import Files, FileModel
from open_webui.models.files import Files, FileModel, FileMetadataResponse
from open_webui.retrieval.vector.connector import VECTOR_DB_CLIENT
from open_webui.routers.retrieval import (
process_file,
@ -178,10 +178,26 @@ async def reindex_knowledge_files(request: Request, user=Depends(get_verified_us
log.info(f"Starting reindexing for {len(knowledge_bases)} knowledge bases")
for knowledge_base in knowledge_bases:
try:
files = Files.get_files_by_ids(knowledge_base.data.get("file_ids", []))
deleted_knowledge_bases = []
for knowledge_base in knowledge_bases:
# -- Robust error handling for missing or invalid data
if not knowledge_base.data or not isinstance(knowledge_base.data, dict):
log.warning(
f"Knowledge base {knowledge_base.id} has no data or invalid data ({knowledge_base.data!r}). Deleting."
)
try:
Knowledges.delete_knowledge_by_id(id=knowledge_base.id)
deleted_knowledge_bases.append(knowledge_base.id)
except Exception as e:
log.error(
f"Failed to delete invalid knowledge base {knowledge_base.id}: {e}"
)
continue
try:
file_ids = knowledge_base.data.get("file_ids", [])
files = Files.get_files_by_ids(file_ids)
try:
if VECTOR_DB_CLIENT.has_collection(collection_name=knowledge_base.id):
VECTOR_DB_CLIENT.delete_collection(
@ -189,10 +205,7 @@ async def reindex_knowledge_files(request: Request, user=Depends(get_verified_us
)
except Exception as e:
log.error(f"Error deleting collection {knowledge_base.id}: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error deleting vector DB collection",
)
continue # Skip, don't raise
failed_files = []
for file in files:
@ -213,10 +226,8 @@ async def reindex_knowledge_files(request: Request, user=Depends(get_verified_us
except Exception as e:
log.error(f"Error processing knowledge base {knowledge_base.id}: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error processing knowledge base",
)
# Don't raise, just continue
continue
if failed_files:
log.warning(
@ -225,7 +236,9 @@ async def reindex_knowledge_files(request: Request, user=Depends(get_verified_us
for failed in failed_files:
log.warning(f"File ID: {failed['file_id']}, Error: {failed['error']}")
log.info("Reindexing completed successfully")
log.info(
f"Reindexing completed. Deleted {len(deleted_knowledge_bases)} invalid knowledge bases: {deleted_knowledge_bases}"
)
return True
@ -235,7 +248,7 @@ async def reindex_knowledge_files(request: Request, user=Depends(get_verified_us
class KnowledgeFilesResponse(KnowledgeResponse):
files: list[FileModel]
files: list[FileMetadataResponse]
@router.get("/{id}", response_model=Optional[KnowledgeFilesResponse])
@ -251,7 +264,7 @@ async def get_knowledge_by_id(id: str, user=Depends(get_verified_user)):
):
file_ids = knowledge.data.get("file_ids", []) if knowledge.data else []
files = Files.get_files_by_ids(file_ids)
files = Files.get_file_metadatas_by_ids(file_ids)
return KnowledgeFilesResponse(
**knowledge.model_dump(),
@ -379,7 +392,7 @@ def add_file_to_knowledge_by_id(
knowledge = Knowledges.update_knowledge_data_by_id(id=id, data=data)
if knowledge:
files = Files.get_files_by_ids(file_ids)
files = Files.get_file_metadatas_by_ids(file_ids)
return KnowledgeFilesResponse(
**knowledge.model_dump(),
@ -456,7 +469,7 @@ def update_file_from_knowledge_by_id(
data = knowledge.data or {}
file_ids = data.get("file_ids", [])
files = Files.get_files_by_ids(file_ids)
files = Files.get_file_metadatas_by_ids(file_ids)
return KnowledgeFilesResponse(
**knowledge.model_dump(),
@ -538,7 +551,7 @@ def remove_file_from_knowledge_by_id(
knowledge = Knowledges.update_knowledge_data_by_id(id=id, data=data)
if knowledge:
files = Files.get_files_by_ids(file_ids)
files = Files.get_file_metadatas_by_ids(file_ids)
return KnowledgeFilesResponse(
**knowledge.model_dump(),
@ -734,7 +747,7 @@ def add_files_to_knowledge_batch(
error_details = [f"{err.file_id}: {err.error}" for err in result.errors]
return KnowledgeFilesResponse(
**knowledge.model_dump(),
files=Files.get_files_by_ids(existing_file_ids),
files=Files.get_file_metadatas_by_ids(existing_file_ids),
warnings={
"message": "Some files failed to process",
"errors": error_details,
@ -742,5 +755,6 @@ def add_files_to_knowledge_batch(
)
return KnowledgeFilesResponse(
**knowledge.model_dump(), files=Files.get_files_by_ids(existing_file_ids)
**knowledge.model_dump(),
files=Files.get_file_metadatas_by_ids(existing_file_ids),
)

View File

@ -54,6 +54,7 @@ from open_webui.config import (
from open_webui.env import (
ENV,
SRC_LOG_LEVELS,
AIOHTTP_CLIENT_SESSION_SSL,
AIOHTTP_CLIENT_TIMEOUT,
AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST,
BYPASS_MODEL_ACCESS_CONTROL,
@ -91,6 +92,7 @@ async def send_get_request(url, key=None, user: UserModel = None):
else {}
),
},
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as response:
return await response.json()
except Exception as e:
@ -141,6 +143,7 @@ async def send_post_request(
else {}
),
},
ssl=AIOHTTP_CLIENT_SESSION_SSL,
)
r.raise_for_status()
@ -216,7 +219,8 @@ async def verify_connection(
key = form_data.key
async with aiohttp.ClientSession(
timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST)
trust_env=True,
timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST),
) as session:
try:
async with session.get(
@ -234,6 +238,7 @@ async def verify_connection(
else {}
),
},
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as r:
if r.status != 200:
detail = f"HTTP Error: {r.status}"
@ -1006,7 +1011,7 @@ class GenerateCompletionForm(BaseModel):
prompt: str
suffix: Optional[str] = None
images: Optional[list[str]] = None
format: Optional[str] = None
format: Optional[Union[dict, str]] = None
options: Optional[dict] = None
system: Optional[str] = None
template: Optional[str] = None
@ -1482,7 +1487,9 @@ async def download_file_stream(
timeout = aiohttp.ClientTimeout(total=600) # Set the timeout
async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
async with session.get(file_url, headers=headers) as response:
async with session.get(
file_url, headers=headers, ssl=AIOHTTP_CLIENT_SESSION_SSL
) as response:
total_size = int(response.headers.get("content-length", 0)) + current_size
with open(file_path, "ab+") as file:
@ -1497,7 +1504,8 @@ async def download_file_stream(
if done:
file.seek(0)
hashed = calculate_sha256(file)
chunk_size = 1024 * 1024 * 2
hashed = calculate_sha256(file, chunk_size)
file.seek(0)
url = f"{ollama_url}/api/blobs/sha256:{hashed}"

View File

@ -21,6 +21,7 @@ from open_webui.config import (
CACHE_DIR,
)
from open_webui.env import (
AIOHTTP_CLIENT_SESSION_SSL,
AIOHTTP_CLIENT_TIMEOUT,
AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST,
ENABLE_FORWARD_USER_INFO_HEADERS,
@ -74,6 +75,7 @@ async def send_get_request(url, key=None, user: UserModel = None):
else {}
),
},
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as response:
return await response.json()
except Exception as e:
@ -92,20 +94,19 @@ async def cleanup_response(
await session.close()
def openai_o1_o3_handler(payload):
def openai_o_series_handler(payload):
"""
Handle o1, o3 specific parameters
Handle "o" series specific parameters
"""
if "max_tokens" in payload:
# Remove "max_tokens" from the payload
# Convert "max_tokens" to "max_completion_tokens" for all o-series models
payload["max_completion_tokens"] = payload["max_tokens"]
del payload["max_tokens"]
# Fix: o1 and o3 do not support the "system" role directly.
# For older models like "o1-mini" or "o1-preview", use role "user".
# For newer o1/o3 models, replace "system" with "developer".
# Handle system role conversion based on model type
if payload["messages"][0]["role"] == "system":
model_lower = payload["model"].lower()
# Legacy models use "user" role instead of "system"
if model_lower.startswith("o1-mini") or model_lower.startswith("o1-preview"):
payload["messages"][0]["role"] = "user"
else:
@ -462,7 +463,8 @@ async def get_models(
r = None
async with aiohttp.ClientSession(
timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST)
trust_env=True,
timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST),
) as session:
try:
async with session.get(
@ -481,6 +483,7 @@ async def get_models(
else {}
),
},
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as r:
if r.status != 200:
# Extract response error details if available
@ -542,7 +545,8 @@ async def verify_connection(
key = form_data.key
async with aiohttp.ClientSession(
timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST)
trust_env=True,
timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST),
) as session:
try:
async with session.get(
@ -561,6 +565,7 @@ async def verify_connection(
else {}
),
},
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as r:
if r.status != 200:
# Extract response error details if available
@ -666,10 +671,10 @@ async def generate_chat_completion(
url = request.app.state.config.OPENAI_API_BASE_URLS[idx]
key = request.app.state.config.OPENAI_API_KEYS[idx]
# Fix: o1,o3 does not support the "max_tokens" parameter, Modify "max_tokens" to "max_completion_tokens"
is_o1_o3 = payload["model"].lower().startswith(("o1", "o3-"))
if is_o1_o3:
payload = openai_o1_o3_handler(payload)
# Check if model is from "o" series
is_o_series = payload["model"].lower().startswith(("o1", "o3", "o4"))
if is_o_series:
payload = openai_o_series_handler(payload)
elif "api.openai.com" not in url:
# Remove "max_completion_tokens" from the payload for backward compatibility
if "max_completion_tokens" in payload:
@ -723,6 +728,7 @@ async def generate_chat_completion(
else {}
),
},
ssl=AIOHTTP_CLIENT_SESSION_SSL,
)
# Check if response is SSE
@ -802,6 +808,7 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
else {}
),
},
ssl=AIOHTTP_CLIENT_SESSION_SSL,
)
r.raise_for_status()

View File

@ -66,7 +66,7 @@ async def process_pipeline_inlet_filter(request, payload, user, models):
if "pipeline" in model:
sorted_filters.append(model)
async with aiohttp.ClientSession() as session:
async with aiohttp.ClientSession(trust_env=True) as session:
for filter in sorted_filters:
urlIdx = filter.get("urlIdx")
if urlIdx is None:
@ -115,7 +115,7 @@ async def process_pipeline_outlet_filter(request, payload, user, models):
if "pipeline" in model:
sorted_filters = [model] + sorted_filters
async with aiohttp.ClientSession() as session:
async with aiohttp.ClientSession(trust_env=True) as session:
for filter in sorted_filters:
urlIdx = filter.get("urlIdx")
if urlIdx is None:

View File

@ -53,6 +53,7 @@ from open_webui.retrieval.web.jina_search import search_jina
from open_webui.retrieval.web.searchapi import search_searchapi
from open_webui.retrieval.web.serpapi import search_serpapi
from open_webui.retrieval.web.searxng import search_searxng
from open_webui.retrieval.web.yacy import search_yacy
from open_webui.retrieval.web.serper import search_serper
from open_webui.retrieval.web.serply import search_serply
from open_webui.retrieval.web.serpstack import search_serpstack
@ -61,6 +62,8 @@ from open_webui.retrieval.web.bing import search_bing
from open_webui.retrieval.web.exa import search_exa
from open_webui.retrieval.web.perplexity import search_perplexity
from open_webui.retrieval.web.sougou import search_sougou
from open_webui.retrieval.web.firecrawl import search_firecrawl
from open_webui.retrieval.web.external import search_external
from open_webui.retrieval.utils import (
get_embedding_function,
@ -90,7 +93,12 @@ from open_webui.env import (
SRC_LOG_LEVELS,
DEVICE_TYPE,
DOCKER,
SENTENCE_TRANSFORMERS_BACKEND,
SENTENCE_TRANSFORMERS_MODEL_KWARGS,
SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND,
SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS,
)
from open_webui.constants import ERROR_MESSAGES
log = logging.getLogger(__name__)
@ -117,6 +125,8 @@ def get_ef(
get_model_path(embedding_model, auto_update),
device=DEVICE_TYPE,
trust_remote_code=RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE,
backend=SENTENCE_TRANSFORMERS_BACKEND,
model_kwargs=SENTENCE_TRANSFORMERS_MODEL_KWARGS,
)
except Exception as e:
log.debug(f"Error loading SentenceTransformer: {e}")
@ -150,6 +160,8 @@ def get_rf(
get_model_path(reranking_model, auto_update),
device=DEVICE_TYPE,
trust_remote_code=RAG_RERANKING_MODEL_TRUST_REMOTE_CODE,
backend=SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND,
model_kwargs=SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS,
)
except Exception as e:
log.error(f"CrossEncoder: {e}")
@ -389,6 +401,9 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)):
"WEB_SEARCH_DOMAIN_FILTER_LIST": request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST,
"BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL": request.app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL,
"SEARXNG_QUERY_URL": request.app.state.config.SEARXNG_QUERY_URL,
"YACY_QUERY_URL": request.app.state.config.YACY_QUERY_URL,
"YACY_USERNAME": request.app.state.config.YACY_USERNAME,
"YACY_PASSWORD": request.app.state.config.YACY_PASSWORD,
"GOOGLE_PSE_API_KEY": request.app.state.config.GOOGLE_PSE_API_KEY,
"GOOGLE_PSE_ENGINE_ID": request.app.state.config.GOOGLE_PSE_ENGINE_ID,
"BRAVE_SEARCH_API_KEY": request.app.state.config.BRAVE_SEARCH_API_KEY,
@ -418,6 +433,10 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)):
"FIRECRAWL_API_KEY": request.app.state.config.FIRECRAWL_API_KEY,
"FIRECRAWL_API_BASE_URL": request.app.state.config.FIRECRAWL_API_BASE_URL,
"TAVILY_EXTRACT_DEPTH": request.app.state.config.TAVILY_EXTRACT_DEPTH,
"EXTERNAL_WEB_SEARCH_URL": request.app.state.config.EXTERNAL_WEB_SEARCH_URL,
"EXTERNAL_WEB_SEARCH_API_KEY": request.app.state.config.EXTERNAL_WEB_SEARCH_API_KEY,
"EXTERNAL_WEB_LOADER_URL": request.app.state.config.EXTERNAL_WEB_LOADER_URL,
"EXTERNAL_WEB_LOADER_API_KEY": request.app.state.config.EXTERNAL_WEB_LOADER_API_KEY,
"YOUTUBE_LOADER_LANGUAGE": request.app.state.config.YOUTUBE_LOADER_LANGUAGE,
"YOUTUBE_LOADER_PROXY_URL": request.app.state.config.YOUTUBE_LOADER_PROXY_URL,
"YOUTUBE_LOADER_TRANSLATION": request.app.state.YOUTUBE_LOADER_TRANSLATION,
@ -434,6 +453,9 @@ class WebConfig(BaseModel):
WEB_SEARCH_DOMAIN_FILTER_LIST: Optional[List[str]] = []
BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL: Optional[bool] = None
SEARXNG_QUERY_URL: Optional[str] = None
YACY_QUERY_URL: Optional[str] = None
YACY_USERNAME: Optional[str] = None
YACY_PASSWORD: Optional[str] = None
GOOGLE_PSE_API_KEY: Optional[str] = None
GOOGLE_PSE_ENGINE_ID: Optional[str] = None
BRAVE_SEARCH_API_KEY: Optional[str] = None
@ -463,6 +485,10 @@ class WebConfig(BaseModel):
FIRECRAWL_API_KEY: Optional[str] = None
FIRECRAWL_API_BASE_URL: Optional[str] = None
TAVILY_EXTRACT_DEPTH: Optional[str] = None
EXTERNAL_WEB_SEARCH_URL: Optional[str] = None
EXTERNAL_WEB_SEARCH_API_KEY: Optional[str] = None
EXTERNAL_WEB_LOADER_URL: Optional[str] = None
EXTERNAL_WEB_LOADER_API_KEY: Optional[str] = None
YOUTUBE_LOADER_LANGUAGE: Optional[List[str]] = None
YOUTUBE_LOADER_PROXY_URL: Optional[str] = None
YOUTUBE_LOADER_TRANSLATION: Optional[str] = None
@ -651,6 +677,9 @@ async def update_rag_config(
form_data.web.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL
)
request.app.state.config.SEARXNG_QUERY_URL = form_data.web.SEARXNG_QUERY_URL
request.app.state.config.YACY_QUERY_URL = form_data.web.YACY_QUERY_URL
request.app.state.config.YACY_USERNAME = form_data.web.YACY_USERNAME
request.app.state.config.YACY_PASSWORD = form_data.web.YACY_PASSWORD
request.app.state.config.GOOGLE_PSE_API_KEY = form_data.web.GOOGLE_PSE_API_KEY
request.app.state.config.GOOGLE_PSE_ENGINE_ID = (
form_data.web.GOOGLE_PSE_ENGINE_ID
@ -697,6 +726,18 @@ async def update_rag_config(
request.app.state.config.FIRECRAWL_API_BASE_URL = (
form_data.web.FIRECRAWL_API_BASE_URL
)
request.app.state.config.EXTERNAL_WEB_SEARCH_URL = (
form_data.web.EXTERNAL_WEB_SEARCH_URL
)
request.app.state.config.EXTERNAL_WEB_SEARCH_API_KEY = (
form_data.web.EXTERNAL_WEB_SEARCH_API_KEY
)
request.app.state.config.EXTERNAL_WEB_LOADER_URL = (
form_data.web.EXTERNAL_WEB_LOADER_URL
)
request.app.state.config.EXTERNAL_WEB_LOADER_API_KEY = (
form_data.web.EXTERNAL_WEB_LOADER_API_KEY
)
request.app.state.config.TAVILY_EXTRACT_DEPTH = (
form_data.web.TAVILY_EXTRACT_DEPTH
)
@ -749,6 +790,9 @@ async def update_rag_config(
"WEB_SEARCH_DOMAIN_FILTER_LIST": request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST,
"BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL": request.app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL,
"SEARXNG_QUERY_URL": request.app.state.config.SEARXNG_QUERY_URL,
"YACY_QUERY_URL": request.app.state.config.YACY_QUERY_URL,
"YACY_USERNAME": request.app.state.config.YACY_USERNAME,
"YACY_PASSWORD": request.app.state.config.YACY_PASSWORD,
"GOOGLE_PSE_API_KEY": request.app.state.config.GOOGLE_PSE_API_KEY,
"GOOGLE_PSE_ENGINE_ID": request.app.state.config.GOOGLE_PSE_ENGINE_ID,
"BRAVE_SEARCH_API_KEY": request.app.state.config.BRAVE_SEARCH_API_KEY,
@ -778,6 +822,10 @@ async def update_rag_config(
"FIRECRAWL_API_KEY": request.app.state.config.FIRECRAWL_API_KEY,
"FIRECRAWL_API_BASE_URL": request.app.state.config.FIRECRAWL_API_BASE_URL,
"TAVILY_EXTRACT_DEPTH": request.app.state.config.TAVILY_EXTRACT_DEPTH,
"EXTERNAL_WEB_SEARCH_URL": request.app.state.config.EXTERNAL_WEB_SEARCH_URL,
"EXTERNAL_WEB_SEARCH_API_KEY": request.app.state.config.EXTERNAL_WEB_SEARCH_API_KEY,
"EXTERNAL_WEB_LOADER_URL": request.app.state.config.EXTERNAL_WEB_LOADER_URL,
"EXTERNAL_WEB_LOADER_API_KEY": request.app.state.config.EXTERNAL_WEB_LOADER_API_KEY,
"YOUTUBE_LOADER_LANGUAGE": request.app.state.config.YOUTUBE_LOADER_LANGUAGE,
"YOUTUBE_LOADER_PROXY_URL": request.app.state.config.YOUTUBE_LOADER_PROXY_URL,
"YOUTUBE_LOADER_TRANSLATION": request.app.state.YOUTUBE_LOADER_TRANSLATION,
@ -1266,6 +1314,7 @@ def search_web(request: Request, engine: str, query: str) -> list[SearchResult]:
"""Search the web using a search engine and return the results as a list of SearchResult objects.
Will look for a search engine API key in environment variables in the following order:
- SEARXNG_QUERY_URL
- YACY_QUERY_URL + YACY_USERNAME + YACY_PASSWORD
- GOOGLE_PSE_API_KEY + GOOGLE_PSE_ENGINE_ID
- BRAVE_SEARCH_API_KEY
- KAGI_SEARCH_API_KEY
@ -1295,6 +1344,18 @@ def search_web(request: Request, engine: str, query: str) -> list[SearchResult]:
)
else:
raise Exception("No SEARXNG_QUERY_URL found in environment variables")
elif engine == "yacy":
if request.app.state.config.YACY_QUERY_URL:
return search_yacy(
request.app.state.config.YACY_QUERY_URL,
request.app.state.config.YACY_USERNAME,
request.app.state.config.YACY_PASSWORD,
query,
request.app.state.config.WEB_SEARCH_RESULT_COUNT,
request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST,
)
else:
raise Exception("No YACY_QUERY_URL found in environment variables")
elif engine == "google_pse":
if (
request.app.state.config.GOOGLE_PSE_API_KEY
@ -1465,6 +1526,22 @@ def search_web(request: Request, engine: str, query: str) -> list[SearchResult]:
raise Exception(
"No SOUGOU_API_SID or SOUGOU_API_SK found in environment variables"
)
elif engine == "firecrawl":
return search_firecrawl(
request.app.state.config.FIRECRAWL_API_BASE_URL,
request.app.state.config.FIRECRAWL_API_KEY,
query,
request.app.state.config.WEB_SEARCH_RESULT_COUNT,
request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST,
)
elif engine == "external":
return search_external(
request.app.state.config.EXTERNAL_WEB_SEARCH_URL,
request.app.state.config.EXTERNAL_WEB_SEARCH_API_KEY,
query,
request.app.state.config.WEB_SEARCH_RESULT_COUNT,
request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST,
)
else:
raise Exception("No search engine API key found in environment variables")
@ -1477,8 +1554,11 @@ async def process_web_search(
logging.info(
f"trying to web search with {request.app.state.config.WEB_SEARCH_ENGINE, form_data.query}"
)
web_results = search_web(
request, request.app.state.config.WEB_SEARCH_ENGINE, form_data.query
web_results = await run_in_threadpool(
search_web,
request,
request.app.state.config.WEB_SEARCH_ENGINE,
form_data.query,
)
except Exception as e:
log.exception(e)
@ -1500,8 +1580,8 @@ async def process_web_search(
)
docs = await loader.aload()
urls = [
doc.metadata["source"] for doc in docs
] # only keep URLs which could be retrieved
doc.metadata.get("source") for doc in docs if doc.metadata.get("source")
] # only keep URLs
if request.app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL:
return {
@ -1521,19 +1601,22 @@ async def process_web_search(
collection_names = []
for doc_idx, doc in enumerate(docs):
if doc and doc.page_content:
collection_name = f"web-search-{calculate_sha256_string(form_data.query + '-' + urls[doc_idx])}"[
:63
]
try:
collection_name = f"web-search-{calculate_sha256_string(form_data.query + '-' + urls[doc_idx])}"[
:63
]
collection_names.append(collection_name)
await run_in_threadpool(
save_docs_to_vector_db,
request,
[doc],
collection_name,
overwrite=True,
user=user,
)
collection_names.append(collection_name)
await run_in_threadpool(
save_docs_to_vector_db,
request,
[doc],
collection_name,
overwrite=True,
user=user,
)
except Exception as e:
log.debug(f"error saving doc {doc_idx}: {e}")
return {
"status": True,

View File

@ -6,6 +6,7 @@ from open_webui.models.groups import Groups
from open_webui.models.chats import Chats
from open_webui.models.users import (
UserModel,
UserListResponse,
UserRoleUpdateForm,
Users,
UserSettings,
@ -33,13 +34,38 @@ router = APIRouter()
############################
@router.get("/", response_model=list[UserModel])
PAGE_ITEM_COUNT = 10
@router.get("/", response_model=UserListResponse)
async def get_users(
skip: Optional[int] = None,
limit: Optional[int] = None,
query: Optional[str] = None,
order_by: Optional[str] = None,
direction: Optional[str] = None,
page: Optional[int] = 1,
user=Depends(get_admin_user),
):
return Users.get_users(skip, limit)
limit = PAGE_ITEM_COUNT
page = max(1, page)
skip = (page - 1) * limit
filter = {}
if query:
filter["query"] = query
if order_by:
filter["order_by"] = order_by
if direction:
filter["direction"] = direction
return Users.get_users(filter=filter, skip=skip, limit=limit)
@router.get("/all", response_model=UserListResponse)
async def get_all_users(
user=Depends(get_admin_user),
):
return Users.get_users()
############################
@ -88,6 +114,8 @@ class ChatPermissions(BaseModel):
file_upload: bool = True
delete: bool = True
edit: bool = True
share: bool = True
export: bool = True
stt: bool = True
tts: bool = True
call: bool = True
@ -288,6 +316,21 @@ async def update_user_by_id(
form_data: UserUpdateForm,
session_user=Depends(get_admin_user),
):
# Prevent modification of the primary admin user by other admins
try:
first_user = Users.get_first_user()
if first_user and user_id == first_user.id and session_user.id != user_id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=ERROR_MESSAGES.ACTION_PROHIBITED,
)
except Exception as e:
log.error(f"Error checking primary admin status: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Could not verify primary admin status.",
)
user = Users.get_user_by_id(user_id)
if user:
@ -335,6 +378,21 @@ async def update_user_by_id(
@router.delete("/{user_id}", response_model=bool)
async def delete_user_by_id(user_id: str, user=Depends(get_admin_user)):
# Prevent deletion of the primary admin user
try:
first_user = Users.get_first_user()
if first_user and user_id == first_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=ERROR_MESSAGES.ACTION_PROHIBITED,
)
except Exception as e:
log.error(f"Error checking primary admin status: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Could not verify primary admin status.",
)
if user.id != user_id:
result = Auths.delete_auth_by_id(user_id)
@ -346,6 +404,7 @@ async def delete_user_by_id(user_id: str, user=Depends(get_admin_user)):
detail=ERROR_MESSAGES.DELETE_USER_ERROR,
)
# Prevent self-deletion
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=ERROR_MESSAGES.ACTION_PROHIBITED,

View File

@ -192,6 +192,9 @@ async def connect(sid, environ, auth):
# print(f"user {user.name}({user.id}) connected with session ID {sid}")
await sio.emit("user-list", {"user_ids": list(USER_POOL.keys())})
await sio.emit("usage", {"models": get_models_in_use()})
return True
return False
@sio.on("user-join")
@ -314,16 +317,18 @@ def get_event_emitter(request_info, update_db=True):
)
)
for session_id in session_ids:
await sio.emit(
"chat-events",
{
"chat_id": request_info.get("chat_id", None),
"message_id": request_info.get("message_id", None),
"data": event_data,
},
to=session_id,
)
emit_tasks = [sio.emit(
"chat-events",
{
"chat_id": request_info.get("chat_id", None),
"message_id": request_info.get("message_id", None),
"data": event_data,
},
to=session_id,
)
for session_id in session_ids]
await asyncio.gather(*emit_tasks)
if update_db:
if "type" in event_data and event_data["type"] == "status":

View File

@ -3,7 +3,7 @@ import shutil
import json
import logging
from abc import ABC, abstractmethod
from typing import BinaryIO, Tuple
from typing import BinaryIO, Tuple, Dict
import boto3
from botocore.config import Config
@ -44,7 +44,9 @@ class StorageProvider(ABC):
pass
@abstractmethod
def upload_file(self, file: BinaryIO, filename: str) -> Tuple[bytes, str]:
def upload_file(
self, file: BinaryIO, filename: str, tags: Dict[str, str]
) -> Tuple[bytes, str]:
pass
@abstractmethod
@ -58,7 +60,9 @@ class StorageProvider(ABC):
class LocalStorageProvider(StorageProvider):
@staticmethod
def upload_file(file: BinaryIO, filename: str) -> Tuple[bytes, str]:
def upload_file(
file: BinaryIO, filename: str, tags: Dict[str, str]
) -> Tuple[bytes, str]:
contents = file.read()
if not contents:
raise ValueError(ERROR_MESSAGES.EMPTY_CONTENT)
@ -131,12 +135,20 @@ class S3StorageProvider(StorageProvider):
self.bucket_name = S3_BUCKET_NAME
self.key_prefix = S3_KEY_PREFIX if S3_KEY_PREFIX else ""
def upload_file(self, file: BinaryIO, filename: str) -> Tuple[bytes, str]:
def upload_file(
self, file: BinaryIO, filename: str, tags: Dict[str, str]
) -> Tuple[bytes, str]:
"""Handles uploading of the file to S3 storage."""
_, file_path = LocalStorageProvider.upload_file(file, filename)
_, file_path = LocalStorageProvider.upload_file(file, filename, tags)
tagging = {"TagSet": [{"Key": k, "Value": v} for k, v in tags.items()]}
try:
s3_key = os.path.join(self.key_prefix, filename)
self.s3_client.upload_file(file_path, self.bucket_name, s3_key)
self.s3_client.put_object_tagging(
Bucket=self.bucket_name,
Key=s3_key,
Tagging=tagging,
)
return (
open(file_path, "rb").read(),
"s3://" + self.bucket_name + "/" + s3_key,
@ -207,9 +219,11 @@ class GCSStorageProvider(StorageProvider):
self.gcs_client = storage.Client()
self.bucket = self.gcs_client.bucket(GCS_BUCKET_NAME)
def upload_file(self, file: BinaryIO, filename: str) -> Tuple[bytes, str]:
def upload_file(
self, file: BinaryIO, filename: str, tags: Dict[str, str]
) -> Tuple[bytes, str]:
"""Handles uploading of the file to GCS storage."""
contents, file_path = LocalStorageProvider.upload_file(file, filename)
contents, file_path = LocalStorageProvider.upload_file(file, filename, tags)
try:
blob = self.bucket.blob(filename)
blob.upload_from_filename(file_path)
@ -277,9 +291,11 @@ class AzureStorageProvider(StorageProvider):
self.container_name
)
def upload_file(self, file: BinaryIO, filename: str) -> Tuple[bytes, str]:
def upload_file(
self, file: BinaryIO, filename: str, tags: Dict[str, str]
) -> Tuple[bytes, str]:
"""Handles uploading of the file to Azure Blob Storage."""
contents, file_path = LocalStorageProvider.upload_file(file, filename)
contents, file_path = LocalStorageProvider.upload_file(file, filename, tags)
try:
blob_client = self.container_client.get_blob_client(filename)
blob_client.upload_blob(contents, overwrite=True)

View File

@ -37,7 +37,7 @@ if TYPE_CHECKING:
class AuditLogEntry:
# `Metadata` audit level properties
id: str
user: dict[str, Any]
user: Optional[dict[str, Any]]
audit_level: str
verb: str
request_uri: str
@ -190,21 +190,40 @@ class AuditLoggingMiddleware:
finally:
await self._log_audit_entry(request, context)
async def _get_authenticated_user(self, request: Request) -> UserModel:
async def _get_authenticated_user(self, request: Request) -> Optional[UserModel]:
auth_header = request.headers.get("Authorization")
assert auth_header
user = get_current_user(request, None, get_http_authorization_cred(auth_header))
return user
try:
user = get_current_user(
request, None, get_http_authorization_cred(auth_header)
)
return user
except Exception as e:
logger.debug(f"Failed to get authenticated user: {str(e)}")
return None
def _should_skip_auditing(self, request: Request) -> bool:
if (
request.method not in {"POST", "PUT", "PATCH", "DELETE"}
or AUDIT_LOG_LEVEL == "NONE"
or not request.headers.get("authorization")
):
return True
ALWAYS_LOG_ENDPOINTS = {
"/api/v1/auths/signin",
"/api/v1/auths/signout",
"/api/v1/auths/signup",
}
path = request.url.path.lower()
for endpoint in ALWAYS_LOG_ENDPOINTS:
if path.startswith(endpoint):
return False # Do NOT skip logging for auth endpoints
# Skip logging if the request is not authenticated
if not request.headers.get("authorization"):
return True
# match either /api/<resource>/...(for the endpoint /api/chat case) or /api/v1/<resource>/...
pattern = re.compile(
r"^/api(?:/v1)?/(" + "|".join(self.excluded_paths) + r")\b"
@ -231,17 +250,32 @@ class AuditLoggingMiddleware:
try:
user = await self._get_authenticated_user(request)
user = (
user.model_dump(include={"id", "name", "email", "role"}) if user else {}
)
request_body = context.request_body.decode("utf-8", errors="replace")
response_body = context.response_body.decode("utf-8", errors="replace")
# Redact sensitive information
if "password" in request_body:
request_body = re.sub(
r'"password":\s*"(.*?)"',
'"password": "********"',
request_body,
)
entry = AuditLogEntry(
id=str(uuid.uuid4()),
user=user.model_dump(include={"id", "name", "email", "role"}),
user=user,
audit_level=self.audit_level.value,
verb=request.method,
request_uri=str(request.url),
response_status_code=context.metadata.get("response_status_code", None),
source_ip=request.client.host if request.client else None,
user_agent=request.headers.get("user-agent"),
request_object=context.request_body.decode("utf-8", errors="replace"),
response_object=context.response_body.decode("utf-8", errors="replace"),
request_object=request_body,
response_object=response_body,
)
self.audit_logger.write(entry)

View File

@ -50,7 +50,7 @@ class JupyterCodeExecuter:
self.password = password
self.timeout = timeout
self.kernel_id = ""
self.session = aiohttp.ClientSession(base_url=self.base_url)
self.session = aiohttp.ClientSession(trust_env=True, base_url=self.base_url)
self.params = {}
self.result = ResultModel()

View File

@ -888,16 +888,20 @@ async def process_chat_payload(request, form_data, user, metadata, model):
# If context is not empty, insert it into the messages
if len(sources) > 0:
context_string = ""
citated_file_idx = {}
for _, source in enumerate(sources, 1):
citation_idx = {}
for source in sources:
if "document" in source:
for doc_context, doc_meta in zip(
source["document"], source["metadata"]
):
file_id = doc_meta.get("file_id")
if file_id not in citated_file_idx:
citated_file_idx[file_id] = len(citated_file_idx) + 1
context_string += f'<source id="{citated_file_idx[file_id]}">{doc_context}</source>\n'
citation_id = (
doc_meta.get("source", None)
or source.get("source", {}).get("id", None)
or "N/A"
)
if citation_id not in citation_idx:
citation_idx[citation_id] = len(citation_idx) + 1
context_string += f'<source id="{citation_idx[citation_id]}">{doc_context}</source>\n'
context_string = context_string.strip()
prompt = get_last_user_message(form_data["messages"])
@ -1129,7 +1133,7 @@ async def process_chat_response(
)
# Send a webhook notification if the user is not active
if get_active_status_by_user_id(user.id) is None:
if not get_active_status_by_user_id(user.id):
webhook_url = Users.get_user_webhook_url_by_id(user.id)
if webhook_url:
post_webhook(
@ -1667,6 +1671,15 @@ async def process_chat_response(
if current_response_tool_call is None:
# Add the new tool call
delta_tool_call.setdefault(
"function", {}
)
delta_tool_call[
"function"
].setdefault("name", "")
delta_tool_call[
"function"
].setdefault("arguments", "")
response_tool_calls.append(
delta_tool_call
)
@ -2211,7 +2224,7 @@ async def process_chat_response(
)
# Send a webhook notification if the user is not active
if get_active_status_by_user_id(user.id) is None:
if not get_active_status_by_user_id(user.id):
webhook_url = Users.get_user_webhook_url_by_id(user.id)
if webhook_url:
post_webhook(

View File

@ -15,7 +15,7 @@ from starlette.responses import RedirectResponse
from open_webui.models.auths import Auths
from open_webui.models.users import Users
from open_webui.models.groups import Groups, GroupModel, GroupUpdateForm
from open_webui.models.groups import Groups, GroupModel, GroupUpdateForm, GroupForm
from open_webui.config import (
DEFAULT_USER_ROLE,
ENABLE_OAUTH_SIGNUP,
@ -23,6 +23,7 @@ from open_webui.config import (
OAUTH_PROVIDERS,
ENABLE_OAUTH_ROLE_MANAGEMENT,
ENABLE_OAUTH_GROUP_MANAGEMENT,
ENABLE_OAUTH_GROUP_CREATION,
OAUTH_ROLES_CLAIM,
OAUTH_GROUPS_CLAIM,
OAUTH_EMAIL_CLAIM,
@ -57,6 +58,7 @@ auth_manager_config.ENABLE_OAUTH_SIGNUP = ENABLE_OAUTH_SIGNUP
auth_manager_config.OAUTH_MERGE_ACCOUNTS_BY_EMAIL = OAUTH_MERGE_ACCOUNTS_BY_EMAIL
auth_manager_config.ENABLE_OAUTH_ROLE_MANAGEMENT = ENABLE_OAUTH_ROLE_MANAGEMENT
auth_manager_config.ENABLE_OAUTH_GROUP_MANAGEMENT = ENABLE_OAUTH_GROUP_MANAGEMENT
auth_manager_config.ENABLE_OAUTH_GROUP_CREATION = ENABLE_OAUTH_GROUP_CREATION
auth_manager_config.OAUTH_ROLES_CLAIM = OAUTH_ROLES_CLAIM
auth_manager_config.OAUTH_GROUPS_CLAIM = OAUTH_GROUPS_CLAIM
auth_manager_config.OAUTH_EMAIL_CLAIM = OAUTH_EMAIL_CLAIM
@ -152,6 +154,51 @@ class OAuthManager:
user_current_groups: list[GroupModel] = Groups.get_groups_by_member_id(user.id)
all_available_groups: list[GroupModel] = Groups.get_groups()
# Create groups if they don't exist and creation is enabled
if auth_manager_config.ENABLE_OAUTH_GROUP_CREATION:
log.debug("Checking for missing groups to create...")
all_group_names = {g.name for g in all_available_groups}
groups_created = False
# Determine creator ID: Prefer admin, fallback to current user if no admin exists
admin_user = Users.get_admin_user()
creator_id = admin_user.id if admin_user else user.id
log.debug(f"Using creator ID {creator_id} for potential group creation.")
for group_name in user_oauth_groups:
if group_name not in all_group_names:
log.info(
f"Group '{group_name}' not found via OAuth claim. Creating group..."
)
try:
new_group_form = GroupForm(
name=group_name,
description=f"Group '{group_name}' created automatically via OAuth.",
permissions=default_permissions, # Use default permissions from function args
user_ids=[], # Start with no users, user will be added later by subsequent logic
)
# Use determined creator ID (admin or fallback to current user)
created_group = Groups.insert_new_group(
creator_id, new_group_form
)
if created_group:
log.info(
f"Successfully created group '{group_name}' with ID {created_group.id} using creator ID {creator_id}"
)
groups_created = True
# Add to local set to prevent duplicate creation attempts in this run
all_group_names.add(group_name)
else:
log.error(
f"Failed to create group '{group_name}' via OAuth."
)
except Exception as e:
log.error(f"Error creating group '{group_name}' via OAuth: {e}")
# Refresh the list of all available groups if any were created
if groups_created:
all_available_groups = Groups.get_groups()
log.debug("Refreshed list of all available groups after creation.")
log.debug(f"Oauth Groups claim: {oauth_claim}")
log.debug(f"User oauth groups: {user_oauth_groups}")
log.debug(f"User's current groups: {[g.name for g in user_current_groups]}")
@ -257,7 +304,7 @@ class OAuthManager:
try:
access_token = token.get("access_token")
headers = {"Authorization": f"Bearer {access_token}"}
async with aiohttp.ClientSession() as session:
async with aiohttp.ClientSession(trust_env=True) as session:
async with session.get(
"https://api.github.com/user/emails", headers=headers
) as resp:
@ -339,7 +386,7 @@ class OAuthManager:
get_kwargs["headers"] = {
"Authorization": f"Bearer {access_token}",
}
async with aiohttp.ClientSession() as session:
async with aiohttp.ClientSession(trust_env=True) as session:
async with session.get(
picture_url, **get_kwargs
) as resp:

View File

@ -157,7 +157,8 @@ def load_function_module_by_id(function_id, content=None):
raise Exception("No Function class found in the module")
except Exception as e:
log.error(f"Error loading module: {function_id}: {e}")
del sys.modules[module_name] # Cleanup by removing the module in case of error
# Cleanup by removing the module in case of error
del sys.modules[module_name]
Functions.update_function_by_id(function_id, {"is_active": False})
raise e
@ -182,3 +183,32 @@ def install_frontmatter_requirements(requirements: str):
else:
log.info("No requirements found in frontmatter.")
def install_tool_and_function_dependencies():
"""
Install all dependencies for all admin tools and active functions.
By first collecting all dependencies from the frontmatter of each tool and function,
and then installing them using pip. Duplicates or similar version specifications are
handled by pip as much as possible.
"""
function_list = Functions.get_functions(active_only=True)
tool_list = Tools.get_tools()
all_dependencies = ""
try:
for function in function_list:
frontmatter = extract_frontmatter(replace_imports(function.content))
if dependencies := frontmatter.get("requirements"):
all_dependencies += f"{dependencies}, "
for tool in tool_list:
# Only install requirements for admin tools
if tool.user.role == "admin":
frontmatter = extract_frontmatter(replace_imports(tool.content))
if dependencies := frontmatter.get("requirements"):
all_dependencies += f"{dependencies}, "
install_frontmatter_requirements(all_dependencies.strip(", "))
except Exception as e:
log.error(f"Error installing requirements: {e}")

View File

@ -36,7 +36,10 @@ from langchain_core.utils.function_calling import (
from open_webui.models.tools import Tools
from open_webui.models.users import UserModel
from open_webui.utils.plugin import load_tool_module_by_id
from open_webui.env import AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA
from open_webui.env import (
AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA,
AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL,
)
import copy
@ -276,8 +279,8 @@ def convert_function_to_pydantic_model(func: Callable) -> type[BaseModel]:
docstring = func.__doc__
description = parse_description(docstring)
function_descriptions = parse_docstring(docstring)
function_description = parse_description(docstring)
function_param_descriptions = parse_docstring(docstring)
field_defs = {}
for name, param in parameters.items():
@ -285,15 +288,15 @@ def convert_function_to_pydantic_model(func: Callable) -> type[BaseModel]:
type_hint = type_hints.get(name, Any)
default_value = param.default if param.default is not param.empty else ...
description = function_descriptions.get(name, None)
param_description = function_param_descriptions.get(name, None)
if description:
field_defs[name] = type_hint, Field(default_value, description=description)
if param_description:
field_defs[name] = type_hint, Field(default_value, description=param_description)
else:
field_defs[name] = type_hint, default_value
model = create_model(func.__name__, **field_defs)
model.__doc__ = description
model.__doc__ = function_description
return model
@ -371,51 +374,64 @@ def convert_openapi_to_tool_payload(openapi_spec):
for path, methods in openapi_spec.get("paths", {}).items():
for method, operation in methods.items():
tool = {
"type": "function",
"name": operation.get("operationId"),
"description": operation.get(
"description", operation.get("summary", "No description available.")
),
"parameters": {"type": "object", "properties": {}, "required": []},
}
# Extract path and query parameters
for param in operation.get("parameters", []):
param_name = param["name"]
param_schema = param.get("schema", {})
tool["parameters"]["properties"][param_name] = {
"type": param_schema.get("type"),
"description": param_schema.get("description", ""),
if operation.get("operationId"):
tool = {
"type": "function",
"name": operation.get("operationId"),
"description": operation.get(
"description",
operation.get("summary", "No description available."),
),
"parameters": {"type": "object", "properties": {}, "required": []},
}
if param.get("required"):
tool["parameters"]["required"].append(param_name)
# Extract and resolve requestBody if available
request_body = operation.get("requestBody")
if request_body:
content = request_body.get("content", {})
json_schema = content.get("application/json", {}).get("schema")
if json_schema:
resolved_schema = resolve_schema(
json_schema, openapi_spec.get("components", {})
)
if resolved_schema.get("properties"):
tool["parameters"]["properties"].update(
resolved_schema["properties"]
# Extract path and query parameters
for param in operation.get("parameters", []):
param_name = param["name"]
param_schema = param.get("schema", {})
description = param_schema.get("description", "")
if not description:
description = param.get("description") or ""
if param_schema.get("enum") and isinstance(
param_schema.get("enum"), list
):
description += (
f". Possible values: {', '.join(param_schema.get('enum'))}"
)
if "required" in resolved_schema:
tool["parameters"]["required"] = list(
set(
tool["parameters"]["required"]
+ resolved_schema["required"]
)
)
elif resolved_schema.get("type") == "array":
tool["parameters"] = resolved_schema # special case for array
tool["parameters"]["properties"][param_name] = {
"type": param_schema.get("type"),
"description": description,
}
if param.get("required"):
tool["parameters"]["required"].append(param_name)
tool_payload.append(tool)
# Extract and resolve requestBody if available
request_body = operation.get("requestBody")
if request_body:
content = request_body.get("content", {})
json_schema = content.get("application/json", {}).get("schema")
if json_schema:
resolved_schema = resolve_schema(
json_schema, openapi_spec.get("components", {})
)
if resolved_schema.get("properties"):
tool["parameters"]["properties"].update(
resolved_schema["properties"]
)
if "required" in resolved_schema:
tool["parameters"]["required"] = list(
set(
tool["parameters"]["required"]
+ resolved_schema["required"]
)
)
elif resolved_schema.get("type") == "array":
tool["parameters"] = (
resolved_schema # special case for array
)
tool_payload.append(tool)
return tool_payload
@ -431,8 +447,10 @@ async def get_tool_server_data(token: str, url: str) -> Dict[str, Any]:
error = None
try:
timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA)
async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.get(url, headers=headers) as response:
async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
async with session.get(
url, headers=headers, ssl=AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL
) as response:
if response.status != 200:
error_body = await response.json()
raise Exception(error_body)
@ -573,19 +591,26 @@ async def execute_tool_server(
if token:
headers["Authorization"] = f"Bearer {token}"
async with aiohttp.ClientSession() as session:
async with aiohttp.ClientSession(trust_env=True) as session:
request_method = getattr(session, http_method.lower())
if http_method in ["post", "put", "patch"]:
async with request_method(
final_url, json=body_params, headers=headers
final_url,
json=body_params,
headers=headers,
ssl=AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL,
) as response:
if response.status >= 400:
text = await response.text()
raise Exception(f"HTTP error {response.status}: {text}")
return await response.json()
else:
async with request_method(final_url, headers=headers) as response:
async with request_method(
final_url,
headers=headers,
ssl=AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL,
) as response:
if response.status >= 400:
text = await response.text()
raise Exception(f"HTTP error {response.status}: {text}")

View File

@ -31,7 +31,7 @@ APScheduler==3.10.4
RestrictedPython==8.0
loguru==0.7.2
loguru==0.7.3
asgiref==3.8.1
# AI libraries
@ -40,8 +40,8 @@ anthropic
google-generativeai==0.8.4
tiktoken
langchain==0.3.19
langchain-community==0.3.18
langchain==0.3.24
langchain-community==0.3.23
fake-useragent==2.1.0
chromadb==0.6.3
@ -49,11 +49,11 @@ pymilvus==2.5.0
qdrant-client~=1.12.0
opensearch-py==2.8.0
playwright==1.49.1 # Caution: version must match docker-compose.playwright.yaml
elasticsearch==8.17.1
elasticsearch==9.0.1
pinecone==6.0.2
transformers
sentence-transformers==3.3.1
sentence-transformers==4.1.0
accelerate
colbert-ai==0.2.21
einops==0.8.1
@ -81,7 +81,7 @@ azure-ai-documentintelligence==1.0.0
pillow==11.1.0
opencv-python-headless==4.11.0.86
rapidocr-onnxruntime==1.3.24
rapidocr-onnxruntime==1.4.4
rank-bm25==0.2.2
onnxruntime==1.20.1
@ -107,7 +107,7 @@ google-auth-oauthlib
## Tests
docker~=7.1.0
pytest~=8.3.2
pytest~=8.3.5
pytest-docker~=3.1.1
googleapis-common-protos==1.63.2

181
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "open-webui",
"version": "0.6.5",
"version": "0.6.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "open-webui",
"version": "0.6.5",
"version": "0.6.6",
"dependencies": {
"@azure/msal-browser": "^4.5.0",
"@codemirror/lang-javascript": "^6.2.2",
@ -18,8 +18,8 @@
"@pyscript/core": "^0.4.32",
"@sveltejs/adapter-node": "^2.0.0",
"@sveltejs/svelte-virtual-list": "^3.0.1",
"@tiptap/core": "^2.10.0",
"@tiptap/extension-code-block-lowlight": "^2.10.0",
"@tiptap/core": "^2.11.9",
"@tiptap/extension-code-block-lowlight": "^2.11.9",
"@tiptap/extension-highlight": "^2.10.0",
"@tiptap/extension-placeholder": "^2.10.0",
"@tiptap/extension-typography": "^2.10.0",
@ -30,7 +30,7 @@
"bits-ui": "^0.19.7",
"codemirror": "^6.0.1",
"codemirror-lang-elixir": "^4.0.0",
"codemirror-lang-hcl": "^0.0.0-beta.2",
"codemirror-lang-hcl": "^0.1.0",
"crc-32": "^1.2.2",
"dayjs": "^1.11.10",
"dompurify": "^3.2.5",
@ -59,7 +59,7 @@
"prosemirror-markdown": "^1.13.1",
"prosemirror-model": "^1.23.0",
"prosemirror-schema-basic": "^1.2.3",
"prosemirror-schema-list": "^1.4.1",
"prosemirror-schema-list": "^1.5.1",
"prosemirror-state": "^1.4.3",
"prosemirror-view": "^1.34.3",
"pyodide": "^0.27.3",
@ -82,7 +82,7 @@
"@tailwindcss/postcss": "^4.0.0",
"@tailwindcss/typography": "^0.5.13",
"@typescript-eslint/eslint-plugin": "^6.17.0",
"@typescript-eslint/parser": "^6.17.0",
"@typescript-eslint/parser": "^8.31.1",
"cypress": "^13.15.0",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
@ -2890,9 +2890,9 @@
}
},
"node_modules/@tiptap/core": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.10.0.tgz",
"integrity": "sha512-58nAjPxLRFcXepdDqQRC1mhrw6E8Sanqr6bbO4Tz0+FWgDJMZvHG+dOK5wHaDVNSgK2iJDz08ETvQayfOOgDvg==",
"version": "2.11.9",
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.11.9.tgz",
"integrity": "sha512-UZSxQLLyJst47xep3jlyKM6y1ebZnmvbGsB7njBVjfxf5H+4yFpRJwwNqrBHM/vyU55LCtPChojqaYC1wXLf6g==",
"license": "MIT",
"funding": {
"type": "github",
@ -2969,9 +2969,9 @@
}
},
"node_modules/@tiptap/extension-code-block-lowlight": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/@tiptap/extension-code-block-lowlight/-/extension-code-block-lowlight-2.10.0.tgz",
"integrity": "sha512-dAv03XIHT5h+sdFmJzvx2FfpfFOOK9SBKHflRUdqTa8eA+0VZNAcPRjvJWVEWqts1fKZDJj774mO28NlhFzk9Q==",
"version": "2.11.9",
"resolved": "https://registry.npmjs.org/@tiptap/extension-code-block-lowlight/-/extension-code-block-lowlight-2.11.9.tgz",
"integrity": "sha512-bB8N59A2aU18/ieyKRZAI0J0xyimmUckYePqBkUX8HFnq8yf9HsM0NPFpqZdK0eqjnZYCXcNwAI3YluLsHuutw==",
"license": "MIT",
"funding": {
"type": "github",
@ -3687,31 +3687,147 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz",
"integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==",
"version": "8.31.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.31.1.tgz",
"integrity": "sha512-oU/OtYVydhXnumd0BobL9rkJg7wFJ9bFFPmSmB/bf/XWN85hlViji59ko6bSKBXyseT9V8l+CN1nwmlbiN0G7Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "6.21.0",
"@typescript-eslint/types": "6.21.0",
"@typescript-eslint/typescript-estree": "6.21.0",
"@typescript-eslint/visitor-keys": "6.21.0",
"@typescript-eslint/scope-manager": "8.31.1",
"@typescript-eslint/types": "8.31.1",
"@typescript-eslint/typescript-estree": "8.31.1",
"@typescript-eslint/visitor-keys": "8.31.1",
"debug": "^4.3.4"
},
"engines": {
"node": "^16.0.0 || >=18.0.0"
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^7.0.0 || ^8.0.0"
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": {
"version": "8.31.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.31.1.tgz",
"integrity": "sha512-BMNLOElPxrtNQMIsFHE+3P0Yf1z0dJqV9zLdDxN/xLlWMlXK/ApEsVEKzpizg9oal8bAT5Sc7+ocal7AC1HCVw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.31.1",
"@typescript-eslint/visitor-keys": "8.31.1"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": {
"version": "8.31.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.31.1.tgz",
"integrity": "sha512-SfepaEFUDQYRoA70DD9GtytljBePSj17qPxFHA/h3eg6lPTqGJ5mWOtbXCk1YrVU1cTJRd14nhaXWFu0l2troQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": {
"version": "8.31.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.31.1.tgz",
"integrity": "sha512-kaA0ueLe2v7KunYOyWYtlf/QhhZb7+qh4Yw6Ni5kgukMIG+iP773tjgBiLWIXYumWCwEq3nLW+TUywEp8uEeag==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.31.1",
"@typescript-eslint/visitor-keys": "8.31.1",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
"minimatch": "^9.0.4",
"semver": "^7.6.0",
"ts-api-utils": "^2.0.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": {
"version": "8.31.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.31.1.tgz",
"integrity": "sha512-I+/rgqOVBn6f0o7NDTmAPWWC6NuqhV174lfYvAm9fUaWeiefLdux9/YI3/nLugEn9L8fcSi0XmpKi/r5u0nmpw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.31.1",
"eslint-visitor-keys": "^4.2.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/parser/node_modules/eslint-visitor-keys": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
"integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/@typescript-eslint/parser/node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@typescript-eslint/parser/node_modules/ts-api-utils": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
"integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18.12"
},
"peerDependencies": {
"typescript": ">=4.8.4"
}
},
"node_modules/@typescript-eslint/scope-manager": {
@ -4995,9 +5111,9 @@
}
},
"node_modules/codemirror-lang-hcl": {
"version": "0.0.0-beta.2",
"resolved": "https://registry.npmjs.org/codemirror-lang-hcl/-/codemirror-lang-hcl-0.0.0-beta.2.tgz",
"integrity": "sha512-R3ew7Z2EYTdHTMXsWKBW9zxnLoLPYO+CrAa3dPZjXLrIR96Q3GR4cwJKF7zkSsujsnWgwRQZonyWpXYXfhQYuQ==",
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/codemirror-lang-hcl/-/codemirror-lang-hcl-0.1.0.tgz",
"integrity": "sha512-duwKEaQDhkJWad4YQ9pv4282BS6hCdR+gS/qTAj3f9bypXNNZ42bIN43h9WK3DjyZRENtVlUQdrQM1sA44wHmA==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
@ -9860,9 +9976,10 @@
}
},
"node_modules/prosemirror-schema-list": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.4.1.tgz",
"integrity": "sha512-jbDyaP/6AFfDfu70VzySsD75Om2t3sXTOdl5+31Wlxlg62td1haUpty/ybajSfJ1pkGadlOfwQq9kgW5IMo1Rg==",
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz",
"integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==",
"license": "MIT",
"dependencies": {
"prosemirror-model": "^1.0.0",
"prosemirror-state": "^1.0.0",

View File

@ -1,6 +1,6 @@
{
"name": "open-webui",
"version": "0.6.5",
"version": "0.6.6",
"private": true,
"scripts": {
"dev": "npm run pyodide:fetch && vite dev --host",
@ -29,7 +29,7 @@
"@tailwindcss/postcss": "^4.0.0",
"@tailwindcss/typography": "^0.5.13",
"@typescript-eslint/eslint-plugin": "^6.17.0",
"@typescript-eslint/parser": "^6.17.0",
"@typescript-eslint/parser": "^8.31.1",
"cypress": "^13.15.0",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
@ -61,8 +61,8 @@
"@pyscript/core": "^0.4.32",
"@sveltejs/adapter-node": "^2.0.0",
"@sveltejs/svelte-virtual-list": "^3.0.1",
"@tiptap/core": "^2.10.0",
"@tiptap/extension-code-block-lowlight": "^2.10.0",
"@tiptap/core": "^2.11.9",
"@tiptap/extension-code-block-lowlight": "^2.11.9",
"@tiptap/extension-highlight": "^2.10.0",
"@tiptap/extension-placeholder": "^2.10.0",
"@tiptap/extension-typography": "^2.10.0",
@ -73,7 +73,7 @@
"bits-ui": "^0.19.7",
"codemirror": "^6.0.1",
"codemirror-lang-elixir": "^4.0.0",
"codemirror-lang-hcl": "^0.0.0-beta.2",
"codemirror-lang-hcl": "^0.1.0",
"crc-32": "^1.2.2",
"dayjs": "^1.11.10",
"dompurify": "^3.2.5",
@ -102,7 +102,7 @@
"prosemirror-markdown": "^1.13.1",
"prosemirror-model": "^1.23.0",
"prosemirror-schema-basic": "^1.2.3",
"prosemirror-schema-list": "^1.4.1",
"prosemirror-schema-list": "^1.5.1",
"prosemirror-state": "^1.4.3",
"prosemirror-view": "^1.34.3",
"pyodide": "^0.27.3",

View File

@ -58,9 +58,10 @@ dependencies = [
"opensearch-py==2.8.0",
"playwright==1.49.1",
"elasticsearch==8.17.1",
"pinecone==6.0.2",
"transformers",
"sentence-transformers==3.3.1",
"sentence-transformers==4.1.0",
"accelerate",
"colbert-ai==0.2.21",
"einops==0.8.1",

View File

@ -1,5 +1,4 @@
import { WEBUI_API_BASE_URL } from '$lib/constants';
import { t } from 'i18next';
type ChannelForm = {
name: string;

View File

@ -116,10 +116,33 @@ export const updateUserRole = async (token: string, id: string, role: string) =>
return res;
};
export const getUsers = async (token: string) => {
export const getUsers = async (
token: string,
query?: string,
orderBy?: string,
direction?: string,
page = 1
) => {
let error = null;
let res = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/users/`, {
let searchParams = new URLSearchParams();
searchParams.set('page', `${page}`);
if (query) {
searchParams.set('query', query);
}
if (orderBy) {
searchParams.set('order_by', orderBy);
}
if (direction) {
searchParams.set('direction', direction);
}
res = await fetch(`${WEBUI_API_BASE_URL}/users/?${searchParams.toString()}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
@ -140,7 +163,35 @@ export const getUsers = async (token: string) => {
throw error;
}
return res ? res : [];
return res;
};
export const getAllUsers = async (token: string) => {
let error = null;
let res = null;
res = await fetch(`${WEBUI_API_BASE_URL}/users/all`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
error = err.detail;
return null;
});
if (error) {
throw error;
}
return res;
};
export const getUserSettings = async (token: string) => {

View File

@ -387,7 +387,7 @@ class Pipe:
<div class="flex-1 pr-3">
<div class="text-xs text-gray-500 line-clamp-2">
<span class=" font-semibold dark:text-gray-200">{$i18n.t('Warning:')}</span>
{$i18n.t('Functions allow arbitrary code execution')} <br />
{$i18n.t('Functions allow arbitrary code execution.')} <br />
<span class=" font-medium dark:text-gray-400"
>{$i18n.t(`don't install random functions from sources you don't trust.`)}</span
>

View File

@ -42,6 +42,8 @@
let STT_AZURE_API_KEY = '';
let STT_AZURE_REGION = '';
let STT_AZURE_LOCALES = '';
let STT_AZURE_BASE_URL = '';
let STT_AZURE_MAX_SPEAKERS = '';
let STT_DEEPGRAM_API_KEY = '';
let STT_WHISPER_MODEL_LOADING = false;
@ -114,7 +116,9 @@
DEEPGRAM_API_KEY: STT_DEEPGRAM_API_KEY,
AZURE_API_KEY: STT_AZURE_API_KEY,
AZURE_REGION: STT_AZURE_REGION,
AZURE_LOCALES: STT_AZURE_LOCALES
AZURE_LOCALES: STT_AZURE_LOCALES,
AZURE_BASE_URL: STT_AZURE_BASE_URL,
AZURE_MAX_SPEAKERS: STT_AZURE_MAX_SPEAKERS
}
});
@ -157,6 +161,8 @@
STT_AZURE_API_KEY = res.stt.AZURE_API_KEY;
STT_AZURE_REGION = res.stt.AZURE_REGION;
STT_AZURE_LOCALES = res.stt.AZURE_LOCALES;
STT_AZURE_BASE_URL = res.stt.AZURE_BASE_URL;
STT_AZURE_MAX_SPEAKERS = res.stt.AZURE_MAX_SPEAKERS;
STT_DEEPGRAM_API_KEY = res.stt.DEEPGRAM_API_KEY;
}
@ -287,6 +293,32 @@
/>
</div>
</div>
</div>
<div>
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('Base URL')}</div>
<div class="flex w-full">
<div class="flex-1">
<input
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_AZURE_BASE_URL}
placeholder={$i18n.t('(leave blank for Azure Commercial URL auto-generation)')}
/>
</div>
</div>
</div>
<div>
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('Max Speakers')}</div>
<div class="flex w-full">
<div class="flex-1">
<input
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_AZURE_MAX_SPEAKERS}
placeholder={$i18n.t('e.g., 3, 4, 5 (leave blank for default)')}
/>
</div>
</div>
</div>
</div>
{:else if STT_ENGINE === ''}

View File

@ -1,7 +1,7 @@
<script lang="ts">
import DOMPurify from 'dompurify';
import { getBackendConfig, getVersionUpdates, getWebhookUrl, updateWebhookUrl } from '$lib/apis';
import { getVersionUpdates, getWebhookUrl, updateWebhookUrl } from '$lib/apis';
import {
getAdminConfig,
getLdapConfig,
@ -601,6 +601,14 @@
<Switch bind:state={adminConfig.ENABLE_MESSAGE_RATING} />
</div>
<div class="mb-2.5 flex w-full items-center justify-between pr-2">
<div class=" self-center text-xs font-medium">
{$i18n.t('Notes')} ({$i18n.t('Beta')})
</div>
<Switch bind:state={adminConfig.ENABLE_NOTES} />
</div>
<div class="mb-2.5 flex w-full items-center justify-between pr-2">
<div class=" self-center text-xs font-medium">
{$i18n.t('Channels')} ({$i18n.t('Beta')})

View File

@ -14,6 +14,7 @@
let webSearchEngines = [
'searxng',
'yacy',
'google_pse',
'brave',
'kagi',
@ -30,9 +31,11 @@
'bing',
'exa',
'perplexity',
'sougou'
'sougou',
'firecrawl',
'external'
];
let webLoaderEngines = ['playwright', 'firecrawl', 'tavily'];
let webLoaderEngines = ['playwright', 'firecrawl', 'tavily', 'external'];
let webConfig = null;
@ -143,6 +146,53 @@
</div>
</div>
</div>
{:else if webConfig.WEB_SEARCH_ENGINE === 'yacy'}
<div class="mb-2.5 flex w-full flex-col">
<div>
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Yacy Instance URL')}
</div>
<div class="flex w-full">
<div class="flex-1">
<input
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"
placeholder={$i18n.t('Enter Yacy URL (e.g. http://yacy.example.com:8090)')}
bind:value={webConfig.YACY_QUERY_URL}
autocomplete="off"
/>
</div>
</div>
</div>
</div>
<div class="mb-2.5 flex w-full flex-col">
<div class="flex gap-2">
<div class="w-full">
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Yacy Username')}
</div>
<input
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 Yacy Username')}
bind:value={webConfig.YACY_USERNAME}
required
/>
</div>
<div class="w-full">
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Yacy Password')}
</div>
<SensitiveInput
placeholder={$i18n.t('Enter Yacy Password')}
bind:value={webConfig.YACY_PASSWORD}
/>
</div>
</div>
</div>
{:else if webConfig.WEB_SEARCH_ENGINE === 'google_pse'}
<div class="mb-2.5 flex w-full flex-col">
<div>
@ -431,6 +481,68 @@
/>
</div>
</div>
{:else if webConfig.WEB_SEARCH_ENGINE === 'firecrawl'}
<div class="mb-2.5 flex w-full flex-col">
<div>
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Firecrawl API Base URL')}
</div>
<div class="flex w-full">
<div class="flex-1">
<input
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"
placeholder={$i18n.t('Enter Firecrawl API Base URL')}
bind:value={webConfig.FIRECRAWL_API_BASE_URL}
autocomplete="off"
/>
</div>
</div>
</div>
<div class="mt-2">
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Firecrawl API Key')}
</div>
<SensitiveInput
placeholder={$i18n.t('Enter Firecrawl API Key')}
bind:value={webConfig.FIRECRAWL_API_KEY}
/>
</div>
</div>
{:else if webConfig.WEB_SEARCH_ENGINE === 'external'}
<div class="mb-2.5 flex w-full flex-col">
<div>
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('External Web Search URL')}
</div>
<div class="flex w-full">
<div class="flex-1">
<input
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"
placeholder={$i18n.t('Enter External Web Search URL')}
bind:value={webConfig.EXTERNAL_WEB_SEARCH_URL}
autocomplete="off"
/>
</div>
</div>
</div>
<div class="mt-2">
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('External Web Search API Key')}
</div>
<SensitiveInput
placeholder={$i18n.t('Enter External Web Search API Key')}
bind:value={webConfig.EXTERNAL_WEB_SEARCH_API_KEY}
/>
</div>
</div>
{/if}
{/if}
@ -588,7 +700,7 @@
</div>
</div>
</div>
{:else if webConfig.WEB_LOADER_ENGINE === 'firecrawl'}
{:else if webConfig.WEB_LOADER_ENGINE === 'firecrawl' && webConfig.WEB_SEARCH_ENGINE !== 'firecrawl'}
<div class="mb-2.5 flex w-full flex-col">
<div>
<div class=" self-center text-xs font-medium mb-1">
@ -652,6 +764,37 @@
</div>
{/if}
</div>
{:else if webConfig.WEB_LOADER_ENGINE === 'external'}
<div class="mb-2.5 flex w-full flex-col">
<div>
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('External Web Loader URL')}
</div>
<div class="flex w-full">
<div class="flex-1">
<input
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"
placeholder={$i18n.t('Enter External Web Loader URL')}
bind:value={webConfig.EXTERNAL_WEB_LOADER_URL}
autocomplete="off"
/>
</div>
</div>
</div>
<div class="mt-2">
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('External Web Loader API Key')}
</div>
<SensitiveInput
placeholder={$i18n.t('Enter External Web Loader API Key')}
bind:value={webConfig.EXTERNAL_WEB_LOADER_API_KEY}
/>
</div>
</div>
{/if}
<div class=" mb-2.5 flex w-full justify-between">

View File

@ -5,32 +5,19 @@
import { goto } from '$app/navigation';
import { user } from '$lib/stores';
import { getUsers } from '$lib/apis/users';
import UserList from './Users/UserList.svelte';
import Groups from './Users/Groups.svelte';
const i18n = getContext('i18n');
let users = [];
let selectedTab = 'overview';
let loaded = false;
$: if (selectedTab) {
getUsersHandler();
}
const getUsersHandler = async () => {
users = await getUsers(localStorage.token);
};
onMount(async () => {
if ($user?.role !== 'admin') {
await goto('/');
} else {
users = await getUsers(localStorage.token);
}
loaded = true;
const containerElement = document.getElementById('users-tabs-container');
@ -102,9 +89,9 @@
<div class="flex-1 mt-1 lg:mt-0 overflow-y-scroll">
{#if selectedTab === 'overview'}
<UserList {users} />
<UserList />
{:else if selectedTab === 'groups'}
<Groups {users} />
<Groups />
{/if}
</div>
</div>

View File

@ -23,13 +23,18 @@
import GroupItem from './Groups/GroupItem.svelte';
import AddGroupModal from './Groups/AddGroupModal.svelte';
import { createNewGroup, getGroups } from '$lib/apis/groups';
import { getUserDefaultPermissions, updateUserDefaultPermissions } from '$lib/apis/users';
import {
getUserDefaultPermissions,
getAllUsers,
updateUserDefaultPermissions
} from '$lib/apis/users';
const i18n = getContext('i18n');
let loaded = false;
export let users = [];
let users = [];
let total = 0;
let groups = [];
let filteredGroups;
@ -63,6 +68,8 @@
file_upload: true,
delete: true,
edit: true,
share: true,
export: true,
stt: true,
tts: true,
call: true,
@ -116,10 +123,22 @@
onMount(async () => {
if ($user?.role !== 'admin') {
await goto('/');
} else {
await setGroups();
defaultPermissions = await getUserDefaultPermissions(localStorage.token);
return;
}
const res = await getAllUsers(localStorage.token).catch((error) => {
toast.error(`${error}`);
return null;
});
if (res) {
users = res.users;
total = res.total;
}
await setGroups();
defaultPermissions = await getUserDefaultPermissions(localStorage.token);
loaded = true;
});
</script>

View File

@ -24,6 +24,8 @@
file_upload: true,
delete: true,
edit: true,
share: true,
export: true,
stt: true,
tts: true,
call: true,
@ -276,6 +278,22 @@
<Switch bind:state={permissions.chat.edit} />
</div>
<div class=" flex w-full justify-between my-2 pr-2">
<div class=" self-center text-xs font-medium">
{$i18n.t('Allow Chat Share')}
</div>
<Switch bind:state={permissions.chat.share} />
</div>
<div class=" flex w-full justify-between my-2 pr-2">
<div class=" self-center text-xs font-medium">
{$i18n.t('Allow Chat Export')}
</div>
<Switch bind:state={permissions.chat.export} />
</div>
<div class=" flex w-full justify-between my-2 pr-2">
<div class=" self-center text-xs font-medium">
{$i18n.t('Allow Speech to Text')}

View File

@ -33,13 +33,17 @@
const i18n = getContext('i18n');
export let users = [];
let search = '';
let selectedUser = null;
let page = 1;
let users = [];
let total = 0;
let query = '';
let orderBy = 'created_at'; // default sort key
let direction = 'asc'; // default sort order
let selectedUser = null;
let showDeleteConfirmDialog = false;
let showAddUserModal = false;
@ -53,7 +57,7 @@
});
if (res) {
users = await getUsers(localStorage.token);
getUserList();
}
};
@ -63,41 +67,44 @@
return null;
});
if (res) {
users = await getUsers(localStorage.token);
getUserList();
}
};
let sortKey = 'created_at'; // default sort key
let sortOrder = 'asc'; // default sort order
function setSortKey(key) {
if (sortKey === key) {
sortOrder = sortOrder === 'asc' ? 'desc' : 'asc';
const setSortKey = (key) => {
if (orderBy === key) {
direction = direction === 'asc' ? 'desc' : 'asc';
} else {
sortKey = key;
sortOrder = 'asc';
orderBy = key;
direction = 'asc';
}
};
const getUserList = async () => {
try {
const res = await getUsers(localStorage.token, query, orderBy, direction, page).catch(
(error) => {
toast.error(`${error}`);
return null;
}
);
if (res) {
users = res.users;
total = res.total;
}
} catch (err) {
console.error(err);
}
};
$: if (page) {
getUserList();
}
let filteredUsers;
$: filteredUsers = users
.filter((user) => {
if (search === '') {
return true;
} else {
let name = user.name.toLowerCase();
let email = user.email.toLowerCase();
const query = search.toLowerCase();
return name.includes(query) || email.includes(query);
}
})
.sort((a, b) => {
if (a[sortKey] < b[sortKey]) return sortOrder === 'asc' ? -1 : 1;
if (a[sortKey] > b[sortKey]) return sortOrder === 'asc' ? 1 : -1;
return 0;
})
.slice((page - 1) * 20, page * 20);
$: if (query !== null && orderBy && direction) {
getUserList();
}
</script>
<ConfirmDialog
@ -113,7 +120,7 @@
{selectedUser}
sessionUser={$user}
on:save={async () => {
users = await getUsers(localStorage.token);
getUserList();
}}
/>
{/key}
@ -121,7 +128,7 @@
<AddUserModal
bind:show={showAddUserModal}
on:save={async () => {
users = await getUsers(localStorage.token);
getUserList();
}}
/>
<UserChatsModal bind:show={showUserChatsModal} user={selectedUser} />
@ -184,7 +191,7 @@
</div>
<input
class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-hidden bg-transparent"
bind:value={search}
bind:value={query}
placeholder={$i18n.t('Search')}
/>
</div>
@ -223,9 +230,9 @@
<div class="flex gap-1.5 items-center">
{$i18n.t('Role')}
{#if sortKey === 'role'}
{#if orderBy === 'role'}
<span class="font-normal"
>{#if sortOrder === 'asc'}
>{#if direction === 'asc'}
<ChevronUp className="size-2" />
{:else}
<ChevronDown className="size-2" />
@ -246,9 +253,9 @@
<div class="flex gap-1.5 items-center">
{$i18n.t('Name')}
{#if sortKey === 'name'}
{#if orderBy === 'name'}
<span class="font-normal"
>{#if sortOrder === 'asc'}
>{#if direction === 'asc'}
<ChevronUp className="size-2" />
{:else}
<ChevronDown className="size-2" />
@ -269,9 +276,9 @@
<div class="flex gap-1.5 items-center">
{$i18n.t('Email')}
{#if sortKey === 'email'}
{#if orderBy === 'email'}
<span class="font-normal"
>{#if sortOrder === 'asc'}
>{#if direction === 'asc'}
<ChevronUp className="size-2" />
{:else}
<ChevronDown className="size-2" />
@ -293,9 +300,9 @@
<div class="flex gap-1.5 items-center">
{$i18n.t('Last Active')}
{#if sortKey === 'last_active_at'}
{#if orderBy === 'last_active_at'}
<span class="font-normal"
>{#if sortOrder === 'asc'}
>{#if direction === 'asc'}
<ChevronUp className="size-2" />
{:else}
<ChevronDown className="size-2" />
@ -315,9 +322,9 @@
>
<div class="flex gap-1.5 items-center">
{$i18n.t('Created at')}
{#if sortKey === 'created_at'}
{#if orderBy === 'created_at'}
<span class="font-normal"
>{#if sortOrder === 'asc'}
>{#if direction === 'asc'}
<ChevronUp className="size-2" />
{:else}
<ChevronDown className="size-2" />
@ -339,9 +346,9 @@
<div class="flex gap-1.5 items-center">
{$i18n.t('OAuth ID')}
{#if sortKey === 'oauth_sub'}
{#if orderBy === 'oauth_sub'}
<span class="font-normal"
>{#if sortOrder === 'asc'}
>{#if direction === 'asc'}
<ChevronUp className="size-2" />
{:else}
<ChevronDown className="size-2" />
@ -359,7 +366,7 @@
</tr>
</thead>
<tbody class="">
{#each filteredUsers as user, userIdx}
{#each users as user, userIdx}
<tr class="bg-white dark:bg-gray-900 dark:border-gray-850 text-xs">
<td class="px-3 py-1 min-w-[7rem] w-28">
<button
@ -486,10 +493,10 @@
{$i18n.t("Click on the user role button to change a user's role.")}
</div>
<Pagination bind:page count={users.length} />
<Pagination bind:page count={total} perPage={10} />
{#if !$config?.license_metadata}
{#if users.length > 50}
{#if total > 50}
<div class="text-sm">
<Markdown
content={`

View File

@ -109,7 +109,7 @@
stopLoading();
};
reader.readAsText(file);
reader.readAsText(file, 'utf-8');
} else {
toast.error($i18n.t('File not found.'));
}

View File

@ -45,7 +45,7 @@
<Modal size="sm" bind:show>
<div>
<div class=" flex justify-between dark:text-gray-300 px-5 py-4">
<div class=" flex justify-between dark:text-gray-300 px-5 pt-4 pb-2">
<div class=" text-lg font-medium self-center">{$i18n.t('Edit User')}</div>
<button
class="self-center"
@ -65,9 +65,8 @@
</svg>
</button>
</div>
<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 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">
<form
class="flex flex-col w-full"
@ -75,7 +74,7 @@
submitHandler();
}}
>
<div class=" flex items-center rounded-md py-2 px-4 w-full">
<div class=" flex items-center rounded-md px-5 py-2 w-full">
<div class=" self-center mr-5">
<img
src={selectedUser.profile_image_url}
@ -94,59 +93,62 @@
</div>
</div>
<hr class="border-gray-100 dark:border-gray-850 my-3 w-full" />
<div class=" px-5 pt-3 pb-5">
<div class=" flex flex-col space-y-1.5">
<div class="flex flex-col w-full">
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Email')}</div>
<div class=" flex flex-col space-y-1.5">
<div class="flex flex-col w-full">
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Email')}</div>
<div class="flex-1">
<input
class="w-full rounded-sm text-sm bg-transparent disabled:text-gray-500 dark:disabled:text-gray-500 outline-hidden"
type="email"
bind:value={_user.email}
placeholder={$i18n.t('Enter Your Email')}
autocomplete="off"
required
disabled={_user.id == sessionUser.id}
/>
</div>
</div>
<div class="flex-1">
<input
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"
bind:value={_user.email}
autocomplete="off"
required
disabled={_user.id == sessionUser.id}
/>
<div class="flex flex-col w-full">
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Name')}</div>
<div class="flex-1">
<input
class="w-full rounded-sm text-sm bg-transparent outline-hidden"
type="text"
bind:value={_user.name}
placeholder={$i18n.t('Enter Your Name')}
autocomplete="off"
required
/>
</div>
</div>
<div class="flex flex-col w-full">
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('New Password')}</div>
<div class="flex-1">
<input
class="w-full rounded-sm text-sm bg-transparent outline-hidden"
type="password"
placeholder={$i18n.t('Enter New Password')}
bind:value={_user.password}
autocomplete="new-password"
/>
</div>
</div>
</div>
<div class="flex flex-col w-full">
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Name')}</div>
<div class="flex-1">
<input
class="w-full rounded-sm py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-hidden"
type="text"
bind:value={_user.name}
autocomplete="off"
required
/>
</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 flex flex-row space-x-1 items-center"
type="submit"
>
{$i18n.t('Save')}
</button>
</div>
<div class="flex flex-col w-full">
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('New Password')}</div>
<div class="flex-1">
<input
class="w-full rounded-sm py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-hidden"
type="password"
bind:value={_user.password}
autocomplete="new-password"
/>
</div>
</div>
</div>
<div class="flex justify-end pt-3 text-sm font-medium">
<button
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
type="submit"
>
{$i18n.t('Save')}
</button>
</div>
</form>
</div>

View File

@ -262,7 +262,7 @@
{#if threadId !== null}
<Drawer
show={threadId !== null}
on:close={() => {
onClose={() => {
threadId = null;
}}
>

View File

@ -236,9 +236,11 @@
await tick();
await tick();
const messageElement = document.getElementById(`message-${message.id}`);
if (messageElement) {
messageElement.scrollIntoView({ behavior: 'smooth' });
if ($settings?.scrollOnBranchChange ?? true) {
const messageElement = document.getElementById(`message-${message.id}`);
if (messageElement) {
messageElement.scrollIntoView({ behavior: 'smooth' });
}
}
await tick();

View File

@ -140,7 +140,7 @@
{#if $showControls}
<Drawer
show={$showControls}
on:close={() => {
onClose={() => {
showControls.set(false);
}}
>

View File

@ -395,39 +395,37 @@
</div>
<div class="w-full relative">
{#if atSelectedModel !== undefined || selectedToolIds.length > 0 || webSearchEnabled || ($settings?.webSearch ?? false) === 'always' || imageGenerationEnabled || codeInterpreterEnabled}
{#if atSelectedModel !== undefined}
<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-linear-to-t from-white dark:from-gray-900 z-10"
>
{#if atSelectedModel !== undefined}
<div class="flex items-center justify-between w-full">
<div class="pl-[1px] flex items-center gap-2 text-sm dark:text-gray-500">
<img
crossorigin="anonymous"
alt="model profile"
class="size-3.5 max-w-[28px] object-cover rounded-full"
src={$models.find((model) => model.id === atSelectedModel.id)?.info?.meta
?.profile_image_url ??
($i18n.language === 'dg-DG'
? `/doge.png`
: `${WEBUI_BASE_URL}/static/favicon.png`)}
/>
<div class="translate-y-[0.5px]">
Talking to <span class=" font-medium">{atSelectedModel.name}</span>
</div>
</div>
<div>
<button
class="flex items-center dark:text-gray-500"
on:click={() => {
atSelectedModel = undefined;
}}
>
<XMark />
</button>
<div class="flex items-center justify-between w-full">
<div class="pl-[1px] flex items-center gap-2 text-sm dark:text-gray-500">
<img
crossorigin="anonymous"
alt="model profile"
class="size-3.5 max-w-[28px] object-cover rounded-full"
src={$models.find((model) => model.id === atSelectedModel.id)?.info?.meta
?.profile_image_url ??
($i18n.language === 'dg-DG'
? `/doge.png`
: `${WEBUI_BASE_URL}/static/favicon.png`)}
/>
<div class="translate-y-[0.5px]">
Talking to <span class=" font-medium">{atSelectedModel.name}</span>
</div>
</div>
{/if}
<div>
<button
class="flex items-center dark:text-gray-500"
on:click={() => {
atSelectedModel = undefined;
}}
>
<XMark />
</button>
</div>
</div>
</div>
{/if}
@ -1063,9 +1061,9 @@
);
}
}}
uploadOneDriveHandler={async () => {
uploadOneDriveHandler={async (authorityType) => {
try {
const fileData = await pickAndDownloadFile();
const fileData = await pickAndDownloadFile(authorityType);
if (fileData) {
const file = new File([fileData.blob], fileData.name, {
type: fileData.blob.type || 'application/octet-stream'

View File

@ -265,7 +265,7 @@
{/each}
{:else}
<div class=" text-gray-500 text-xs mt-1 mb-2">
{$i18n.t('No files found.')}
{$i18n.t('File not found.')}
</div>
{/if}
</div> -->

View File

@ -146,7 +146,7 @@
{/if}
<Tooltip
content={!fileUploadEnabled ? $i18n.t('You do not have permission to upload files') : ''}
content={!fileUploadEnabled ? $i18n.t('You do not have permission to upload files.') : ''}
className="w-full"
>
<DropdownMenu.Item
@ -173,7 +173,7 @@
</Tooltip>
<Tooltip
content={!fileUploadEnabled ? $i18n.t('You do not have permission to upload files') : ''}
content={!fileUploadEnabled ? $i18n.t('You do not have permission to upload files.') : ''}
className="w-full"
>
<DropdownMenu.Item
@ -229,94 +229,119 @@
{/if}
{#if $config?.features?.enable_onedrive_integration}
<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-xl"
on:click={() => {
uploadOneDriveHandler();
}}
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" class="w-5 h-5" fill="none">
<mask
id="mask0_87_7796"
style="mask-type:alpha"
maskUnits="userSpaceOnUse"
x="0"
y="6"
width="32"
height="20"
<DropdownMenu.Sub>
<DropdownMenu.SubTrigger
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-xl w-full"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" class="w-5 h-5" fill="none">
<mask
id="mask0_87_7796"
style="mask-type:alpha"
maskUnits="userSpaceOnUse"
x="0"
y="6"
width="32"
height="20"
>
<path
d="M7.82979 26C3.50549 26 0 22.5675 0 18.3333C0 14.1921 3.35322 10.8179 7.54613 10.6716C9.27535 7.87166 12.4144 6 16 6C20.6308 6 24.5169 9.12183 25.5829 13.3335C29.1316 13.3603 32 16.1855 32 19.6667C32 23.0527 29 26 25.8723 25.9914L7.82979 26Z"
fill="#C4C4C4"
/>
</mask>
<g mask="url(#mask0_87_7796)">
<path
d="M7.83017 26.0001C5.37824 26.0001 3.18957 24.8966 1.75391 23.1691L18.0429 16.3335L30.7089 23.4647C29.5926 24.9211 27.9066 26.0001 26.0004 25.9915C23.1254 26.0001 12.0629 26.0001 7.83017 26.0001Z"
fill="url(#paint0_linear_87_7796)"
/>
<path
d="M25.5785 13.3149L18.043 16.3334L30.709 23.4647C31.5199 22.4065 32.0004 21.0916 32.0004 19.6669C32.0004 16.1857 29.1321 13.3605 25.5833 13.3337C25.5817 13.3274 25.5801 13.3212 25.5785 13.3149Z"
fill="url(#paint1_linear_87_7796)"
/>
<path
d="M7.06445 10.7028L18.0423 16.3333L25.5779 13.3148C24.5051 9.11261 20.6237 6 15.9997 6C12.4141 6 9.27508 7.87166 7.54586 10.6716C7.3841 10.6773 7.22358 10.6877 7.06445 10.7028Z"
fill="url(#paint2_linear_87_7796)"
/>
<path
d="M1.7535 23.1687L18.0425 16.3331L7.06471 10.7026C3.09947 11.0792 0 14.3517 0 18.3331C0 20.1665 0.657197 21.8495 1.7535 23.1687Z"
fill="url(#paint3_linear_87_7796)"
/>
</g>
<defs>
<linearGradient
id="paint0_linear_87_7796"
x1="4.42591"
y1="24.6668"
x2="27.2309"
y2="23.2764"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#2086B8" />
<stop offset="1" stop-color="#46D3F6" />
</linearGradient>
<linearGradient
id="paint1_linear_87_7796"
x1="23.8302"
y1="19.6668"
x2="30.2108"
y2="15.2082"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#1694DB" />
<stop offset="1" stop-color="#62C3FE" />
</linearGradient>
<linearGradient
id="paint2_linear_87_7796"
x1="8.51037"
y1="7.33333"
x2="23.3335"
y2="15.9348"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#0D3D78" />
<stop offset="1" stop-color="#063B83" />
</linearGradient>
<linearGradient
id="paint3_linear_87_7796"
x1="-0.340429"
y1="19.9998"
x2="14.5634"
y2="14.4649"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#16589B" />
<stop offset="1" stop-color="#1464B7" />
</linearGradient>
</defs>
</svg>
<div class="line-clamp-1">{$i18n.t('Microsoft OneDrive')}</div>
</DropdownMenu.SubTrigger>
<DropdownMenu.SubContent
class="w-[calc(100vw-2rem)] max-w-[280px] rounded-xl px-1 py-1 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-sm"
side={$mobile ? 'bottom' : 'right'}
sideOffset={$mobile ? 5 : 0}
alignOffset={$mobile ? 0 : -8}
>
<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-xl"
on:click={() => {
uploadOneDriveHandler('personal');
}}
>
<path
d="M7.82979 26C3.50549 26 0 22.5675 0 18.3333C0 14.1921 3.35322 10.8179 7.54613 10.6716C9.27535 7.87166 12.4144 6 16 6C20.6308 6 24.5169 9.12183 25.5829 13.3335C29.1316 13.3603 32 16.1855 32 19.6667C32 23.0527 29 26 25.8723 25.9914L7.82979 26Z"
fill="#C4C4C4"
/>
</mask>
<g mask="url(#mask0_87_7796)">
<path
d="M7.83017 26.0001C5.37824 26.0001 3.18957 24.8966 1.75391 23.1691L18.0429 16.3335L30.7089 23.4647C29.5926 24.9211 27.9066 26.0001 26.0004 25.9915C23.1254 26.0001 12.0629 26.0001 7.83017 26.0001Z"
fill="url(#paint0_linear_87_7796)"
/>
<path
d="M25.5785 13.3149L18.043 16.3334L30.709 23.4647C31.5199 22.4065 32.0004 21.0916 32.0004 19.6669C32.0004 16.1857 29.1321 13.3605 25.5833 13.3337C25.5817 13.3274 25.5801 13.3212 25.5785 13.3149Z"
fill="url(#paint1_linear_87_7796)"
/>
<path
d="M7.06445 10.7028L18.0423 16.3333L25.5779 13.3148C24.5051 9.11261 20.6237 6 15.9997 6C12.4141 6 9.27508 7.87166 7.54586 10.6716C7.3841 10.6773 7.22358 10.6877 7.06445 10.7028Z"
fill="url(#paint2_linear_87_7796)"
/>
<path
d="M1.7535 23.1687L18.0425 16.3331L7.06471 10.7026C3.09947 11.0792 0 14.3517 0 18.3331C0 20.1665 0.657197 21.8495 1.7535 23.1687Z"
fill="url(#paint3_linear_87_7796)"
/>
</g>
<defs>
<linearGradient
id="paint0_linear_87_7796"
x1="4.42591"
y1="24.6668"
x2="27.2309"
y2="23.2764"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#2086B8" />
<stop offset="1" stop-color="#46D3F6" />
</linearGradient>
<linearGradient
id="paint1_linear_87_7796"
x1="23.8302"
y1="19.6668"
x2="30.2108"
y2="15.2082"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#1694DB" />
<stop offset="1" stop-color="#62C3FE" />
</linearGradient>
<linearGradient
id="paint2_linear_87_7796"
x1="8.51037"
y1="7.33333"
x2="23.3335"
y2="15.9348"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#0D3D78" />
<stop offset="1" stop-color="#063B83" />
</linearGradient>
<linearGradient
id="paint3_linear_87_7796"
x1="-0.340429"
y1="19.9998"
x2="14.5634"
y2="14.4649"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#16589B" />
<stop offset="1" stop-color="#1464B7" />
</linearGradient>
</defs>
</svg>
<div class="line-clamp-1">{$i18n.t('OneDrive')}</div>
</DropdownMenu.Item>
<div class="line-clamp-1">{$i18n.t('Microsoft OneDrive (personal)')}</div>
</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-xl"
on:click={() => {
uploadOneDriveHandler('organizations');
}}
>
<div class="flex flex-col">
<div class="line-clamp-1">{$i18n.t('Microsoft OneDrive (work/school)')}</div>
<div class="text-xs text-gray-500">Includes SharePoint</div>
</div>
</DropdownMenu.Item>
</DropdownMenu.SubContent>
</DropdownMenu.Sub>
{/if}
</DropdownMenu.Content>
</div>

View File

@ -83,6 +83,7 @@
});
return acc;
}, []);
console.log('citations', citations);
showRelevance = calculateShowRelevance(citations);
showPercentage = shouldShowPercentage(citations);

View File

@ -139,13 +139,16 @@
{percentage.toFixed(2)}%
</span>
{/if}
{#if typeof document?.distance === 'number'}
<span class="text-gray-500 dark:text-gray-500">
({(document?.distance ?? 0).toFixed(4)})
</span>
{/if}
{:else if typeof document?.distance === 'number'}
<span class="text-gray-500 dark:text-gray-500">
({(document?.distance ?? 0).toFixed(4)})
</span>
{:else}
<span class="text-gray-500 dark:text-gray-500">
{(document?.distance ?? 0).toFixed(4)}
</span>
{/if}
</div>
</Tooltip>

View File

@ -154,11 +154,11 @@
}, [])}
{onSourceClick}
{onTaskClick}
on:update={(e) => {
dispatch('update', e.detail);
onUpdate={(value) => {
dispatch('update', value);
}}
on:code={(e) => {
const { lang, code } = e.detail;
onCode={(value) => {
const { lang, code } = value;
if (
($settings?.detectArtifacts ?? true) &&

View File

@ -7,9 +7,6 @@
import markedKatexExtension from '$lib/utils/marked/katex-extension';
import MarkdownTokens from './Markdown/MarkdownTokens.svelte';
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
export let id = '';
export let content;
@ -18,6 +15,9 @@
export let sourceIds = [];
export let onUpdate = () => {};
export let onCode = () => {};
export let onSourceClick = () => {};
export let onTaskClick = () => {};
@ -40,17 +40,5 @@
</script>
{#key id}
<MarkdownTokens
{tokens}
{id}
{save}
{onTaskClick}
{onSourceClick}
on:update={(e) => {
dispatch('update', e.detail);
}}
on:code={(e) => {
dispatch('code', e.detail);
}}
/>
<MarkdownTokens {tokens} {id} {save} {onTaskClick} {onSourceClick} {onUpdate} {onCode} />
{/key}

View File

@ -0,0 +1,66 @@
<script lang="ts">
import DOMPurify from 'dompurify';
import type { Token } from 'marked';
import { WEBUI_BASE_URL } from '$lib/constants';
import Source from './Source.svelte';
import { settings } from '$lib/stores';
export let id: string;
export let token: Token;
export let onSourceClick: Function = () => {};
let html: string | null = null;
$: if (token.type === 'html' && token?.text) {
html = DOMPurify.sanitize(token.text);
} else {
html = null;
}
</script>
{#if token.type === 'html'}
{#if html && html.includes('<video')}
{@html html}
{:else if token.text && token.text.match(/<iframe\s+[^>]*src="https:\/\/www\.youtube\.com\/embed\/([a-zA-Z0-9_-]{11})(?:\?[^"]*)?"[^>]*><\/iframe>/)}
{@const match = token.text.match(
/<iframe\s+[^>]*src="https:\/\/www\.youtube\.com\/embed\/([a-zA-Z0-9_-]{11})(?:\?[^"]*)?"[^>]*><\/iframe>/
)}
{@const ytId = match && match[1]}
{#if ytId}
<iframe
class="w-full aspect-video my-2"
src={`https://www.youtube.com/embed/${ytId}`}
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen
>
</iframe>
{/if}
{:else if token.text.includes(`<file type="html"`)}
{@const match = token.text.match(/<file type="html" id="([^"]+)"/)}
{@const fileId = match && match[1]}
{#if fileId}
<iframe
class="w-full my-2"
src={`${WEBUI_BASE_URL}/api/v1/files/${fileId}/content/html`}
title="Content"
frameborder="0"
sandbox="allow-scripts{($settings?.iframeSandboxAllowForms ?? false)
? ' allow-forms'
: ''}{($settings?.iframeSandboxAllowSameOrigin ?? false) ? ' allow-same-origin' : ''}"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen
width="100%"
onload="this.style.height=(this.contentWindow.document.body.scrollHeight+20)+'px';"
></iframe>
{/if}
{:else if token.text.includes(`<source_id`)}
<Source {id} {token} onClick={onSourceClick} />
{:else}
{token.text}
{/if}
{/if}

View File

@ -13,6 +13,7 @@
import Image from '$lib/components/common/Image.svelte';
import KatexRenderer from './KatexRenderer.svelte';
import Source from './Source.svelte';
import HtmlToken from './HTMLToken.svelte';
export let id: string;
export let tokens: Token[];
@ -23,16 +24,7 @@
{#if token.type === 'escape'}
{unescapeHtml(token.text)}
{:else if token.type === 'html'}
{@const html = DOMPurify.sanitize(token.text)}
{#if html && html.includes('<video')}
{@html html}
{:else if token.text.includes(`<iframe src="${WEBUI_BASE_URL}/api/v1/files/`)}
{@html `${token.text}`}
{:else if token.text.includes(`<source_id`)}
<Source {id} {token} onClick={onSourceClick} />
{:else}
{@html html}
{/if}
<HtmlToken {id} {token} {onSourceClick} />
{:else if token.type === 'link'}
{#if token.tokens}
<a href={token.href} target="_blank" rel="nofollow" title={token.title}>

View File

@ -1,6 +1,6 @@
<script lang="ts">
import DOMPurify from 'dompurify';
import { createEventDispatcher, onMount, getContext } from 'svelte';
import { onMount, getContext } from 'svelte';
const i18n = getContext('i18n');
import fileSaver from 'file-saver';
@ -21,8 +21,7 @@
import Source from './Source.svelte';
import { settings } from '$lib/stores';
const dispatch = createEventDispatcher();
import HtmlToken from './HTMLToken.svelte';
export let id: string;
export let tokens: Token[];
@ -31,6 +30,9 @@
export let save = false;
export let onUpdate: Function = () => {};
export let onCode: Function = () => {};
export let onTaskClick: Function = () => {};
export let onSourceClick: Function = () => {};
@ -93,11 +95,9 @@
code={token?.text ?? ''}
{attributes}
{save}
onCode={(value) => {
dispatch('code', value);
}}
{onCode}
onSave={(value) => {
dispatch('update', {
onUpdate({
raw: token.raw,
oldContent: token.text,
newContent: value
@ -267,16 +267,7 @@
</div>
</Collapsible>
{:else if token.type === 'html'}
{@const html = DOMPurify.sanitize(token.text)}
{#if html && html.includes('<video')}
{@html html}
{:else if token.text.includes(`<iframe src="${WEBUI_BASE_URL}/api/v1/files/`)}
{@html `${token.text}`}
{:else if token.text.includes(`<source_id`)}
<Source {id} {token} onClick={onSourceClick} />
{:else}
{token.text}
{/if}
<HtmlToken {id} {token} {onSourceClick} />
{:else if token.type === 'iframe'}
<iframe
src="{WEBUI_BASE_URL}/api/v1/files/{token.fileId}/content"

View File

@ -200,9 +200,11 @@
await initHandler();
await tick();
const messageElement = document.getElementById(`message-${messageId}`);
if (messageElement) {
messageElement.scrollIntoView({ block: 'start' });
if ($settings?.scrollOnBranchChange ?? true) {
const messageElement = document.getElementById(`message-${messageId}`);
if (messageElement) {
messageElement.scrollIntoView({ block: 'start' });
}
}
});
</script>
@ -238,10 +240,9 @@
messageChildrenIds = history.messages[currentMessageId].childrenIds;
}
history.currentId = currentMessageId;
await tick();
await updateChat();
triggerScroll();
// await tick();
// await updateChat();
// triggerScroll();
}
}}
>
@ -293,7 +294,7 @@
<div class="w-full rounded-xl pl-5 pr-2 py-2">
<Name>
Merged Response
{$i18n.t('Merged Response')}
{#if message.timestamp}
<span

View File

@ -65,10 +65,12 @@
await tick();
messageEditTextAreaElement.style.height = '';
messageEditTextAreaElement.style.height = `${messageEditTextAreaElement.scrollHeight}px`;
if (messageEditTextAreaElement) {
messageEditTextAreaElement.style.height = '';
messageEditTextAreaElement.style.height = `${messageEditTextAreaElement.scrollHeight}px`;
messageEditTextAreaElement?.focus();
messageEditTextAreaElement?.focus();
}
};
const editMessageConfirmHandler = async (submit = true) => {

View File

@ -778,7 +778,7 @@
</div>
</button>
</div>
{:else if filteredItems.length === 0}
{:else}
<div class="mb-3"></div>
{/if}

View File

@ -159,15 +159,21 @@ Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
4. Notwithstanding any other provision of this License, and as a material condition of the rights granted herein, licensees are strictly prohibited from altering, removing, obscuring, or replacing any "Open WebUI" branding, including but not limited to the name, logo, or any visual, textual, or symbolic identifiers that distinguish the software and its interfaces, in any deployment or distribution, regardless of the number of users, except as explicitly set forth in Clauses 5 and 6 below.
5. The branding restriction enumerated in Clause 4 shall not apply in the following limited circumstances: (i) deployments or distributions where the total number of end users (defined as individual natural persons with direct access to the application) does not exceed fifty (50) within any rolling thirty (30) day period; (ii) cases in which the licensee is an official contributor to the codebase—with a substantive code change successfully merged into the main branch of the official codebase maintained by the copyright holder—who has obtained specific prior written permission for branding adjustment from the copyright holder; or (iii) where the licensee has obtained a duly executed enterprise license expressly permitting such modification. For all other cases, any removal or alteration of the "Open WebUI" branding shall constitute a material breach of license.
6. All code, modifications, or derivative works incorporated into this project prior to the incorporation of this branding clause remain licensed under the BSD 3-Clause License, and prior contributors retain all BSD-3 rights therein; if any such contributor requests the removal of their BSD-3-licensed code, the copyright holder will do so, and any replacement code will be licensed under the project's primary license then in effect. By contributing after this clause's adoption, you agree to the project's Contributor License Agreement (CLA) and to these updated terms for all new contributions.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE

View File

@ -87,7 +87,7 @@
</script>
<div class="flex flex-col h-full justify-between text-sm">
<div class=" space-y-3 overflow-y-scroll max-h-[28rem] lg:max-h-full">
<div class=" overflow-y-scroll max-h-[28rem] lg:max-h-full">
<input
id="profile-image-input"
bind:this={profileImageInputElement}
@ -236,7 +236,7 @@
<div class="flex-1">
<input
class="w-full text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden"
class="w-full text-sm dark:text-gray-300 bg-transparent outline-hidden"
type="text"
bind:value={name}
required
@ -265,98 +265,46 @@
{/if}
</div>
<div class="py-0.5">
<hr class="border-gray-50 dark:border-gray-850 my-2" />
<div class="my-2">
<UpdatePassword />
</div>
<hr class="border-gray-50 dark:border-gray-850 my-2" />
{#if ($config?.features?.enable_api_key ?? true) || $user?.role === 'admin'}
<div class="flex justify-between items-center text-sm mb-2">
<div class=" font-medium">{$i18n.t('API keys')}</div>
<button
class=" text-xs font-medium text-gray-500"
type="button"
on:click={() => {
showAPIKeys = !showAPIKeys;
}}>{showAPIKeys ? $i18n.t('Hide') : $i18n.t('Show')}</button
>
</div>
<div class="flex justify-between items-center text-sm">
<div class=" font-medium">{$i18n.t('API keys')}</div>
<button
class=" text-xs font-medium text-gray-500"
type="button"
on:click={() => {
showAPIKeys = !showAPIKeys;
}}>{showAPIKeys ? $i18n.t('Hide') : $i18n.t('Show')}</button
>
</div>
{#if showAPIKeys}
<div class="flex flex-col gap-4">
{#if $user?.role === 'admin'}
<div class="justify-between w-full">
<div class="flex justify-between w-full">
<div class="self-center text-xs font-medium mb-1">{$i18n.t('JWT Token')}</div>
</div>
{#if showAPIKeys}
<div class="flex flex-col gap-4">
<div class="justify-between w-full">
<div class="flex justify-between w-full">
<div class="self-center text-xs font-medium">{$i18n.t('JWT Token')}</div>
</div>
<div class="flex mt-2">
<SensitiveInput value={localStorage.token} readOnly={true} />
<button
class="ml-1.5 px-1.5 py-1 dark:hover:bg-gray-850 transition rounded-lg"
on:click={() => {
copyToClipboard(localStorage.token);
JWTTokenCopied = true;
setTimeout(() => {
JWTTokenCopied = false;
}, 2000);
}}
>
{#if JWTTokenCopied}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
clip-rule="evenodd"
/>
</svg>
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M11.986 3H12a2 2 0 0 1 2 2v6a2 2 0 0 1-1.5 1.937V7A2.5 2.5 0 0 0 10 4.5H4.063A2 2 0 0 1 6 3h.014A2.25 2.25 0 0 1 8.25 1h1.5a2.25 2.25 0 0 1 2.236 2ZM10.5 4v-.75a.75.75 0 0 0-.75-.75h-1.5a.75.75 0 0 0-.75.75V4h3Z"
clip-rule="evenodd"
/>
<path
fill-rule="evenodd"
d="M3 6a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1V7a1 1 0 0 0-1-1H3Zm1.75 2.5a.75.75 0 0 0 0 1.5h3.5a.75.75 0 0 0 0-1.5h-3.5ZM4 11.75a.75.75 0 0 1 .75-.75h3.5a.75.75 0 0 1 0 1.5h-3.5a.75.75 0 0 1-.75-.75Z"
clip-rule="evenodd"
/>
</svg>
{/if}
</button>
</div>
</div>
{#if $config?.features?.enable_api_key ?? true}
<div class="justify-between w-full">
<div class="flex justify-between w-full">
<div class="self-center text-xs font-medium">{$i18n.t('API Key')}</div>
</div>
<div class="flex mt-2">
{#if APIKey}
<SensitiveInput value={APIKey} readOnly={true} />
<div class="flex">
<SensitiveInput value={localStorage.token} readOnly={true} />
<button
class="ml-1.5 px-1.5 py-1 dark:hover:bg-gray-850 transition rounded-lg"
on:click={() => {
copyToClipboard(APIKey);
APIKeyCopied = true;
copyToClipboard(localStorage.token);
JWTTokenCopied = true;
setTimeout(() => {
APIKeyCopied = false;
JWTTokenCopied = false;
}, 2000);
}}
>
{#if APIKeyCopied}
{#if JWTTokenCopied}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
@ -389,46 +337,105 @@
</svg>
{/if}
</button>
</div>
</div>
{/if}
{#if $config?.features?.enable_api_key ?? true}
<div class="justify-between w-full">
{#if $user?.role === 'admin'}
<div class="flex justify-between w-full">
<div class="self-center text-xs font-medium mb-1">{$i18n.t('API Key')}</div>
</div>
{/if}
<div class="flex">
{#if APIKey}
<SensitiveInput value={APIKey} readOnly={true} />
<Tooltip content={$i18n.t('Create new key')}>
<button
class=" px-1.5 py-1 dark:hover:bg-gray-850transition rounded-lg"
class="ml-1.5 px-1.5 py-1 dark:hover:bg-gray-850 transition rounded-lg"
on:click={() => {
copyToClipboard(APIKey);
APIKeyCopied = true;
setTimeout(() => {
APIKeyCopied = false;
}, 2000);
}}
>
{#if APIKeyCopied}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
clip-rule="evenodd"
/>
</svg>
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M11.986 3H12a2 2 0 0 1 2 2v6a2 2 0 0 1-1.5 1.937V7A2.5 2.5 0 0 0 10 4.5H4.063A2 2 0 0 1 6 3h.014A2.25 2.25 0 0 1 8.25 1h1.5a2.25 2.25 0 0 1 2.236 2ZM10.5 4v-.75a.75.75 0 0 0-.75-.75h-1.5a.75.75 0 0 0-.75.75V4h3Z"
clip-rule="evenodd"
/>
<path
fill-rule="evenodd"
d="M3 6a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1V7a1 1 0 0 0-1-1H3Zm1.75 2.5a.75.75 0 0 0 0 1.5h3.5a.75.75 0 0 0 0-1.5h-3.5ZM4 11.75a.75.75 0 0 1 .75-.75h3.5a.75.75 0 0 1 0 1.5h-3.5a.75.75 0 0 1-.75-.75Z"
clip-rule="evenodd"
/>
</svg>
{/if}
</button>
<Tooltip content={$i18n.t('Create new key')}>
<button
class=" px-1.5 py-1 dark:hover:bg-gray-850transition rounded-lg"
on:click={() => {
createAPIKeyHandler();
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="size-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99"
/>
</svg>
</button>
</Tooltip>
{:else}
<button
class="flex gap-1.5 items-center font-medium px-3.5 py-1.5 rounded-lg bg-gray-100/70 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-850 transition"
on:click={() => {
createAPIKeyHandler();
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="size-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99"
/>
</svg>
</button>
</Tooltip>
{:else}
<button
class="flex gap-1.5 items-center font-medium px-3.5 py-1.5 rounded-lg bg-gray-100/70 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-850 transition"
on:click={() => {
createAPIKeyHandler();
}}
>
<Plus strokeWidth="2" className=" size-3.5" />
<Plus strokeWidth="2" className=" size-3.5" />
{$i18n.t('Create new secret key')}</button
>
{/if}
{$i18n.t('Create new secret key')}</button
>
{/if}
</div>
</div>
</div>
{/if}
</div>
{/if}
</div>
{/if}
{/if}
</div>

View File

@ -140,28 +140,31 @@
</div>
<div class=" self-center text-sm font-medium">{$i18n.t('Import Chats')}</div>
</button>
<button
class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
on:click={() => {
exportChats();
}}
>
<div class=" self-center mr-3">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 3.5a.75.75 0 0 1 .75.75v2.69l.72-.72a.75.75 0 1 1 1.06 1.06l-2 2a.75.75 0 0 1-1.06 0l-2-2a.75.75 0 0 1 1.06-1.06l.72.72V6.25A.75.75 0 0 1 8 5.5Z"
clip-rule="evenodd"
/>
</svg>
</div>
<div class=" self-center text-sm font-medium">{$i18n.t('Export Chats')}</div>
</button>
{#if $user?.role === 'admin' || ($user.permissions?.chat?.export ?? true)}
<button
class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
on:click={() => {
exportChats();
}}
>
<div class=" self-center mr-3">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 3.5a.75.75 0 0 1 .75.75v2.69l.72-.72a.75.75 0 1 1 1.06 1.06l-2 2a.75.75 0 0 1-1.06 0l-2-2a.75.75 0 0 1 1.06-1.06l.72.72V6.25A.75.75 0 0 1 8 5.5Z"
clip-rule="evenodd"
/>
</svg>
</div>
<div class=" self-center text-sm font-medium">{$i18n.t('Export Chats')}</div>
</button>
{/if}
</div>
<hr class=" border-gray-100 dark:border-gray-850" />

View File

@ -1,6 +1,4 @@
<script lang="ts">
import { getBackendConfig } from '$lib/apis';
import { setDefaultPromptSuggestions } from '$lib/apis/configs';
import { config, models, settings, user } from '$lib/stores';
import { createEventDispatcher, onMount, getContext } from 'svelte';
import { toast } from 'svelte-sonner';
@ -30,7 +28,9 @@
// Interface
let defaultModelId = '';
let showUsername = false;
let notificationSound = true;
let notificationSoundAlways = false;
let detectArtifacts = true;
@ -117,6 +117,11 @@
saveSettings({ notificationSound: notificationSound });
};
const toggleNotificationSoundAlways = async () => {
notificationSoundAlways = !notificationSoundAlways;
saveSettings({ notificationSoundAlways: notificationSoundAlways });
};
const toggleShowChangelog = async () => {
showChangelog = !showChangelog;
saveSettings({ showChangelog: showChangelog });
@ -294,7 +299,8 @@
chatDirection = $settings.chatDirection ?? 'auto';
userLocation = $settings.userLocation ?? false;
notificationSound = $settings.notificationSound ?? true;
notificationSound = $settings?.notificationSound ?? true;
notificationSoundAlways = $settings?.notificationSoundAlways ?? false;
hapticFeedback = $settings.hapticFeedback ?? false;
ctrlEnterToSend = $settings.ctrlEnterToSend ?? false;
@ -477,6 +483,30 @@
</div>
</div>
{#if notificationSound}
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs">
{$i18n.t('Always Play Notification Sound')}
</div>
<button
class="p-1 px-3 text-xs flex rounded-sm transition"
on:click={() => {
toggleNotificationSoundAlways();
}}
type="button"
>
{#if notificationSoundAlways === true}
<span class="ml-2 self-center">{$i18n.t('On')}</span>
{:else}
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
{/if}
</button>
</div>
</div>
{/if}
{#if $user?.role === 'admin'}
<div>
<div class=" py-0.5 flex w-full justify-between">
@ -855,7 +885,7 @@
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs">
{$i18n.t('Scroll to bottom when switching between branches')}
{$i18n.t('Scroll On Branch Change')}
</div>
<button

View File

@ -1,6 +1,4 @@
<script lang="ts">
import { getBackendConfig } from '$lib/apis';
import { setDefaultPromptSuggestions } from '$lib/apis/configs';
import Switch from '$lib/components/common/Switch.svelte';
import { config, models, settings, user } from '$lib/stores';
import { createEventDispatcher, onMount, getContext, tick } from 'svelte';

View File

@ -2,7 +2,7 @@
import { decode } from 'html-entities';
import { v4 as uuidv4 } from 'uuid';
import { getContext, createEventDispatcher } from 'svelte';
import { getContext } from 'svelte';
const i18n = getContext('i18n');
import dayjs from '$lib/dayjs';
@ -26,9 +26,6 @@
// Assuming $i18n.languages is an array of language codes
$: loadLocale($i18n.languages);
const dispatch = createEventDispatcher();
$: dispatch('change', open);
import { slide } from 'svelte/transition';
import { quintOut } from 'svelte/easing';
@ -55,6 +52,10 @@
export let disabled = false;
export let hide = false;
export let onChange: Function = () => {};
$: onChange(open);
const collapsibleId = uuidv4();
function parseJSONString(str) {

View File

@ -1,13 +1,12 @@
<script lang="ts">
import { onDestroy, onMount, createEventDispatcher } from 'svelte';
import { onDestroy, onMount } from 'svelte';
import { flyAndScale } from '$lib/utils/transitions';
import { fade, fly, slide } from 'svelte/transition';
import { isApp } from '$lib/stores';
const dispatch = createEventDispatcher();
export let show = false;
export let className = '';
export let onClose = () => {};
let modalElement = null;
let mounted = false;
@ -33,7 +32,7 @@
window.addEventListener('keydown', handleKeyDown);
document.body.style.overflow = 'hidden';
} else if (modalElement) {
dispatch('close');
onClose();
window.removeEventListener('keydown', handleKeyDown);
if (document.body.contains(modalElement)) {

View File

@ -121,8 +121,8 @@
bind:open
className="w-full "
buttonClassName="w-full"
on:change={(e) => {
dispatch('change', e.detail);
onChange={(state) => {
dispatch('change', state);
}}
>
<!-- svelte-ignore a11y-no-static-element-interactions -->

View File

@ -4,6 +4,7 @@
export let value = '';
export let placeholder = '';
export let rows = 1;
export let minSize = null;
export let required = false;
export let className =
'w-full rounded-lg px-3 py-2 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden h-full';
@ -29,7 +30,9 @@
const resize = () => {
if (textareaElement) {
textareaElement.style.height = '';
textareaElement.style.height = `${textareaElement.scrollHeight}px`;
textareaElement.style.height = minSize
? `${Math.max(textareaElement.scrollHeight, minSize)}px`
: `${textareaElement.scrollHeight}px`;
}
};
</script>

View File

@ -18,7 +18,8 @@
showArtifacts,
mobile,
temporaryChatEnabled,
theme
theme,
user
} from '$lib/stores';
import { flyAndScale } from '$lib/utils/transitions';
@ -212,7 +213,7 @@
</DropdownMenu.Item>
{/if}
{#if !$temporaryChatEnabled}
{#if !$temporaryChatEnabled && ($user?.role === 'admin' || ($user.permissions?.chat?.share ?? true))}
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
id="chat-share-button"
@ -288,14 +289,16 @@
transition={flyAndScale}
sideOffset={8}
>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
on:click={() => {
downloadJSONExport();
}}
>
<div class="flex items-center line-clamp-1">{$i18n.t('Export chat (.json)')}</div>
</DropdownMenu.Item>
{#if $user?.role === 'admin' || ($user.permissions?.chat?.export ?? true)}
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
on:click={() => {
downloadJSONExport();
}}
>
<div class="flex items-center line-clamp-1">{$i18n.t('Export chat (.json)')}</div>
</DropdownMenu.Item>
{/if}
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
on:click={() => {

View File

@ -609,6 +609,48 @@
</div>
{/if}
{#if $config?.features?.enable_notes ?? false}
<div class="px-1.5 flex justify-center text-gray-800 dark:text-gray-200">
<a
class="grow flex items-center space-x-3 rounded-lg px-2 py-[7px] hover:bg-gray-100 dark:hover:bg-gray-900 transition"
href="/notes"
on:click={() => {
selectedChatId = null;
chatId.set('');
if ($mobile) {
showSidebar.set(false);
}
}}
draggable="false"
>
<div class="self-center">
<svg
class="size-4"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="none"
viewBox="0 0 24 24"
>
<path
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 3v4a1 1 0 0 1-1 1H5m4 8h6m-6-4h6m4-8v16a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V7.914a1 1 0 0 1 .293-.707l3.914-3.914A1 1 0 0 1 9.914 3H18a1 1 0 0 1 1 1Z"
/>
</svg>
</div>
<div class="flex self-center translate-y-[0.5px]">
<div class=" self-center font-medium text-sm font-primary">{$i18n.t('Notes')}</div>
</div>
</a>
</div>
{/if}
<div class="relative {$temporaryChatEnabled ? 'opacity-20' : ''}">
{#if $temporaryChatEnabled}
<div class="absolute z-40 w-full h-full flex justify-center"></div>

View File

@ -26,7 +26,7 @@
getChatPinnedStatusById,
toggleChatPinnedStatusById
} from '$lib/apis/chats';
import { chats, theme } from '$lib/stores';
import { chats, theme, user } from '$lib/stores';
import { createMessagesList } from '$lib/utils';
import { downloadChatAsPDF } from '$lib/apis/utils';
import Download from '$lib/components/icons/Download.svelte';
@ -233,15 +233,17 @@
<div class="flex items-center">{$i18n.t('Archive')}</div>
</DropdownMenu.Item>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
on:click={() => {
shareHandler();
}}
>
<Share />
<div class="flex items-center">{$i18n.t('Share')}</div>
</DropdownMenu.Item>
{#if $user?.role === 'admin' || ($user.permissions?.chat?.share ?? true)}
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
on:click={() => {
shareHandler();
}}
>
<Share />
<div class="flex items-center">{$i18n.t('Share')}</div>
</DropdownMenu.Item>
{/if}
<DropdownMenu.Sub>
<DropdownMenu.SubTrigger
@ -256,14 +258,17 @@
transition={flyAndScale}
sideOffset={8}
>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
on:click={() => {
downloadJSONExport();
}}
>
<div class="flex items-center line-clamp-1">{$i18n.t('Export chat (.json)')}</div>
</DropdownMenu.Item>
{#if $user?.role === 'admin' || ($user.permissions?.chat?.export ?? true)}
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
on:click={() => {
downloadJSONExport();
}}
>
<div class="flex items-center line-clamp-1">{$i18n.t('Export chat (.json)')}</div>
</DropdownMenu.Item>
{/if}
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
on:click={() => {

View File

@ -252,7 +252,7 @@
const nameUpdateHandler = async () => {
if (name === '') {
toast.error($i18n.t('Folder name cannot be empty'));
toast.error($i18n.t('Folder name cannot be empty.'));
return;
}
@ -374,8 +374,8 @@
buttonClassName="w-full"
hide={(folders[folderId]?.childrenIds ?? []).length === 0 &&
(folders[folderId].items?.chats ?? []).length === 0}
on:change={(e) => {
dispatch('open', e.detail);
onChange={(state) => {
dispatch('open', state);
}}
>
<!-- svelte-ignore a11y-no-static-element-interactions -->

View File

@ -11,7 +11,12 @@
import { page } from '$app/stores';
import { mobile, showSidebar, knowledge as _knowledge, config, user } from '$lib/stores';
import { updateFileDataContentById, uploadFile, deleteFileById } from '$lib/apis/files';
import {
updateFileDataContentById,
uploadFile,
deleteFileById,
getFileById
} from '$lib/apis/files';
import {
addFileToKnowledgeById,
getKnowledgeById,
@ -84,12 +89,15 @@
let selectedFile = null;
let selectedFileId = null;
let selectedFileContent = '';
// Add cache object
let fileContentCache = new Map();
$: if (selectedFileId) {
const file = (knowledge?.files ?? []).find((file) => file.id === selectedFileId);
if (file) {
file.data = file.data ?? { content: '' };
selectedFile = file;
fileSelectHandler(file);
} else {
selectedFile = null;
}
@ -394,7 +402,10 @@
const updateFileContentHandler = async () => {
const fileId = selectedFile.id;
const content = selectedFile.data.content;
const content = selectedFileContent;
// Clear the cache for this file since we're updating it
fileContentCache.delete(fileId);
const res = updateFileDataContentById(localStorage.token, fileId, content).catch((e) => {
toast.error(`${e}`);
@ -450,6 +461,29 @@
}
};
const fileSelectHandler = async (file) => {
try {
selectedFile = file;
// Check cache first
if (fileContentCache.has(file.id)) {
selectedFileContent = fileContentCache.get(file.id);
return;
}
const response = await getFileById(localStorage.token, file.id);
if (response) {
selectedFileContent = response.data.content;
// Cache the content
fileContentCache.set(file.id, response.data.content);
} else {
toast.error($i18n.t('No content found in file.'));
}
} catch (e) {
toast.error($i18n.t('Failed to load file content.'));
}
};
const onDragOver = (e) => {
e.preventDefault();
@ -728,7 +762,7 @@
{#key selectedFile.id}
<RichTextInput
className="input-prose-sm"
bind:value={selectedFile.data.content}
bind:value={selectedFileContent}
placeholder={$i18n.t('Add content here')}
preserveBreaks={true}
/>
@ -747,7 +781,7 @@
<Drawer
className="h-full"
show={selectedFileId !== null}
on:close={() => {
onClose={() => {
selectedFileId = null;
}}
>
@ -786,7 +820,7 @@
{#key selectedFile.id}
<RichTextInput
className="input-prose-sm"
bind:value={selectedFile.data.content}
bind:value={selectedFileContent}
placeholder={$i18n.t('Add content here')}
preserveBreaks={true}
/>

View File

@ -56,6 +56,7 @@
}
}
let system = '';
let info = {
id: '',
base_model_id: null,
@ -155,6 +156,7 @@
}
}
info.params.system = system.trim() === '' ? null : system;
info.params.stop = params.stop ? params.stop.split(',').filter((s) => s.trim()) : null;
Object.keys(info.params).forEach((key) => {
if (info.params[key] === '' || info.params[key] === null) {
@ -201,6 +203,8 @@
}
}
system = model?.params?.system ?? '';
params = { ...params, ...model?.params };
params.stop = params?.stop
? (typeof params.stop === 'string' ? params.stop.split(',') : (params?.stop ?? [])).join(
@ -553,7 +557,7 @@
className=" text-sm w-full bg-transparent outline-hidden resize-none overflow-y-hidden "
placeholder={`Write your model system prompt content here\ne.g.) You are Mario from Super Mario Bros, acting as an assistant.`}
rows={4}
bind:value={info.params.system}
bind:value={system}
/>
</div>
</div>

View File

@ -8,6 +8,7 @@
import LockClosed from '$lib/components/icons/LockClosed.svelte';
import AccessControlModal from '../common/AccessControlModal.svelte';
import { user } from '$lib/stores';
import { slugify } from '$lib/utils';
export let onSubmit: Function;
export let edit = false;
@ -25,8 +26,15 @@
let showAccessControlModal = false;
$: if (!edit) {
command = title !== '' ? `${title.replace(/\s+/g, '-').toLowerCase()}` : '';
let hasManualEdit = false;
$: if (!edit && !hasManualEdit) {
command = title !== '' ? slugify(title) : '';
}
// Track manual edits
function handleCommandInput(e: Event) {
hasManualEdit = true;
}
const submitHandler = async () => {
@ -64,7 +72,7 @@
command = prompt.command.at(0) === '/' ? prompt.command.slice(1) : prompt.command;
content = prompt.content;
accessControl = prompt?.access_control ?? null;
accessControl = prompt?.access_control ?? {};
}
});
</script>
@ -125,6 +133,7 @@
class=" w-full bg-transparent outline-hidden"
placeholder={$i18n.t('Command')}
bind:value={command}
on:input={handleCommandInput}
required
disabled={edit}
/>

View File

@ -503,7 +503,7 @@
<ul class=" mt-1 list-disc pl-4 text-xs">
<li>
{$i18n.t('Tools have a function calling system that allows arbitrary code execution')}.
{$i18n.t('Tools have a function calling system that allows arbitrary code execution.')}.
</li>
<li>{$i18n.t('Do not install tools from sources you do not fully trust.')}</li>
</ul>

View File

@ -62,6 +62,8 @@
"Allow Chat Delete": "",
"Allow Chat Deletion": "يستطيع حذف المحادثات",
"Allow Chat Edit": "",
"Allow Chat Export": "",
"Allow Chat Share": "",
"Allow File Upload": "",
"Allow Multiple Models in Chat": "",
"Allow non-local voices": "",
@ -76,6 +78,7 @@
"Always": "",
"Always Collapse Code Blocks": "",
"Always Expand Details": "",
"Always Play Notification Sound": "",
"Amazing": "",
"an assistant": "مساعد",
"Analyzed": "",
@ -414,6 +417,10 @@
"Enter Document Intelligence Key": "",
"Enter domains separated by commas (e.g., example.com,site.org)": "",
"Enter Exa API Key": "",
"Enter External Web Loader API Key": "",
"Enter External Web Loader URL": "",
"Enter External Web Search API Key": "",
"Enter External Web Search URL": "",
"Enter Firecrawl API Base URL": "",
"Enter Firecrawl API Key": "",
"Enter Github Raw URL": "أدخل عنوان URL ل Github Raw",
@ -431,6 +438,7 @@
"Enter Model ID": "",
"Enter model tag (e.g. {{modelTag}})": "(e.g. {{modelTag}}) أدخل الموديل تاق",
"Enter Mojeek Search API Key": "",
"Enter New Password": "",
"Enter Number of Steps (e.g. 50)": "(e.g. 50) أدخل عدد الخطوات",
"Enter Perplexity API Key": "",
"Enter Playwright Timeout": "",
@ -467,11 +475,15 @@
"Enter Top K Reranker": "",
"Enter URL (e.g. http://127.0.0.1:7860/)": "الرابط (e.g. http://127.0.0.1:7860/)",
"Enter URL (e.g. http://localhost:11434)": "URL (e.g. http://localhost:11434)",
"Enter Yacy Password": "",
"Enter Yacy URL (e.g. http://yacy.example.com:8090)": "",
"Enter Yacy Username": "",
"Enter your current password": "",
"Enter Your Email": "أدخل البريد الاكتروني",
"Enter Your Full Name": "أدخل الاسم كامل",
"Enter your message": "",
"Enter your name": "",
"Enter Your Name": "",
"Enter your new password": "",
"Enter Your Password": "ادخل كلمة المرور",
"Enter Your Role": "أدخل الصلاحيات",
@ -511,10 +523,15 @@
"Export Tools": "",
"External": "",
"External Models": "",
"External Web Loader API Key": "",
"External Web Loader URL": "",
"External Web Search API Key": "",
"External Web Search URL": "",
"Failed to add file.": "",
"Failed to connect to {{URL}} OpenAPI tool server": "",
"Failed to create API Key.": "فشل في إنشاء مفتاح API.",
"Failed to fetch models": "",
"Failed to load file content.": "",
"Failed to read clipboard contents": "فشل في قراءة محتويات الحافظة",
"Failed to save connections": "",
"Failed to save models configuration": "",
@ -544,7 +561,6 @@
"Fluidly stream large external response chunks": "دفق قطع الاستجابة الخارجية الكبيرة بسلاسة",
"Focus chat input": "التركيز على إدخال الدردشة",
"Folder deleted successfully": "",
"Folder name cannot be empty": "",
"Folder name cannot be empty.": "",
"Folder name updated successfully": "",
"Followed instructions perfectly": "اتبعت التعليمات على أكمل وجه",
@ -565,7 +581,6 @@
"Function Name": "",
"Function updated successfully": "",
"Functions": "",
"Functions allow arbitrary code execution": "",
"Functions allow arbitrary code execution.": "",
"Functions imported successfully": "",
"Gemini": "",
@ -718,8 +733,12 @@
"Memory deleted successfully": "",
"Memory updated successfully": "",
"Merge Responses": "",
"Merged Response": "نتيجة الردود المدمجة",
"Message rating should be enabled to use this feature": "",
"Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "لن تتم مشاركة الرسائل التي ترسلها بعد إنشاء الرابط الخاص بك. سيتمكن المستخدمون الذين لديهم عنوان URL من عرض الدردشة المشتركة",
"Microsoft OneDrive": "",
"Microsoft OneDrive (personal)": "",
"Microsoft OneDrive (work/school)": "",
"Min P": "",
"Mirostat": "Mirostat",
"Mirostat Eta": "Mirostat Eta",
@ -761,11 +780,11 @@
"New Password": "كلمة المرور الجديدة",
"new-channel": "",
"No content found": "",
"No content found in file.": "",
"No content to speak": "",
"No distance available": "",
"No feedbacks found": "",
"No file selected": "",
"No files found.": "",
"No groups with access, add a group to grant access": "",
"No HTML, CSS, or JavaScript content found.": "",
"No inference engine with management support found": "",
@ -939,7 +958,7 @@
"Save Tag": "",
"Saved": "",
"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "لم يعد حفظ سجلات الدردشة مباشرة في مساحة تخزين متصفحك مدعومًا. يرجى تخصيص بعض الوقت لتنزيل وحذف سجلات الدردشة الخاصة بك عن طريق النقر على الزر أدناه. لا تقلق، يمكنك بسهولة إعادة استيراد سجلات الدردشة الخاصة بك إلى الواجهة الخلفية من خلاله",
"Scroll to bottom when switching between branches": "",
"Scroll On Branch Change": "",
"Search": "البحث",
"Search a model": "البحث عن موديل",
"Search Base": "",
@ -1140,7 +1159,6 @@
"Tools Access": "",
"Tools are a function calling system with arbitrary code execution": "",
"Tools Function Calling Prompt": "",
"Tools have a function calling system that allows arbitrary code execution": "",
"Tools have a function calling system that allows arbitrary code execution.": "",
"Tools Public Sharing": "",
"Top K": "Top K",
@ -1246,13 +1264,15 @@
"Write a summary in 50 words that summarizes [topic or keyword].": "اكتب ملخصًا في 50 كلمة يلخص [الموضوع أو الكلمة الرئيسية]",
"Write something...": "",
"Write your model template content here": "",
"Yacy Instance URL": "",
"Yacy Password": "",
"Yacy Username": "",
"Yesterday": "أمس",
"You": "انت",
"You are currently using a trial license. Please contact support to upgrade your license.": "",
"You can only chat with a maximum of {{maxCount}} file(s) at a time.": "",
"You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "",
"You cannot upload an empty file.": "",
"You do not have permission to upload files": "",
"You do not have permission to upload files.": "",
"You have no archived conversations.": "لا تملك محادثات محفوظه",
"You have shared this chat": "تم مشاركة هذه المحادثة",

View File

@ -62,6 +62,8 @@
"Allow Chat Delete": "السماح بحذف المحادثة",
"Allow Chat Deletion": "السماح بحذف المحادثة",
"Allow Chat Edit": "السماح بتعديل المحادثة",
"Allow Chat Export": "",
"Allow Chat Share": "",
"Allow File Upload": "السماح بتحميل الملفات",
"Allow Multiple Models in Chat": "",
"Allow non-local voices": "السماح بالأصوات غير المحلية",
@ -76,6 +78,7 @@
"Always": "دائمًا",
"Always Collapse Code Blocks": "",
"Always Expand Details": "",
"Always Play Notification Sound": "",
"Amazing": "رائع",
"an assistant": "مساعد",
"Analyzed": "تم التحليل",
@ -414,6 +417,10 @@
"Enter Document Intelligence Key": "أدخل مفتاح تحليل المستندات",
"Enter domains separated by commas (e.g., example.com,site.org)": "أدخل النطاقات مفصولة بفواصل (مثال: example.com,site.org)",
"Enter Exa API Key": "أدخل مفتاح API لـ Exa",
"Enter External Web Loader API Key": "",
"Enter External Web Loader URL": "",
"Enter External Web Search API Key": "",
"Enter External Web Search URL": "",
"Enter Firecrawl API Base URL": "",
"Enter Firecrawl API Key": "",
"Enter Github Raw URL": "أدخل عنوان URL ل Github Raw",
@ -431,6 +438,7 @@
"Enter Model ID": "أدخل معرف النموذج",
"Enter model tag (e.g. {{modelTag}})": "(e.g. {{modelTag}}) أدخل الموديل تاق",
"Enter Mojeek Search API Key": "أدخل مفتاح API لـ Mojeek Search",
"Enter New Password": "",
"Enter Number of Steps (e.g. 50)": "(e.g. 50) أدخل عدد الخطوات",
"Enter Perplexity API Key": "أدخل مفتاح API لـ Perplexity",
"Enter Playwright Timeout": "",
@ -467,11 +475,15 @@
"Enter Top K Reranker": "",
"Enter URL (e.g. http://127.0.0.1:7860/)": "الرابط (e.g. http://127.0.0.1:7860/)",
"Enter URL (e.g. http://localhost:11434)": "URL (e.g. http://localhost:11434)",
"Enter Yacy Password": "",
"Enter Yacy URL (e.g. http://yacy.example.com:8090)": "",
"Enter Yacy Username": "",
"Enter your current password": "أدخل كلمة المرور الحالية",
"Enter Your Email": "أدخل البريد الاكتروني",
"Enter Your Full Name": "أدخل الاسم كامل",
"Enter your message": "أدخل رسالتك",
"Enter your name": "",
"Enter Your Name": "",
"Enter your new password": "أدخل كلمة المرور الجديدة",
"Enter Your Password": "ادخل كلمة المرور",
"Enter Your Role": "أدخل الصلاحيات",
@ -511,10 +523,15 @@
"Export Tools": "تصدير الأدوات",
"External": "",
"External Models": "نماذج خارجية",
"External Web Loader API Key": "",
"External Web Loader URL": "",
"External Web Search API Key": "",
"External Web Search URL": "",
"Failed to add file.": "فشل في إضافة الملف.",
"Failed to connect to {{URL}} OpenAPI tool server": "",
"Failed to create API Key.": "فشل في إنشاء مفتاح API.",
"Failed to fetch models": "فشل في جلب النماذج",
"Failed to load file content.": "",
"Failed to read clipboard contents": "فشل في قراءة محتويات الحافظة",
"Failed to save connections": "",
"Failed to save models configuration": "فشل في حفظ إعدادات النماذج",
@ -544,7 +561,6 @@
"Fluidly stream large external response chunks": "دفق قطع الاستجابة الخارجية الكبيرة بسلاسة",
"Focus chat input": "التركيز على إدخال الدردشة",
"Folder deleted successfully": "تم حذف المجلد بنجاح",
"Folder name cannot be empty": "لا يمكن أن يكون اسم المجلد فارغًا",
"Folder name cannot be empty.": "لا يمكن أن يكون اسم المجلد فارغًا.",
"Folder name updated successfully": "تم تحديث اسم المجلد بنجاح",
"Followed instructions perfectly": "اتبعت التعليمات على أكمل وجه",
@ -565,7 +581,6 @@
"Function Name": "اسم الوظيفة",
"Function updated successfully": "تم تحديث الوظيفة بنجاح",
"Functions": "الوظائف",
"Functions allow arbitrary code execution": "الوظائف تتيح تنفيذ كود برمجي مخصص",
"Functions allow arbitrary code execution.": "الوظائف تتيح تنفيذ كود برمجي مخصص.",
"Functions imported successfully": "تم استيراد الوظائف بنجاح",
"Gemini": "Gemini",
@ -718,8 +733,12 @@
"Memory deleted successfully": "تم حذف الذاكرة بنجاح",
"Memory updated successfully": "تم تحديث الذاكرة بنجاح",
"Merge Responses": "دمج الردود",
"Merged Response": "نتيجة الردود المدمجة",
"Message rating should be enabled to use this feature": "يجب تفعيل تقييم الرسائل لاستخدام هذه الميزة",
"Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "لن تتم مشاركة الرسائل التي ترسلها بعد إنشاء الرابط الخاص بك. سيتمكن المستخدمون الذين لديهم عنوان URL من عرض الدردشة المشتركة",
"Microsoft OneDrive": "",
"Microsoft OneDrive (personal)": "",
"Microsoft OneDrive (work/school)": "",
"Min P": "الحد الأدنى P",
"Mirostat": "Mirostat",
"Mirostat Eta": "Mirostat Eta",
@ -761,11 +780,11 @@
"New Password": "كلمة المرور الجديدة",
"new-channel": "قناة جديدة",
"No content found": "لم يتم العثور على محتوى",
"No content found in file.": "",
"No content to speak": "لا يوجد محتوى للتحدث عنه",
"No distance available": "لا توجد مسافة متاحة",
"No feedbacks found": "لم يتم العثور على ملاحظات",
"No file selected": "لم يتم تحديد ملف",
"No files found.": "لم يتم العثور على ملفات.",
"No groups with access, add a group to grant access": "لا توجد مجموعات لها حق الوصول، أضف مجموعة لمنح الوصول",
"No HTML, CSS, or JavaScript content found.": "لم يتم العثور على محتوى HTML أو CSS أو JavaScript.",
"No inference engine with management support found": "لم يتم العثور على محرك استدلال يدعم الإدارة",
@ -939,7 +958,7 @@
"Save Tag": "حفظ الوسم",
"Saved": "تم الحفظ",
"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "لم يعد حفظ سجلات الدردشة مباشرة في مساحة تخزين متصفحك مدعومًا. يرجى تخصيص بعض الوقت لتنزيل وحذف سجلات الدردشة الخاصة بك عن طريق النقر على الزر أدناه. لا تقلق، يمكنك بسهولة إعادة استيراد سجلات الدردشة الخاصة بك إلى الواجهة الخلفية من خلاله",
"Scroll to bottom when switching between branches": "التمرير للأسفل عند التبديل بين الفروع",
"Scroll On Branch Change": "",
"Search": "البحث",
"Search a model": "البحث عن موديل",
"Search Base": "قاعدة البحث",
@ -1140,7 +1159,6 @@
"Tools Access": "الوصول إلى الأدوات",
"Tools are a function calling system with arbitrary code execution": "الأدوات عبارة عن نظام لاستدعاء الوظائف يسمح بتنفيذ كود برمجي مخصص",
"Tools Function Calling Prompt": "توجيه استدعاء وظائف الأدوات",
"Tools have a function calling system that allows arbitrary code execution": "تحتوي الأدوات على نظام لاستدعاء الوظائف يتيح تنفيذ كود برمجي مخصص",
"Tools have a function calling system that allows arbitrary code execution.": "تحتوي الأدوات على نظام لاستدعاء الوظائف يتيح تنفيذ كود برمجي مخصص.",
"Tools Public Sharing": "",
"Top K": "Top K",
@ -1246,13 +1264,15 @@
"Write a summary in 50 words that summarizes [topic or keyword].": "اكتب ملخصًا في 50 كلمة يلخص [الموضوع أو الكلمة الرئيسية]",
"Write something...": "اكتب شيئًا...",
"Write your model template content here": "اكتب هنا محتوى قالب النموذج الخاص بك",
"Yacy Instance URL": "",
"Yacy Password": "",
"Yacy Username": "",
"Yesterday": "أمس",
"You": "انت",
"You are currently using a trial license. Please contact support to upgrade your license.": "أنت تستخدم حالياً ترخيصًا تجريبيًا. يُرجى التواصل مع الدعم للترقية.",
"You can only chat with a maximum of {{maxCount}} file(s) at a time.": "يمكنك الدردشة مع {{maxCount}} ملف(ات) كحد أقصى في نفس الوقت.",
"You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "يمكنك تخصيص تفاعلك مع النماذج اللغوية عن طريق إضافة الذكريات باستخدام زر \"إدارة\" أدناه، مما يجعلها أكثر فائدة وتناسبًا لك.",
"You cannot upload an empty file.": "لا يمكنك رفع ملف فارغ.",
"You do not have permission to upload files": "ليس لديك صلاحية لرفع الملفات",
"You do not have permission to upload files.": "ليس لديك صلاحية لرفع الملفات.",
"You have no archived conversations.": "لا تملك محادثات محفوظه",
"You have shared this chat": "تم مشاركة هذه المحادثة",

View File

@ -62,6 +62,8 @@
"Allow Chat Delete": "Разреши изтриване на чат",
"Allow Chat Deletion": "Позволи Изтриване на Чат",
"Allow Chat Edit": "Разреши редактиране на чат",
"Allow Chat Export": "",
"Allow Chat Share": "",
"Allow File Upload": "Разреши качване на файлове",
"Allow Multiple Models in Chat": "",
"Allow non-local voices": "Разреши нелокални гласове",
@ -76,6 +78,7 @@
"Always": "Винаги",
"Always Collapse Code Blocks": "",
"Always Expand Details": "",
"Always Play Notification Sound": "",
"Amazing": "Невероятно",
"an assistant": "асистент",
"Analyzed": "Анализирано",
@ -414,6 +417,10 @@
"Enter Document Intelligence Key": "",
"Enter domains separated by commas (e.g., example.com,site.org)": "Въведете домейни, разделени със запетаи (напр. example.com,site.org)",
"Enter Exa API Key": "Въведете API ключ за Exa",
"Enter External Web Loader API Key": "",
"Enter External Web Loader URL": "",
"Enter External Web Search API Key": "",
"Enter External Web Search URL": "",
"Enter Firecrawl API Base URL": "",
"Enter Firecrawl API Key": "",
"Enter Github Raw URL": "Въведете URL адрес на Github Raw",
@ -431,6 +438,7 @@
"Enter Model ID": "Въведете ID на модела",
"Enter model tag (e.g. {{modelTag}})": "Въведете таг на модел (напр. {{modelTag}})",
"Enter Mojeek Search API Key": "Въведете API ключ за Mojeek Search",
"Enter New Password": "",
"Enter Number of Steps (e.g. 50)": "Въведете брой стъпки (напр. 50)",
"Enter Perplexity API Key": "",
"Enter Playwright Timeout": "",
@ -467,11 +475,15 @@
"Enter Top K Reranker": "",
"Enter URL (e.g. http://127.0.0.1:7860/)": "Въведете URL (напр. http://127.0.0.1:7860/)",
"Enter URL (e.g. http://localhost:11434)": "Въведете URL (напр. http://localhost:11434)",
"Enter Yacy Password": "",
"Enter Yacy URL (e.g. http://yacy.example.com:8090)": "",
"Enter Yacy Username": "",
"Enter your current password": "Въведете текущата си парола",
"Enter Your Email": "Въведете имейл",
"Enter Your Full Name": "Въведете вашето пълно име",
"Enter your message": "Въведете съобщението си",
"Enter your name": "",
"Enter Your Name": "",
"Enter your new password": "Въведете новата си парола",
"Enter Your Password": "Въведете вашата парола",
"Enter Your Role": "Въведете вашата роля",
@ -511,10 +523,15 @@
"Export Tools": "Експортиране на инструменти",
"External": "",
"External Models": "Външни модели",
"External Web Loader API Key": "",
"External Web Loader URL": "",
"External Web Search API Key": "",
"External Web Search URL": "",
"Failed to add file.": "Неуспешно добавяне на файл.",
"Failed to connect to {{URL}} OpenAPI tool server": "",
"Failed to create API Key.": "Неуспешно създаване на API ключ.",
"Failed to fetch models": "Неуспешно извличане на модели",
"Failed to load file content.": "",
"Failed to read clipboard contents": "Грешка при четене на съдържанието от клипборда",
"Failed to save connections": "",
"Failed to save models configuration": "Неуспешно запазване на конфигурацията на моделите",
@ -544,7 +561,6 @@
"Fluidly stream large external response chunks": "Плавно предаване на големи части от външен отговор",
"Focus chat input": "Фокусиране на чат вход",
"Folder deleted successfully": "Папката е изтрита успешно",
"Folder name cannot be empty": "Името на папката не може да бъде празно",
"Folder name cannot be empty.": "Името на папката не може да бъде празно.",
"Folder name updated successfully": "Името на папката е актуализирано успешно",
"Followed instructions perfectly": "Следвайте инструкциите перфектно",
@ -565,7 +581,6 @@
"Function Name": "Име на функцията",
"Function updated successfully": "Функцията е актуализирана успешно",
"Functions": "Функции",
"Functions allow arbitrary code execution": "Функциите позволяват произволно изпълнение на код",
"Functions allow arbitrary code execution.": "Функциите позволяват произволно изпълнение на код.",
"Functions imported successfully": "Функциите са импортирани успешно",
"Gemini": "",
@ -718,8 +733,12 @@
"Memory deleted successfully": "Паметта е изтрита успешно",
"Memory updated successfully": "Паметта е актуализирана успешно",
"Merge Responses": "Обединяване на отговори",
"Merged Response": "Обединен отговор",
"Message rating should be enabled to use this feature": "Оценяването на съобщения трябва да бъде активирано, за да използвате тази функция",
"Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "Съобщенията, които изпращате след създаването на връзката, няма да бъдат споделяни. Потребителите с URL адреса ще могат да видят споделения чат.",
"Microsoft OneDrive": "",
"Microsoft OneDrive (personal)": "",
"Microsoft OneDrive (work/school)": "",
"Min P": "Мин P",
"Mirostat": "Mirostat",
"Mirostat Eta": "Mirostat Eta",
@ -761,11 +780,11 @@
"New Password": "Нова парола",
"new-channel": "нов-канал",
"No content found": "Не е намерено съдържание",
"No content found in file.": "",
"No content to speak": "Няма съдържание за изговаряне",
"No distance available": "Няма налично разстояние",
"No feedbacks found": "Не са намерени обратни връзки",
"No file selected": "Не е избран файл",
"No files found.": "Не са намерени файлове.",
"No groups with access, add a group to grant access": "Няма групи с достъп, добавете група, за да предоставите достъп",
"No HTML, CSS, or JavaScript content found.": "Не е намерено HTML, CSS или JavaScript съдържание.",
"No inference engine with management support found": "Не е намерен механизъм за извод с поддръжка на управление",
@ -939,7 +958,7 @@
"Save Tag": "Запиши таг",
"Saved": "Запазено",
"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "Запазването на чат логове директно в хранилището на вашия браузър вече не се поддържа. Моля, отделете малко време, за да изтеглите и изтриете чат логовете си, като щракнете върху бутона по-долу. Не се притеснявайте, можете лесно да импортирате отново чат логовете си в бекенда чрез",
"Scroll to bottom when switching between branches": "Превъртане до дъното при превключване между клонове",
"Scroll On Branch Change": "",
"Search": "Търси",
"Search a model": "Търси модел",
"Search Base": "База за търсене",
@ -1140,7 +1159,6 @@
"Tools Access": "Достъп до инструменти",
"Tools are a function calling system with arbitrary code execution": "Инструментите са система за извикване на функции с произволно изпълнение на код",
"Tools Function Calling Prompt": "Промпт за извикване на функции на инструментите",
"Tools have a function calling system that allows arbitrary code execution": "Инструментите имат система за извикване на функции, която позволява произволно изпълнение на код",
"Tools have a function calling system that allows arbitrary code execution.": "Инструментите имат система за извикване на функции, която позволява произволно изпълнение на код.",
"Tools Public Sharing": "",
"Top K": "Топ K",
@ -1246,13 +1264,15 @@
"Write a summary in 50 words that summarizes [topic or keyword].": "Напиши описание в 50 думи, което обобщава [тема или ключова дума].",
"Write something...": "Напишете нещо...",
"Write your model template content here": "Напишете съдържанието на вашия шаблон за модел тук",
"Yacy Instance URL": "",
"Yacy Password": "",
"Yacy Username": "",
"Yesterday": "вчера",
"You": "Вие",
"You are currently using a trial license. Please contact support to upgrade your license.": "",
"You can only chat with a maximum of {{maxCount}} file(s) at a time.": "Можете да чатите с максимум {{maxCount}} файл(а) наведнъж.",
"You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "Можете да персонализирате взаимодействията си с LLM-и, като добавите спомени чрез бутона 'Управление' по-долу, правейки ги по-полезни и съобразени с вас.",
"You cannot upload an empty file.": "Не можете да качите празен файл.",
"You do not have permission to upload files": "Нямате разрешение да качвате файлове",
"You do not have permission to upload files.": "Нямате разрешение да качвате файлове.",
"You have no archived conversations.": "Нямате архивирани разговори.",
"You have shared this chat": "Вие сте споделили този чат",

View File

@ -62,6 +62,8 @@
"Allow Chat Delete": "",
"Allow Chat Deletion": "চ্যাট ডিলিট করতে দিন",
"Allow Chat Edit": "",
"Allow Chat Export": "",
"Allow Chat Share": "",
"Allow File Upload": "",
"Allow Multiple Models in Chat": "",
"Allow non-local voices": "",
@ -76,6 +78,7 @@
"Always": "",
"Always Collapse Code Blocks": "",
"Always Expand Details": "",
"Always Play Notification Sound": "",
"Amazing": "",
"an assistant": "একটা এসিস্ট্যান্ট",
"Analyzed": "",
@ -414,6 +417,10 @@
"Enter Document Intelligence Key": "",
"Enter domains separated by commas (e.g., example.com,site.org)": "",
"Enter Exa API Key": "",
"Enter External Web Loader API Key": "",
"Enter External Web Loader URL": "",
"Enter External Web Search API Key": "",
"Enter External Web Search URL": "",
"Enter Firecrawl API Base URL": "",
"Enter Firecrawl API Key": "",
"Enter Github Raw URL": "গিটহাব কাঁচা URL লিখুন",
@ -431,6 +438,7 @@
"Enter Model ID": "",
"Enter model tag (e.g. {{modelTag}})": "মডেল ট্যাগ লিখুন (e.g. {{modelTag}})",
"Enter Mojeek Search API Key": "",
"Enter New Password": "",
"Enter Number of Steps (e.g. 50)": "ধাপের সংখ্যা দিন (যেমন: 50)",
"Enter Perplexity API Key": "",
"Enter Playwright Timeout": "",
@ -467,11 +475,15 @@
"Enter Top K Reranker": "",
"Enter URL (e.g. http://127.0.0.1:7860/)": "ইউআরএল দিন (যেমন http://127.0.0.1:7860/)",
"Enter URL (e.g. http://localhost:11434)": "ইউআরএল দিন (যেমন http://localhost:11434)",
"Enter Yacy Password": "",
"Enter Yacy URL (e.g. http://yacy.example.com:8090)": "",
"Enter Yacy Username": "",
"Enter your current password": "",
"Enter Your Email": "আপনার ইমেইল লিখুন",
"Enter Your Full Name": "আপনার পূর্ণ নাম লিখুন",
"Enter your message": "",
"Enter your name": "",
"Enter Your Name": "",
"Enter your new password": "",
"Enter Your Password": "আপনার পাসওয়ার্ড লিখুন",
"Enter Your Role": "আপনার রোল লিখুন",
@ -511,10 +523,15 @@
"Export Tools": "",
"External": "",
"External Models": "",
"External Web Loader API Key": "",
"External Web Loader URL": "",
"External Web Search API Key": "",
"External Web Search URL": "",
"Failed to add file.": "",
"Failed to connect to {{URL}} OpenAPI tool server": "",
"Failed to create API Key.": "API Key তৈরি করা যায়নি।",
"Failed to fetch models": "",
"Failed to load file content.": "",
"Failed to read clipboard contents": "ক্লিপবোর্ডের বিষয়বস্তু পড়া সম্ভব হয়নি",
"Failed to save connections": "",
"Failed to save models configuration": "",
@ -544,7 +561,6 @@
"Fluidly stream large external response chunks": "বড় এক্সটার্নাল রেসপন্স চাঙ্কগুলো মসৃণভাবে প্রবাহিত করুন",
"Focus chat input": "চ্যাট ইনপুট ফোকাস করুন",
"Folder deleted successfully": "",
"Folder name cannot be empty": "",
"Folder name cannot be empty.": "",
"Folder name updated successfully": "",
"Followed instructions perfectly": "নির্দেশাবলী নিখুঁতভাবে অনুসরণ করা হয়েছে",
@ -565,7 +581,6 @@
"Function Name": "",
"Function updated successfully": "",
"Functions": "",
"Functions allow arbitrary code execution": "",
"Functions allow arbitrary code execution.": "",
"Functions imported successfully": "",
"Gemini": "",
@ -718,8 +733,12 @@
"Memory deleted successfully": "",
"Memory updated successfully": "",
"Merge Responses": "",
"Merged Response": "একত্রিত প্রতিক্রিয়া ফলাফল",
"Message rating should be enabled to use this feature": "",
"Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "আপনার লিঙ্ক তৈরি করার পরে আপনার পাঠানো বার্তাগুলি শেয়ার করা হবে না। ইউআরএল ব্যবহারকারীরা শেয়ার করা চ্যাট দেখতে পারবেন।",
"Microsoft OneDrive": "",
"Microsoft OneDrive (personal)": "",
"Microsoft OneDrive (work/school)": "",
"Min P": "",
"Mirostat": "Mirostat",
"Mirostat Eta": "Mirostat Eta",
@ -761,11 +780,11 @@
"New Password": "নতুন পাসওয়ার্ড",
"new-channel": "",
"No content found": "",
"No content found in file.": "",
"No content to speak": "",
"No distance available": "",
"No feedbacks found": "",
"No file selected": "",
"No files found.": "",
"No groups with access, add a group to grant access": "",
"No HTML, CSS, or JavaScript content found.": "",
"No inference engine with management support found": "",
@ -939,7 +958,7 @@
"Save Tag": "",
"Saved": "",
"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "মাধ্যমে",
"Scroll to bottom when switching between branches": "",
"Scroll On Branch Change": "",
"Search": "অনুসন্ধান",
"Search a model": "মডেল অনুসন্ধান করুন",
"Search Base": "",
@ -1140,7 +1159,6 @@
"Tools Access": "",
"Tools are a function calling system with arbitrary code execution": "",
"Tools Function Calling Prompt": "",
"Tools have a function calling system that allows arbitrary code execution": "",
"Tools have a function calling system that allows arbitrary code execution.": "",
"Tools Public Sharing": "",
"Top K": "Top K",
@ -1246,13 +1264,15 @@
"Write a summary in 50 words that summarizes [topic or keyword].": "৫০ শব্দের মধ্যে [topic or keyword] এর একটি সারসংক্ষেপ লিখুন।",
"Write something...": "",
"Write your model template content here": "",
"Yacy Instance URL": "",
"Yacy Password": "",
"Yacy Username": "",
"Yesterday": "আগামী",
"You": "আপনি",
"You are currently using a trial license. Please contact support to upgrade your license.": "",
"You can only chat with a maximum of {{maxCount}} file(s) at a time.": "",
"You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "",
"You cannot upload an empty file.": "",
"You do not have permission to upload files": "",
"You do not have permission to upload files.": "",
"You have no archived conversations.": "আপনার কোনও আর্কাইভ করা কথোপকথন নেই।",
"You have shared this chat": "আপনি এই চ্যাটটি শেয়ার করেছেন",

View File

@ -62,6 +62,8 @@
"Allow Chat Delete": "ཁ་བརྡ་བསུབ་པར་གནང་བ་སྤྲོད་པ།",
"Allow Chat Deletion": "ཁ་བརྡ་བསུབ་པར་གནང་བ་སྤྲོད་པ།",
"Allow Chat Edit": "ཁ་བརྡ་ཞུ་དག་ལ་གནང་བ་སྤྲོད་པ།",
"Allow Chat Export": "",
"Allow Chat Share": "",
"Allow File Upload": "ཡིག་ཆ་སྤར་བར་གནང་བ་སྤྲོད་པ།",
"Allow Multiple Models in Chat": "",
"Allow non-local voices": "ས་གནས་མིན་པའི་སྐད་གདངས་ལ་གནང་བ་སྤྲོད་པ།",
@ -76,6 +78,7 @@
"Always": "རྟག་ཏུ།",
"Always Collapse Code Blocks": "རྟག་ཏུ་ཀོཌ་གཏོགས་ཁོངས་བསྐུམ་པ།",
"Always Expand Details": "རྟག་ཏུ་ཞིབ་ཕྲ་རྒྱ་བསྐྱེད་པ།",
"Always Play Notification Sound": "",
"Amazing": "ངོ་མཚར་ཆེན།",
"an assistant": "ལག་རོགས་པ།",
"Analyzed": "དབྱེ་ཞིབ་བྱས་པ།",
@ -414,6 +417,10 @@
"Enter Document Intelligence Key": "ཡིག་ཆའི་རིག་ནུས་ལྡེ་མིག་འཇུག་པ།",
"Enter domains separated by commas (e.g., example.com,site.org)": "ཚེག་བསྐུངས་ཀྱིས་ལོགས་སུ་བཀར་བའི་ཁྱབ་ཁོངས་འཇུག་པ། (དཔེར་ན། example.com,site.org)",
"Enter Exa API Key": "Exa API ལྡེ་མིག་འཇུག་པ།",
"Enter External Web Loader API Key": "",
"Enter External Web Loader URL": "",
"Enter External Web Search API Key": "",
"Enter External Web Search URL": "",
"Enter Firecrawl API Base URL": "",
"Enter Firecrawl API Key": "",
"Enter Github Raw URL": "Github Raw URL འཇུག་པ།",
@ -431,6 +438,7 @@
"Enter Model ID": "དཔེ་དབྱིབས་ཀྱི་ ID འཇུག་པ།",
"Enter model tag (e.g. {{modelTag}})": "དཔེ་དབྱིབས་ཀྱི་རྟགས་འཇུག་པ། (དཔེར་ན། {{modelTag}})",
"Enter Mojeek Search API Key": "Mojeek Search API ལྡེ་མིག་འཇུག་པ།",
"Enter New Password": "",
"Enter Number of Steps (e.g. 50)": "གོམ་གྲངས་འཇུག་པ། (དཔེར་ན། ༥༠)",
"Enter Perplexity API Key": "Perplexity API ལྡེ་མིག་འཇུག་པ།",
"Enter Playwright Timeout": "",
@ -467,11 +475,15 @@
"Enter Top K Reranker": "Top K Reranker འཇུག་པ།",
"Enter URL (e.g. http://127.0.0.1:7860/)": "URL འཇུག་པ། (དཔེར་ན། http://127.0.0.1:7860/)",
"Enter URL (e.g. http://localhost:11434)": "URL འཇུག་པ། (དཔེར་ན། http://localhost:11434)",
"Enter Yacy Password": "",
"Enter Yacy URL (e.g. http://yacy.example.com:8090)": "",
"Enter Yacy Username": "",
"Enter your current password": "ཁྱེད་ཀྱི་ད་ལྟའི་གསང་གྲངས་འཇུག་པ།",
"Enter Your Email": "ཁྱེད་ཀྱི་ཡིག་ཟམ་འཇུག་པ།",
"Enter Your Full Name": "ཁྱེད་ཀྱི་མིང་ཆ་ཚང་འཇུག་པ།",
"Enter your message": "ཁྱེད་ཀྱི་འཕྲིན་འཇུག་པ།",
"Enter your name": "",
"Enter Your Name": "",
"Enter your new password": "ཁྱེད་ཀྱི་གསང་གྲངས་གསར་པ་འཇུག་པ།",
"Enter Your Password": "ཁྱེད་ཀྱི་གསང་གྲངས་འཇུག་པ།",
"Enter Your Role": "ཁྱེད་ཀྱི་གནས་ཚད་འཇུག་པ།",
@ -511,10 +523,15 @@
"Export Tools": "ལག་ཆ་ཕྱིར་གཏོང་།",
"External": "ཕྱི་རོལ།",
"External Models": "ཕྱི་རོལ་གྱི་དཔེ་དབྱིབས།",
"External Web Loader API Key": "",
"External Web Loader URL": "",
"External Web Search API Key": "",
"External Web Search URL": "",
"Failed to add file.": "ཡིག་ཆ་སྣོན་པར་མ་ཐུབ།",
"Failed to connect to {{URL}} OpenAPI tool server": "{{URL}} OpenAPI ལག་ཆའི་སར་བར་ལ་སྦྲེལ་མཐུད་བྱེད་མ་ཐུབ།",
"Failed to create API Key.": "API ལྡེ་མིག་བཟོ་མ་ཐུབ།",
"Failed to fetch models": "དཔེ་དབྱིབས་ལེན་པར་མ་ཐུབ།",
"Failed to load file content.": "",
"Failed to read clipboard contents": "སྦྱར་སྡེར་གྱི་ནང་དོན་ཀློག་མ་ཐུབ།",
"Failed to save connections": "",
"Failed to save models configuration": "དཔེ་དབྱིབས་སྒྲིག་འགོད་ཉར་ཚགས་བྱེད་མ་ཐུབ།",
@ -544,7 +561,6 @@
"Fluidly stream large external response chunks": "ཕྱི་རོལ་གྱི་ལན་གྱི་དུམ་བུ་ཆེན་པོ་རྒྱུན་བཞིན་རྒྱུག་པ།",
"Focus chat input": "ཁ་བརྡའི་ནང་འཇུག་ལ་དམིགས་པ།",
"Folder deleted successfully": "ཡིག་སྣོད་ལེགས་པར་བསུབས་ཟིན།",
"Folder name cannot be empty": "ཡིག་སྣོད་ཀྱི་མིང་སྟོང་པ་ཡིན་མི་ཆོག",
"Folder name cannot be empty.": "ཡིག་སྣོད་ཀྱི་མིང་སྟོང་པ་ཡིན་མི་ཆོག",
"Folder name updated successfully": "ཡིག་སྣོད་ཀྱི་མིང་ལེགས་པར་གསར་སྒྱུར་བྱས་ཟིན།",
"Followed instructions perfectly": "ལམ་སྟོན་ཡང་དག་པར་བསྒྲུབས།",
@ -565,7 +581,6 @@
"Function Name": "ལས་འགན་གྱི་མིང་།",
"Function updated successfully": "ལས་འགན་ལེགས་པར་གསར་སྒྱུར་བྱས་ཟིན།",
"Functions": "ལས་འགན།",
"Functions allow arbitrary code execution": "ལས་འགན་གྱིས་གང་འདོད་ཀྱི་ཀོཌ་ལག་བསྟར་ལ་གནང་བ་སྤྲོད།",
"Functions allow arbitrary code execution.": "ལས་འགན་གྱིས་གང་འདོད་ཀྱི་ཀོཌ་ལག་བསྟར་ལ་གནང་བ་སྤྲོད།",
"Functions imported successfully": "ལས་འགན་ལེགས་པར་ནང་འདྲེན་བྱས།",
"Gemini": "Gemini",
@ -718,8 +733,12 @@
"Memory deleted successfully": "དྲན་ཤེས་ལེགས་པར་བསུབས་ཟིན།",
"Memory updated successfully": "དྲན་ཤེས་ལེགས་པར་གསར་སྒྱུར་བྱས་ཟིན།",
"Merge Responses": "ལན་ཟླ་སྒྲིལ།",
"Merged Response": "བསྡུར་མཐུན་གྱི་ལན་གསལ་གནས་ཡོད།",
"Message rating should be enabled to use this feature": "ཁྱད་ཆོས་འདི་བེད་སྤྱོད་གཏོང་བར་འཕྲིན་ལ་སྐར་མ་སྤྲོད་པ་སྒུལ་བསྐྱོད་བྱེད་དགོས།",
"Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "ཁྱེད་ཀྱི་སྦྲེལ་ཐག་བཟོས་རྗེས་ཁྱེད་ཀྱིས་བསྐུར་བའི་འཕྲིན་དག་མཉམ་སྤྱོད་བྱེད་མི་འགྱུར། URL ཡོད་པའི་བེད་སྤྱོད་མཁན་ཚོས་མཉམ་སྤྱོད་ཁ་བརྡ་ལྟ་ཐུབ་ངེས།",
"Microsoft OneDrive": "",
"Microsoft OneDrive (personal)": "",
"Microsoft OneDrive (work/school)": "",
"Min P": "P ཉུང་ཤོས།",
"Mirostat": "Mirostat",
"Mirostat Eta": "Mirostat Eta",
@ -761,11 +780,11 @@
"New Password": "གསང་གྲངས་གསར་པ།",
"new-channel": "བགྲོ་གླེང་གསར་པ།",
"No content found": "ནང་དོན་མ་རྙེད།",
"No content found in file.": "",
"No content to speak": "བཤད་རྒྱུའི་ནང་དོན་མེད།",
"No distance available": "ཐག་རིང་ཚད་མེད།",
"No feedbacks found": "བསམ་འཆར་མ་རྙེད།",
"No file selected": "ཡིག་ཆ་གདམ་ག་མ་བྱས།",
"No files found.": "ཡིག་ཆ་མ་རྙེད།",
"No groups with access, add a group to grant access": "འཛུལ་སྤྱོད་ཡོད་པའི་ཚོགས་པ་མེད། འཛུལ་སྤྱོད་སྤྲོད་པར་ཚོགས་པ་ཞིག་སྣོན་པ།",
"No HTML, CSS, or JavaScript content found.": "HTML, CSS, ཡང་ན་ JavaScript གི་ནང་དོན་མ་རྙེད།",
"No inference engine with management support found": "དོ་དམ་རྒྱབ་སྐྱོར་ཡོད་པའི་དཔོག་རྩིས་འཕྲུལ་འཁོར་མ་རྙེད།",
@ -939,7 +958,7 @@
"Save Tag": "རྟགས་ཉར་ཚགས།",
"Saved": "ཉར་ཚགས་བྱས།",
"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "ཁ་བརྡའི་ཟིན་ཐོ་ཐད་ཀར་ཁྱེད་ཀྱི་བརྡ་འཚོལ་ཆས་ཀྱི་གསོག་ཆས་སུ་ཉར་ཚགས་བྱེད་པར་ད་ནས་བཟུང་རྒྱབ་སྐྱོར་མེད། གཤམ་གྱི་མཐེབ་གནོན་མནན་ནས་ཁྱེད་ཀྱི་ཁ་བརྡའི་ཟིན་ཐོ་ཕབ་ལེན་དང་བསུབ་པར་དུས་ཚོད་ཅུང་ཟད་བླང་རོགས། སེམས་ཁྲལ་མེད། ཁྱེད་ཀྱིས་སྟབས་བདེ་པོར་ཁྱེད་ཀྱི་ཁ་བརྡའི་ཟིན་ཐོ་རྒྱབ་སྣེ་ལ་བསྐྱར་དུ་ནང་འདྲེན་བྱེད་ཐུབ།",
"Scroll to bottom when switching between branches": "ཡན་ལག་བརྗེ་སྐབས་མཐིལ་དུ་འགྲིལ་བ།",
"Scroll On Branch Change": "",
"Search": "འཚོལ་བཤེར།",
"Search a model": "དཔེ་དབྱིབས་ཤིག་འཚོལ་བ།",
"Search Base": "འཚོལ་བཤེར་གཞི་རྩ།",
@ -1140,7 +1159,6 @@
"Tools Access": "ལག་ཆར་འཛུལ་སྤྱོད།",
"Tools are a function calling system with arbitrary code execution": "ལག་ཆ་ནི་གང་འདོད་ཀྱི་ཀོཌ་ལག་བསྟར་ཡོད་པའི་ལས་འགན་འབོད་པའི་མ་ལག་ཅིག་ཡིན།",
"Tools Function Calling Prompt": "ལག་ཆ་ལས་འགན་འབོད་པའི་འགུལ་སློང་།",
"Tools have a function calling system that allows arbitrary code execution": "ལག་ཆར་གང་འདོད་ཀྱི་ཀོཌ་ལག་བསྟར་ལ་གནང་བ་སྤྲོད་པའི་ལས་འགན་འབོད་པའི་མ་ལག་ཡོད།",
"Tools have a function calling system that allows arbitrary code execution.": "ལག་ཆར་གང་འདོད་ཀྱི་ཀོཌ་ལག་བསྟར་ལ་གནང་བ་སྤྲོད་པའི་ལས་འགན་འབོད་པའི་མ་ལག་ཡོད།",
"Tools Public Sharing": "ལག་ཆ་སྤྱི་སྤྱོད་མཉམ་སྤྱོད།",
"Top K": "Top K",
@ -1246,13 +1264,15 @@
"Write a summary in 50 words that summarizes [topic or keyword].": "[བརྗོད་གཞི་ཡང་ན་གནད་ཚིག] ཕྱོགས་སྡོམ་བྱེད་པའི་ཚིག་ ༥༠ ནང་གི་སྙིང་བསྡུས་ཤིག་འབྲི་བ།",
"Write something...": "ཅི་ཞིག་འབྲི་བ།...",
"Write your model template content here": "ཁྱེད་ཀྱི་དཔེ་དབྱིབས་མ་དཔེའི་ནང་དོན་འདིར་འབྲི་བ།",
"Yacy Instance URL": "",
"Yacy Password": "",
"Yacy Username": "",
"Yesterday": "ཁ་ས།",
"You": "ཁྱེད།",
"You are currently using a trial license. Please contact support to upgrade your license.": "ཁྱེད་ཀྱིས་ད་ལྟ་ཚོད་ལྟའི་ཆོག་མཆན་ཞིག་བེད་སྤྱོད་གཏོང་བཞིན་འདུག ཁྱེད་ཀྱི་ཆོག་མཆན་རིམ་སྤོར་བྱེད་པར་རོགས་སྐྱོར་དང་འབྲེལ་གཏུག་བྱེད་རོགས།",
"You can only chat with a maximum of {{maxCount}} file(s) at a time.": "ཁྱེད་ཀྱིས་ཐེངས་གཅིག་ལ་ཡིག་ཆ་ {{maxCount}} ལས་མང་བ་དང་ཁ་བརྡ་བྱེད་མི་ཐུབ།",
"You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "ཁྱེད་ཀྱིས་གཤམ་གྱི་ 'དོ་དམ་' མཐེབ་གནོན་བརྒྱུད་དྲན་ཤེས་སྣོན་ནས་ LLMs དང་མཉམ་དུ་འབྲེལ་འདྲིས་བྱེད་པ་སྒེར་སྤྱོད་ཅན་བཟོ་ཐུབ། དེ་དག་ཁྱེད་ལ་སྔར་ལས་ཕན་ཐོགས་པ་དང་འཚམ་པོ་བཟོ་ཐུབ།",
"You cannot upload an empty file.": "ཁྱེད་ཀྱིས་ཡིག་ཆ་སྟོང་པ་སྤར་མི་ཐུབ།",
"You do not have permission to upload files": "ཁྱེད་ལ་ཡིག་ཆ་སྤར་བའི་དབང་ཚད་མེད།",
"You do not have permission to upload files.": "ཁྱེད་ལ་ཡིག་ཆ་སྤར་བའི་དབང་ཚད་མེད།",
"You have no archived conversations.": "ཁྱེད་ལ་ཡིག་མཛོད་དུ་བཞག་པའི་ཁ་བརྡ་མེད།",
"You have shared this chat": "ཁྱེད་ཀྱིས་ཁ་བརྡ་འདི་མཉམ་སྤྱོད་བྱས་ཡོད།",

View File

@ -57,17 +57,19 @@
"All": "Tots",
"All Documents": "Tots els documents",
"All models deleted successfully": "Tots els models s'han eliminat correctament",
"Allow Call": "",
"Allow Call": "Permetre la trucada",
"Allow Chat Controls": "Permetre els controls de xat",
"Allow Chat Delete": "Permetre eliminar el xat",
"Allow Chat Deletion": "Permetre la supressió del xat",
"Allow Chat Edit": "Permetre editar el xat",
"Allow Chat Export": "Permetre exportar el xat",
"Allow Chat Share": "Permetre compartir el xat",
"Allow File Upload": "Permetre la pujada d'arxius",
"Allow Multiple Models in Chat": "",
"Allow Multiple Models in Chat": "Permetre múltiple models al xat",
"Allow non-local voices": "Permetre veus no locals",
"Allow Speech to Text": "",
"Allow Speech to Text": "Permetre Parla a Text",
"Allow Temporary Chat": "Permetre el xat temporal",
"Allow Text to Speech": "",
"Allow Text to Speech": "Permetre Text a Parla",
"Allow User Location": "Permetre la ubicació de l'usuari",
"Allow Voice Interruption in Call": "Permetre la interrupció de la veu en una trucada",
"Allowed Endpoints": "Punts d'accés permesos",
@ -76,6 +78,7 @@
"Always": "Sempre",
"Always Collapse Code Blocks": "Reduir sempre els blocs de codi",
"Always Expand Details": "Expandir sempre els detalls",
"Always Play Notification Sound": "Reproduir sempre un so de notificació",
"Amazing": "Al·lucinant",
"an assistant": "un assistent",
"Analyzed": "Analitzat",
@ -83,7 +86,7 @@
"and": "i",
"and {{COUNT}} more": "i {{COUNT}} més",
"and create a new shared link.": "i crear un nou enllaç compartit.",
"Android": "",
"Android": "Android",
"API Base URL": "URL Base de l'API",
"API Key": "clau API",
"API Key created.": "clau API creada.",
@ -245,7 +248,7 @@
"Copied shared chat URL to clipboard!": "S'ha copiat l'URL compartida al porta-retalls!",
"Copied to clipboard": "Copiat al porta-retalls",
"Copy": "Copiar",
"Copy Formatted Text": "",
"Copy Formatted Text": "Copiar el text formatat",
"Copy last code block": "Copiar l'últim bloc de codi",
"Copy last response": "Copiar l'última resposta",
"Copy Link": "Copiar l'enllaç",
@ -308,7 +311,7 @@
"Deleted User": "Usuari eliminat",
"Describe your knowledge base and objectives": "Descriu la teva base de coneixement i objectius",
"Description": "Descripció",
"Detect Artifacts Automatically": "",
"Detect Artifacts Automatically": "Detectar automàticament els artefactes",
"Didn't fully follow instructions": "No s'han seguit les instruccions completament",
"Direct": "Directe",
"Direct Connections": "Connexions directes",
@ -364,7 +367,7 @@
"e.g. my_filter": "p. ex. els_meus_filtres",
"e.g. my_tools": "p. ex. les_meves_eines",
"e.g. Tools for performing various operations": "p. ex. Eines per dur a terme operacions",
"e.g., en-US,ja-JP (leave blank for auto-detect)": "",
"e.g., en-US,ja-JP (leave blank for auto-detect)": "p. ex. en-US, ja-JP, ca-ES (deixa-ho en blanc per detecció automàtica)",
"Edit": "Editar",
"Edit Arena Model": "Editar model de l'Arena",
"Edit Channel": "Editar el canal",
@ -414,8 +417,12 @@
"Enter Document Intelligence Key": "Introdueix la clau de Document Intelligence",
"Enter domains separated by commas (e.g., example.com,site.org)": "Introdueix els dominis separats per comes (p. ex. example.com,site.org)",
"Enter Exa API Key": "Introdueix la clau API de d'EXA",
"Enter Firecrawl API Base URL": "",
"Enter Firecrawl API Key": "",
"Enter External Web Loader API Key": "Introdueix la clau API d'External Web Loader",
"Enter External Web Loader URL": "Introdueix la URL d'External Web Loader",
"Enter External Web Search API Key": "Introdueix la clau API d'External Web Search",
"Enter External Web Search URL": "Introdueix la URL d'External Web Search",
"Enter Firecrawl API Base URL": "Introdueix la URL base de Firecrawl API",
"Enter Firecrawl API Key": "Introdueix la clau API de Firecrawl",
"Enter Github Raw URL": "Introdueix l'URL en brut de Github",
"Enter Google PSE API Key": "Introdueix la clau API de Google PSE",
"Enter Google PSE Engine Id": "Introdueix l'identificador del motor PSE de Google",
@ -431,10 +438,11 @@
"Enter Model ID": "Introdueix l'identificador del model",
"Enter model tag (e.g. {{modelTag}})": "Introdueix l'etiqueta del model (p. ex. {{modelTag}})",
"Enter Mojeek Search API Key": "Introdueix la clau API de Mojeek Search",
"Enter New Password": "",
"Enter Number of Steps (e.g. 50)": "Introdueix el nombre de passos (p. ex. 50)",
"Enter Perplexity API Key": "Introdueix la clau API de Perplexity",
"Enter Playwright Timeout": "",
"Enter Playwright WebSocket URL": "",
"Enter Playwright Timeout": "Introdueix el timeout de Playwright",
"Enter Playwright WebSocket URL": "Introdueix la URL de Playwright WebSocket",
"Enter proxy URL (e.g. https://user:password@host:port)": "Entra l'URL (p. ex. https://user:password@host:port)",
"Enter reasoning effort": "Introdueix l'esforç de raonament",
"Enter Sampler (e.g. Euler a)": "Introdueix el mostrejador (p.ex. Euler a)",
@ -452,13 +460,13 @@
"Enter server host": "Introdueix el servidor",
"Enter server label": "Introdueix l'etiqueta del servidor",
"Enter server port": "Introdueix el port del servidor",
"Enter Sougou Search API sID": "",
"Enter Sougou Search API SK": "",
"Enter Sougou Search API sID": "Introdueix el sID de l'API de Sougou Search",
"Enter Sougou Search API SK": "Introdueix l'SK de l'API de Sougou Search",
"Enter stop sequence": "Introdueix la seqüència de parada",
"Enter system prompt": "Introdueix la indicació de sistema",
"Enter system prompt here": "Entra la indicació de sistema aquí",
"Enter Tavily API Key": "Introdueix la clau API de Tavily",
"Enter Tavily Extract Depth": "",
"Enter Tavily Extract Depth": "Introdueix la profunditat d'extracció de Tavily",
"Enter the public URL of your WebUI. This URL will be used to generate links in the notifications.": "Entra la URL pública de WebUI. Aquesta URL s'utilitzarà per generar els enllaços en les notificacions.",
"Enter Tika Server URL": "Introdueix l'URL del servidor Tika",
"Enter timeout in seconds": "Entra el temps màxim en segons",
@ -467,11 +475,15 @@
"Enter Top K Reranker": "Introdueix el Top K Reranker",
"Enter URL (e.g. http://127.0.0.1:7860/)": "Introdueix l'URL (p. ex. http://127.0.0.1:7860/)",
"Enter URL (e.g. http://localhost:11434)": "Introdueix l'URL (p. ex. http://localhost:11434)",
"Enter Yacy Password": "",
"Enter Yacy URL (e.g. http://yacy.example.com:8090)": "",
"Enter Yacy Username": "",
"Enter your current password": "Introdueix la teva contrasenya actual",
"Enter Your Email": "Introdueix el teu correu electrònic",
"Enter Your Full Name": "Introdueix el teu nom complet",
"Enter your message": "Introdueix el teu missatge",
"Enter your name": "Entra el teu nom",
"Enter Your Name": "",
"Enter your new password": "Introdueix la teva nova contrasenya",
"Enter Your Password": "Introdueix la teva contrasenya",
"Enter Your Role": "Introdueix el teu rol",
@ -511,10 +523,15 @@
"Export Tools": "Exportar les eines",
"External": "Extern",
"External Models": "Models externs",
"External Web Loader API Key": "Clau API d'External Web Loader",
"External Web Loader URL": "URL d'External Web Loader",
"External Web Search API Key": "Clau API d'External Web Search",
"External Web Search URL": "URL d'External Web Search",
"Failed to add file.": "No s'ha pogut afegir l'arxiu.",
"Failed to connect to {{URL}} OpenAPI tool server": "No s'ha pogut connecta al servidor d'eines OpenAPI {{URL}}",
"Failed to create API Key.": "No s'ha pogut crear la clau API.",
"Failed to fetch models": "No s'han pogut obtenir els models",
"Failed to load file content.": "No s'ha pogut carregar el contingut del fitxer",
"Failed to read clipboard contents": "No s'ha pogut llegir el contingut del porta-retalls",
"Failed to save connections": "No s'han pogut desar les connexions",
"Failed to save models configuration": "No s'ha pogut desar la configuració dels models",
@ -539,12 +556,11 @@
"Filter is now globally enabled": "El filtre ha estat activat globalment",
"Filters": "Filtres",
"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "S'ha detectat la suplantació d'identitat de l'empremta digital: no es poden utilitzar les inicials com a avatar. S'estableix la imatge de perfil predeterminada.",
"Firecrawl API Base URL": "",
"Firecrawl API Key": "",
"Firecrawl API Base URL": "URL de l'API de base de Firecrawl",
"Firecrawl API Key": "Clau API de Firecrawl",
"Fluidly stream large external response chunks": "Transmetre amb fluïdesa grans trossos de resposta externa",
"Focus chat input": "Estableix el focus a l'entrada del xat",
"Folder deleted successfully": "Carpeta eliminada correctament",
"Folder name cannot be empty": "El nom de la carpeta no pot ser buit",
"Folder name cannot be empty.": "El nom de la carpeta no pot ser buit.",
"Folder name updated successfully": "Nom de la carpeta actualitzat correctament",
"Followed instructions perfectly": "S'han seguit les instruccions perfectament",
@ -565,7 +581,6 @@
"Function Name": "Nom de la funció",
"Function updated successfully": "La funció s'ha actualitzat correctament",
"Functions": "Funcions",
"Functions allow arbitrary code execution": "Les funcions permeten l'execució de codi arbitrari",
"Functions allow arbitrary code execution.": "Les funcions permeten l'execució de codi arbitrari.",
"Functions imported successfully": "Les funcions s'han importat correctament",
"Gemini": "Gemini",
@ -605,8 +620,8 @@
"Hybrid Search": "Cerca híbrida",
"I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.": "Afirmo que he llegit i entenc les implicacions de la meva acció. Soc conscient dels riscos associats a l'execució de codi arbitrari i he verificat la fiabilitat de la font.",
"ID": "ID",
"iframe Sandbox Allow Forms": "",
"iframe Sandbox Allow Same Origin": "",
"iframe Sandbox Allow Forms": "Permetre formularis sandbox iframe",
"iframe Sandbox Allow Same Origin": "Permetre same-origin sandbox iframe",
"Ignite curiosity": "Despertar la curiositat",
"Image": "Imatge",
"Image Compression": "Compressió d'imatges",
@ -667,7 +682,7 @@
"Label": "Etiqueta",
"Landing Page Mode": "Mode de la pàgina d'entrada",
"Language": "Idioma",
"Language Locales": "",
"Language Locales": "Localització d'idiomes",
"Last Active": "Activitat recent",
"Last Modified": "Modificació",
"Last reply": "Darrera resposta",
@ -718,8 +733,12 @@
"Memory deleted successfully": "Memòria eliminada correctament",
"Memory updated successfully": "Memòria actualitzada correctament",
"Merge Responses": "Fusionar les respostes",
"Merged Response": "Resposta combinada",
"Message rating should be enabled to use this feature": "La classificació dels missatges s'hauria d'activar per utilitzar aquesta funció",
"Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "Els missatges enviats després de crear el teu enllaç no es compartiran. Els usuaris amb l'URL podran veure el xat compartit.",
"Microsoft OneDrive": "Microsoft OneDrive",
"Microsoft OneDrive (personal)": "Microsoft OneDrive (personal)",
"Microsoft OneDrive (work/school)": "Microsoft OneDrive (feina/escola)",
"Min P": "Min P",
"Mirostat": "Mirostat",
"Mirostat Eta": "Eta de Mirostat",
@ -761,11 +780,11 @@
"New Password": "Nova contrasenya",
"new-channel": "nou-canal",
"No content found": "No s'ha trobat contingut",
"No content found in file.": "No s'ha trobat contingut en el fitxer.",
"No content to speak": "No hi ha contingut per parlar",
"No distance available": "No hi ha distància disponible",
"No feedbacks found": "No s'han trobat comentaris",
"No file selected": "No s'ha escollit cap fitxer",
"No files found.": "No s'han trobat arxius.",
"No groups with access, add a group to grant access": "No hi ha cap grup amb accés, afegeix un grup per concedir accés",
"No HTML, CSS, or JavaScript content found.": "No s'ha trobat contingut HTML, CSS o JavaScript.",
"No inference engine with management support found": "No s'ha trobat un motor d'inferència amb suport de gestió",
@ -851,8 +870,8 @@
"Pipelines Valves": "Vàlvules de les Pipelines",
"Plain text (.txt)": "Text pla (.txt)",
"Playground": "Zona de jocs",
"Playwright Timeout (ms)": "",
"Playwright WebSocket URL": "",
"Playwright Timeout (ms)": "Temps d'espera (ms) de Playwright",
"Playwright WebSocket URL": "URL del WebSocket de Playwright",
"Please carefully review the following warnings:": "Si us plau, revisa els següents avisos amb cura:",
"Please do not close the settings page while loading the model.": "No tanquis la pàgina de configuració mentre carregues el model.",
"Please enter a prompt": "Si us plau, entra una indicació",
@ -898,11 +917,11 @@
"References from": "Referències de",
"Refused when it shouldn't have": "Refusat quan no hauria d'haver estat",
"Regenerate": "Regenerar",
"Reindex": "",
"Reindex Knowledge Base Vectors": "",
"Reindex": "Reindexar",
"Reindex Knowledge Base Vectors": "Reindexar els vector base del Coneixement",
"Release Notes": "Notes de la versió",
"Relevance": "Rellevància",
"Relevance Threshold": "",
"Relevance Threshold": "Límit de rellevància",
"Remove": "Eliminar",
"Remove Model": "Eliminar el model",
"Rename": "Canviar el nom",
@ -939,7 +958,7 @@
"Save Tag": "Desar l'etiqueta",
"Saved": "Desat",
"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "Desar els registres de xat directament a l'emmagatzematge del teu navegador ja no està suportat. Si us plau, descarregr i elimina els registres de xat fent clic al botó de sota. No et preocupis, pots tornar a importar fàcilment els teus registres de xat al backend a través de",
"Scroll to bottom when switching between branches": "Desplaçar a la part inferior quan es canviï de branca",
"Scroll On Branch Change": "Fer scroll en canviar de branca",
"Search": "Cercar",
"Search a model": "Cercar un model",
"Search Base": "Base de cerca",
@ -1032,8 +1051,8 @@
"Sign up to {{WEBUI_NAME}}": "Registrar-se a {{WEBUI_NAME}}",
"Signing in to {{WEBUI_NAME}}": "Iniciant sessió a {{WEBUI_NAME}}",
"sk-1234": "sk-1234",
"Sougou Search API sID": "",
"Sougou Search API SK": "",
"Sougou Search API sID": "sID de l'API de Sougou Search",
"Sougou Search API SK": "SK de l'API de Sougou Search",
"Source": "Font",
"Speech Playback Speed": "Velocitat de la parla",
"Speech recognition error: {{error}}": "Error de reconeixement de veu: {{error}}",
@ -1061,7 +1080,7 @@
"Tap to interrupt": "Prem per interrompre",
"Tasks": "Tasques",
"Tavily API Key": "Clau API de Tavily",
"Tavily Extract Depth": "",
"Tavily Extract Depth": "Profunditat d'extracció de Tavily",
"Tell us more:": "Dona'ns més informació:",
"Temperature": "Temperatura",
"Template": "Plantilla",
@ -1140,7 +1159,6 @@
"Tools Access": "Accés a les eines",
"Tools are a function calling system with arbitrary code execution": "Les eines són un sistema de crida a funcions amb execució de codi arbitrari",
"Tools Function Calling Prompt": "Indicació per a la crida de funcions",
"Tools have a function calling system that allows arbitrary code execution": "Les eines disposen d'un sistema de crida a funcions que permet execució de codi arbitrari",
"Tools have a function calling system that allows arbitrary code execution.": "Les eines disposen d'un sistema de crida a funcions que permet execució de codi arbitrari.",
"Tools Public Sharing": "Compartició pública d'eines",
"Top K": "Top K",
@ -1184,8 +1202,8 @@
"Use Gravatar": "Utilitzar Gravatar",
"Use groups to group your users and assign permissions.": "Utilitza grups per agrupar els usuaris i assignar permisos.",
"Use Initials": "Utilitzar inicials",
"Use no proxy to fetch page contents.": "",
"Use proxy designated by http_proxy and https_proxy environment variables to fetch page contents.": "",
"Use no proxy to fetch page contents.": "No utilitzis un proxy per obtenir contingut de la pàgina.",
"Use proxy designated by http_proxy and https_proxy environment variables to fetch page contents.": "Utilitza el proxy designat per les variables d'entorn http_proxy i https_proxy per obtenir el contingut de la pàgina.",
"use_mlock (Ollama)": "use_mlock (Ollama)",
"use_mmap (Ollama)": "use_mmap (Ollama)",
"user": "usuari",
@ -1203,7 +1221,7 @@
"variable": "variable",
"variable to have them replaced with clipboard content.": "variable per tenir-les reemplaçades amb el contingut del porta-retalls.",
"Verify Connection": "Verificar la connexió",
"Verify SSL Certificate": "",
"Verify SSL Certificate": "Verificar el certificat SSL",
"Version": "Versió",
"Version {{selectedVersion}} of {{totalVersions}}": "Versió {{selectedVersion}} de {{totalVersions}}",
"View Replies": "Veure les respostes",
@ -1218,7 +1236,7 @@
"Warning: Jupyter execution enables arbitrary code execution, posing severe security risks—proceed with extreme caution.": "Avís: l'execució de Jupyter permet l'execució de codi arbitrari, la qual cosa comporta greus riscos de seguretat; procediu amb extrema precaució.",
"Web": "Web",
"Web API": "Web API",
"Web Loader Engine": "",
"Web Loader Engine": "Motor de càrrega Web",
"Web Search": "Cerca la web",
"Web Search Engine": "Motor de cerca de la web",
"Web Search in Chat": "Cerca a internet al xat",
@ -1246,13 +1264,15 @@
"Write a summary in 50 words that summarizes [topic or keyword].": "Escriu un resum en 50 paraules que resumeixi [tema o paraula clau].",
"Write something...": "Escriu quelcom...",
"Write your model template content here": "Introdueix el contingut de la plantilla del teu model aquí",
"Yacy Instance URL": "",
"Yacy Password": "",
"Yacy Username": "",
"Yesterday": "Ahir",
"You": "Tu",
"You are currently using a trial license. Please contact support to upgrade your license.": "Actualment esteu utilitzant una llicència de prova. Poseu-vos en contacte amb el servei d'assistència per actualitzar la vostra llicència.",
"You can only chat with a maximum of {{maxCount}} file(s) at a time.": "Només pots xatejar amb un màxim de {{maxCount}} fitxers alhora.",
"You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "Pots personalitzar les teves interaccions amb els models de llenguatge afegint memòries mitjançant el botó 'Gestiona' que hi ha a continuació, fent-les més útils i adaptades a tu.",
"You cannot upload an empty file.": "No es pot pujar un ariux buit.",
"You do not have permission to upload files": "No tens permisos per pujar arxius",
"You do not have permission to upload files.": "No tens permisos per pujar arxius.",
"You have no archived conversations.": "No tens converses arxivades.",
"You have shared this chat": "Has compartit aquest xat",

View File

@ -62,6 +62,8 @@
"Allow Chat Delete": "",
"Allow Chat Deletion": "Tugoti nga mapapas ang mga chat",
"Allow Chat Edit": "",
"Allow Chat Export": "",
"Allow Chat Share": "",
"Allow File Upload": "",
"Allow Multiple Models in Chat": "",
"Allow non-local voices": "",
@ -76,6 +78,7 @@
"Always": "",
"Always Collapse Code Blocks": "",
"Always Expand Details": "",
"Always Play Notification Sound": "",
"Amazing": "",
"an assistant": "usa ka katabang",
"Analyzed": "",
@ -414,6 +417,10 @@
"Enter Document Intelligence Key": "",
"Enter domains separated by commas (e.g., example.com,site.org)": "",
"Enter Exa API Key": "",
"Enter External Web Loader API Key": "",
"Enter External Web Loader URL": "",
"Enter External Web Search API Key": "",
"Enter External Web Search URL": "",
"Enter Firecrawl API Base URL": "",
"Enter Firecrawl API Key": "",
"Enter Github Raw URL": "",
@ -431,6 +438,7 @@
"Enter Model ID": "",
"Enter model tag (e.g. {{modelTag}})": "Pagsulod sa template tag (e.g. {{modelTag}})",
"Enter Mojeek Search API Key": "",
"Enter New Password": "",
"Enter Number of Steps (e.g. 50)": "Pagsulod sa gidaghanon sa mga lakang (e.g. 50)",
"Enter Perplexity API Key": "",
"Enter Playwright Timeout": "",
@ -467,11 +475,15 @@
"Enter Top K Reranker": "",
"Enter URL (e.g. http://127.0.0.1:7860/)": "Pagsulod sa URL (e.g. http://127.0.0.1:7860/)",
"Enter URL (e.g. http://localhost:11434)": "",
"Enter Yacy Password": "",
"Enter Yacy URL (e.g. http://yacy.example.com:8090)": "",
"Enter Yacy Username": "",
"Enter your current password": "",
"Enter Your Email": "Pagsulod sa imong e-mail address",
"Enter Your Full Name": "Ibutang ang imong tibuok nga ngalan",
"Enter your message": "",
"Enter your name": "",
"Enter Your Name": "",
"Enter your new password": "",
"Enter Your Password": "Ibutang ang imong password",
"Enter Your Role": "",
@ -511,10 +523,15 @@
"Export Tools": "",
"External": "",
"External Models": "",
"External Web Loader API Key": "",
"External Web Loader URL": "",
"External Web Search API Key": "",
"External Web Search URL": "",
"Failed to add file.": "",
"Failed to connect to {{URL}} OpenAPI tool server": "",
"Failed to create API Key.": "",
"Failed to fetch models": "",
"Failed to load file content.": "",
"Failed to read clipboard contents": "Napakyas sa pagbasa sa sulod sa clipboard",
"Failed to save connections": "",
"Failed to save models configuration": "",
@ -544,7 +561,6 @@
"Fluidly stream large external response chunks": "Hapsay nga paghatud sa daghang mga tipik sa eksternal nga mga tubag",
"Focus chat input": "Pag-focus sa entry sa diskusyon",
"Folder deleted successfully": "",
"Folder name cannot be empty": "",
"Folder name cannot be empty.": "",
"Folder name updated successfully": "",
"Followed instructions perfectly": "",
@ -565,7 +581,6 @@
"Function Name": "",
"Function updated successfully": "",
"Functions": "",
"Functions allow arbitrary code execution": "",
"Functions allow arbitrary code execution.": "",
"Functions imported successfully": "",
"Gemini": "",
@ -718,8 +733,12 @@
"Memory deleted successfully": "",
"Memory updated successfully": "",
"Merge Responses": "",
"Merged Response": "Gihiusa nga Resulta sa Tubag",
"Message rating should be enabled to use this feature": "",
"Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "",
"Microsoft OneDrive": "",
"Microsoft OneDrive (personal)": "",
"Microsoft OneDrive (work/school)": "",
"Min P": "",
"Mirostat": "Mirostat",
"Mirostat Eta": "Mirostat Eta",
@ -761,11 +780,11 @@
"New Password": "Bag-ong Password",
"new-channel": "",
"No content found": "",
"No content found in file.": "",
"No content to speak": "",
"No distance available": "",
"No feedbacks found": "",
"No file selected": "",
"No files found.": "",
"No groups with access, add a group to grant access": "",
"No HTML, CSS, or JavaScript content found.": "",
"No inference engine with management support found": "",
@ -939,7 +958,7 @@
"Save Tag": "",
"Saved": "",
"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "Ang pag-save sa mga chat log direkta sa imong browser storage dili na suportado. ",
"Scroll to bottom when switching between branches": "",
"Scroll On Branch Change": "",
"Search": "Pagpanukiduki",
"Search a model": "",
"Search Base": "",
@ -1140,7 +1159,6 @@
"Tools Access": "",
"Tools are a function calling system with arbitrary code execution": "",
"Tools Function Calling Prompt": "",
"Tools have a function calling system that allows arbitrary code execution": "",
"Tools have a function calling system that allows arbitrary code execution.": "",
"Tools Public Sharing": "",
"Top K": "Top K",
@ -1246,13 +1264,15 @@
"Write a summary in 50 words that summarizes [topic or keyword].": "Pagsulat og 50 ka pulong nga summary nga nagsumaryo [topic o keyword].",
"Write something...": "",
"Write your model template content here": "",
"Yacy Instance URL": "",
"Yacy Password": "",
"Yacy Username": "",
"Yesterday": "",
"You": "",
"You are currently using a trial license. Please contact support to upgrade your license.": "",
"You can only chat with a maximum of {{maxCount}} file(s) at a time.": "",
"You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "",
"You cannot upload an empty file.": "",
"You do not have permission to upload files": "",
"You do not have permission to upload files.": "",
"You have no archived conversations.": "",
"You have shared this chat": "",

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