Merge remote-tracking branch 'upstream/dev' into update-IT-translation

This commit is contained in:
Dave 2025-05-18 04:46:41 +02:00
commit ebf2e4141e
158 changed files with 5013 additions and 1983 deletions

View File

@ -26,6 +26,9 @@ ARG BUILD_HASH
WORKDIR /app WORKDIR /app
# to store git revision in build
RUN apk add --no-cache git
COPY package.json package-lock.json ./ COPY package.json package-lock.json ./
RUN npm ci RUN npm ci

View File

@ -989,6 +989,26 @@ DEFAULT_USER_ROLE = PersistentConfig(
os.getenv("DEFAULT_USER_ROLE", "pending"), os.getenv("DEFAULT_USER_ROLE", "pending"),
) )
PENDING_USER_OVERLAY_TITLE = PersistentConfig(
"PENDING_USER_OVERLAY_TITLE",
"ui.pending_user_overlay_title",
os.environ.get("PENDING_USER_OVERLAY_TITLE", ""),
)
PENDING_USER_OVERLAY_CONTENT = PersistentConfig(
"PENDING_USER_OVERLAY_CONTENT",
"ui.pending_user_overlay_content",
os.environ.get("PENDING_USER_OVERLAY_CONTENT", ""),
)
RESPONSE_WATERMARK = PersistentConfig(
"RESPONSE_WATERMARK",
"ui.watermark",
os.environ.get("RESPONSE_WATERMARK", ""),
)
USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS = ( USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS = (
os.environ.get("USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS", "False").lower() os.environ.get("USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS", "False").lower()
== "true" == "true"
@ -1731,6 +1751,9 @@ QDRANT_API_KEY = os.environ.get("QDRANT_API_KEY", None)
QDRANT_ON_DISK = os.environ.get("QDRANT_ON_DISK", "false").lower() == "true" 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_PREFER_GRPC = os.environ.get("QDRANT_PREFER_GRPC", "False").lower() == "true"
QDRANT_GRPC_PORT = int(os.environ.get("QDRANT_GRPC_PORT", "6334")) QDRANT_GRPC_PORT = int(os.environ.get("QDRANT_GRPC_PORT", "6334"))
ENABLE_QDRANT_MULTITENANCY_MODE = (
os.environ.get("ENABLE_QDRANT_MULTITENANCY_MODE", "false").lower() == "true"
)
# OpenSearch # OpenSearch
OPENSEARCH_URI = os.environ.get("OPENSEARCH_URI", "https://localhost:9200") OPENSEARCH_URI = os.environ.get("OPENSEARCH_URI", "https://localhost:9200")
@ -1825,6 +1848,18 @@ CONTENT_EXTRACTION_ENGINE = PersistentConfig(
os.environ.get("CONTENT_EXTRACTION_ENGINE", "").lower(), os.environ.get("CONTENT_EXTRACTION_ENGINE", "").lower(),
) )
EXTERNAL_DOCUMENT_LOADER_URL = PersistentConfig(
"EXTERNAL_DOCUMENT_LOADER_URL",
"rag.external_document_loader_url",
os.environ.get("EXTERNAL_DOCUMENT_LOADER_URL", ""),
)
EXTERNAL_DOCUMENT_LOADER_API_KEY = PersistentConfig(
"EXTERNAL_DOCUMENT_LOADER_API_KEY",
"rag.external_document_loader_api_key",
os.environ.get("EXTERNAL_DOCUMENT_LOADER_API_KEY", ""),
)
TIKA_SERVER_URL = PersistentConfig( TIKA_SERVER_URL = PersistentConfig(
"TIKA_SERVER_URL", "TIKA_SERVER_URL",
"rag.tika_server_url", "rag.tika_server_url",
@ -1849,6 +1884,12 @@ DOCLING_OCR_LANG = PersistentConfig(
os.getenv("DOCLING_OCR_LANG", "eng,fra,deu,spa"), os.getenv("DOCLING_OCR_LANG", "eng,fra,deu,spa"),
) )
DOCLING_DO_PICTURE_DESCRIPTION = PersistentConfig(
"DOCLING_DO_PICTURE_DESCRIPTION",
"rag.docling_do_picture_description",
os.getenv("DOCLING_DO_PICTURE_DESCRIPTION", "False").lower() == "true",
)
DOCUMENT_INTELLIGENCE_ENDPOINT = PersistentConfig( DOCUMENT_INTELLIGENCE_ENDPOINT = PersistentConfig(
"DOCUMENT_INTELLIGENCE_ENDPOINT", "DOCUMENT_INTELLIGENCE_ENDPOINT",
"rag.document_intelligence_endpoint", "rag.document_intelligence_endpoint",
@ -1920,6 +1961,16 @@ RAG_FILE_MAX_SIZE = PersistentConfig(
), ),
) )
RAG_ALLOWED_FILE_EXTENSIONS = PersistentConfig(
"RAG_ALLOWED_FILE_EXTENSIONS",
"rag.file.allowed_extensions",
[
ext.strip()
for ext in os.environ.get("RAG_ALLOWED_FILE_EXTENSIONS", "").split(",")
if ext.strip()
],
)
RAG_EMBEDDING_ENGINE = PersistentConfig( RAG_EMBEDDING_ENGINE = PersistentConfig(
"RAG_EMBEDDING_ENGINE", "RAG_EMBEDDING_ENGINE",
"rag.embedding_engine", "rag.embedding_engine",
@ -2839,6 +2890,12 @@ LDAP_CA_CERT_FILE = PersistentConfig(
os.environ.get("LDAP_CA_CERT_FILE", ""), os.environ.get("LDAP_CA_CERT_FILE", ""),
) )
LDAP_VALIDATE_CERT = PersistentConfig(
"LDAP_VALIDATE_CERT",
"ldap.server.validate_cert",
os.environ.get("LDAP_VALIDATE_CERT", "True").lower() == "true",
)
LDAP_CIPHERS = PersistentConfig( LDAP_CIPHERS = PersistentConfig(
"LDAP_CIPHERS", "ldap.server.ciphers", os.environ.get("LDAP_CIPHERS", "ALL") "LDAP_CIPHERS", "ldap.server.ciphers", os.environ.get("LDAP_CIPHERS", "ALL")
) )

View File

@ -197,6 +197,7 @@ from open_webui.config import (
RAG_EMBEDDING_ENGINE, RAG_EMBEDDING_ENGINE,
RAG_EMBEDDING_BATCH_SIZE, RAG_EMBEDDING_BATCH_SIZE,
RAG_RELEVANCE_THRESHOLD, RAG_RELEVANCE_THRESHOLD,
RAG_ALLOWED_FILE_EXTENSIONS,
RAG_FILE_MAX_COUNT, RAG_FILE_MAX_COUNT,
RAG_FILE_MAX_SIZE, RAG_FILE_MAX_SIZE,
RAG_OPENAI_API_BASE_URL, RAG_OPENAI_API_BASE_URL,
@ -206,10 +207,13 @@ from open_webui.config import (
CHUNK_OVERLAP, CHUNK_OVERLAP,
CHUNK_SIZE, CHUNK_SIZE,
CONTENT_EXTRACTION_ENGINE, CONTENT_EXTRACTION_ENGINE,
EXTERNAL_DOCUMENT_LOADER_URL,
EXTERNAL_DOCUMENT_LOADER_API_KEY,
TIKA_SERVER_URL, TIKA_SERVER_URL,
DOCLING_SERVER_URL, DOCLING_SERVER_URL,
DOCLING_OCR_ENGINE, DOCLING_OCR_ENGINE,
DOCLING_OCR_LANG, DOCLING_OCR_LANG,
DOCLING_DO_PICTURE_DESCRIPTION,
DOCUMENT_INTELLIGENCE_ENDPOINT, DOCUMENT_INTELLIGENCE_ENDPOINT,
DOCUMENT_INTELLIGENCE_KEY, DOCUMENT_INTELLIGENCE_KEY,
MISTRAL_OCR_API_KEY, MISTRAL_OCR_API_KEY,
@ -291,6 +295,8 @@ from open_webui.config import (
ENABLE_EVALUATION_ARENA_MODELS, ENABLE_EVALUATION_ARENA_MODELS,
USER_PERMISSIONS, USER_PERMISSIONS,
DEFAULT_USER_ROLE, DEFAULT_USER_ROLE,
PENDING_USER_OVERLAY_CONTENT,
PENDING_USER_OVERLAY_TITLE,
DEFAULT_PROMPT_SUGGESTIONS, DEFAULT_PROMPT_SUGGESTIONS,
DEFAULT_MODELS, DEFAULT_MODELS,
DEFAULT_ARENA_MODEL, DEFAULT_ARENA_MODEL,
@ -317,6 +323,7 @@ from open_webui.config import (
LDAP_APP_PASSWORD, LDAP_APP_PASSWORD,
LDAP_USE_TLS, LDAP_USE_TLS,
LDAP_CA_CERT_FILE, LDAP_CA_CERT_FILE,
LDAP_VALIDATE_CERT,
LDAP_CIPHERS, LDAP_CIPHERS,
# Misc # Misc
ENV, ENV,
@ -327,6 +334,7 @@ from open_webui.config import (
DEFAULT_LOCALE, DEFAULT_LOCALE,
OAUTH_PROVIDERS, OAUTH_PROVIDERS,
WEBUI_URL, WEBUI_URL,
RESPONSE_WATERMARK,
# Admin # Admin
ENABLE_ADMIN_CHAT_ACCESS, ENABLE_ADMIN_CHAT_ACCESS,
ENABLE_ADMIN_EXPORT, ENABLE_ADMIN_EXPORT,
@ -373,6 +381,7 @@ from open_webui.env import (
OFFLINE_MODE, OFFLINE_MODE,
ENABLE_OTEL, ENABLE_OTEL,
EXTERNAL_PWA_MANIFEST_URL, EXTERNAL_PWA_MANIFEST_URL,
AIOHTTP_CLIENT_SESSION_SSL,
) )
@ -573,6 +582,11 @@ app.state.config.DEFAULT_MODELS = DEFAULT_MODELS
app.state.config.DEFAULT_PROMPT_SUGGESTIONS = DEFAULT_PROMPT_SUGGESTIONS app.state.config.DEFAULT_PROMPT_SUGGESTIONS = DEFAULT_PROMPT_SUGGESTIONS
app.state.config.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE app.state.config.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE
app.state.config.PENDING_USER_OVERLAY_CONTENT = PENDING_USER_OVERLAY_CONTENT
app.state.config.PENDING_USER_OVERLAY_TITLE = PENDING_USER_OVERLAY_TITLE
app.state.config.RESPONSE_WATERMARK = RESPONSE_WATERMARK
app.state.config.USER_PERMISSIONS = USER_PERMISSIONS app.state.config.USER_PERMISSIONS = USER_PERMISSIONS
app.state.config.WEBHOOK_URL = WEBHOOK_URL app.state.config.WEBHOOK_URL = WEBHOOK_URL
app.state.config.BANNERS = WEBUI_BANNERS app.state.config.BANNERS = WEBUI_BANNERS
@ -609,6 +623,7 @@ app.state.config.LDAP_SEARCH_BASE = LDAP_SEARCH_BASE
app.state.config.LDAP_SEARCH_FILTERS = LDAP_SEARCH_FILTERS app.state.config.LDAP_SEARCH_FILTERS = LDAP_SEARCH_FILTERS
app.state.config.LDAP_USE_TLS = LDAP_USE_TLS app.state.config.LDAP_USE_TLS = LDAP_USE_TLS
app.state.config.LDAP_CA_CERT_FILE = LDAP_CA_CERT_FILE app.state.config.LDAP_CA_CERT_FILE = LDAP_CA_CERT_FILE
app.state.config.LDAP_VALIDATE_CERT = LDAP_VALIDATE_CERT
app.state.config.LDAP_CIPHERS = LDAP_CIPHERS app.state.config.LDAP_CIPHERS = LDAP_CIPHERS
@ -631,6 +646,7 @@ app.state.FUNCTIONS = {}
app.state.config.TOP_K = RAG_TOP_K app.state.config.TOP_K = RAG_TOP_K
app.state.config.TOP_K_RERANKER = RAG_TOP_K_RERANKER app.state.config.TOP_K_RERANKER = RAG_TOP_K_RERANKER
app.state.config.RELEVANCE_THRESHOLD = RAG_RELEVANCE_THRESHOLD app.state.config.RELEVANCE_THRESHOLD = RAG_RELEVANCE_THRESHOLD
app.state.config.ALLOWED_FILE_EXTENSIONS = RAG_ALLOWED_FILE_EXTENSIONS
app.state.config.FILE_MAX_SIZE = RAG_FILE_MAX_SIZE app.state.config.FILE_MAX_SIZE = RAG_FILE_MAX_SIZE
app.state.config.FILE_MAX_COUNT = RAG_FILE_MAX_COUNT app.state.config.FILE_MAX_COUNT = RAG_FILE_MAX_COUNT
@ -641,10 +657,13 @@ app.state.config.ENABLE_RAG_HYBRID_SEARCH = ENABLE_RAG_HYBRID_SEARCH
app.state.config.ENABLE_WEB_LOADER_SSL_VERIFICATION = ENABLE_WEB_LOADER_SSL_VERIFICATION app.state.config.ENABLE_WEB_LOADER_SSL_VERIFICATION = ENABLE_WEB_LOADER_SSL_VERIFICATION
app.state.config.CONTENT_EXTRACTION_ENGINE = CONTENT_EXTRACTION_ENGINE app.state.config.CONTENT_EXTRACTION_ENGINE = CONTENT_EXTRACTION_ENGINE
app.state.config.EXTERNAL_DOCUMENT_LOADER_URL = EXTERNAL_DOCUMENT_LOADER_URL
app.state.config.EXTERNAL_DOCUMENT_LOADER_API_KEY = EXTERNAL_DOCUMENT_LOADER_API_KEY
app.state.config.TIKA_SERVER_URL = TIKA_SERVER_URL app.state.config.TIKA_SERVER_URL = TIKA_SERVER_URL
app.state.config.DOCLING_SERVER_URL = DOCLING_SERVER_URL app.state.config.DOCLING_SERVER_URL = DOCLING_SERVER_URL
app.state.config.DOCLING_OCR_ENGINE = DOCLING_OCR_ENGINE app.state.config.DOCLING_OCR_ENGINE = DOCLING_OCR_ENGINE
app.state.config.DOCLING_OCR_LANG = DOCLING_OCR_LANG app.state.config.DOCLING_OCR_LANG = DOCLING_OCR_LANG
app.state.config.DOCLING_DO_PICTURE_DESCRIPTION = DOCLING_DO_PICTURE_DESCRIPTION
app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT = DOCUMENT_INTELLIGENCE_ENDPOINT app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT = DOCUMENT_INTELLIGENCE_ENDPOINT
app.state.config.DOCUMENT_INTELLIGENCE_KEY = DOCUMENT_INTELLIGENCE_KEY app.state.config.DOCUMENT_INTELLIGENCE_KEY = DOCUMENT_INTELLIGENCE_KEY
app.state.config.MISTRAL_OCR_API_KEY = MISTRAL_OCR_API_KEY app.state.config.MISTRAL_OCR_API_KEY = MISTRAL_OCR_API_KEY
@ -1167,11 +1186,12 @@ async def chat_completion(
"chat_id": form_data.pop("chat_id", None), "chat_id": form_data.pop("chat_id", None),
"message_id": form_data.pop("id", None), "message_id": form_data.pop("id", None),
"session_id": form_data.pop("session_id", None), "session_id": form_data.pop("session_id", None),
"filter_ids": form_data.pop("filter_ids", []),
"tool_ids": form_data.get("tool_ids", None), "tool_ids": form_data.get("tool_ids", None),
"tool_servers": form_data.pop("tool_servers", None), "tool_servers": form_data.pop("tool_servers", None),
"files": form_data.get("files", None), "files": form_data.get("files", None),
"features": form_data.get("features", None), "features": form_data.get("features", {}),
"variables": form_data.get("variables", None), "variables": form_data.get("variables", {}),
"model": model, "model": model,
"direct": model_item.get("direct", False), "direct": model_item.get("direct", False),
**( **(
@ -1395,6 +1415,11 @@ async def get_app_config(request: Request):
"sharepoint_url": ONEDRIVE_SHAREPOINT_URL.value, "sharepoint_url": ONEDRIVE_SHAREPOINT_URL.value,
"sharepoint_tenant_id": ONEDRIVE_SHAREPOINT_TENANT_ID.value, "sharepoint_tenant_id": ONEDRIVE_SHAREPOINT_TENANT_ID.value,
}, },
"ui": {
"pending_user_overlay_title": app.state.config.PENDING_USER_OVERLAY_TITLE,
"pending_user_overlay_content": app.state.config.PENDING_USER_OVERLAY_CONTENT,
"response_watermark": app.state.config.RESPONSE_WATERMARK,
},
"license_metadata": app.state.LICENSE_METADATA, "license_metadata": app.state.LICENSE_METADATA,
**( **(
{ {
@ -1446,7 +1471,8 @@ async def get_app_latest_release_version(user=Depends(get_verified_user)):
timeout = aiohttp.ClientTimeout(total=1) timeout = aiohttp.ClientTimeout(total=1)
async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session: async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
async with session.get( async with session.get(
"https://api.github.com/repos/open-webui/open-webui/releases/latest" "https://api.github.com/repos/open-webui/open-webui/releases/latest",
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as response: ) as response:
response.raise_for_status() response.raise_for_status()
data = await response.json() data = await response.json()

View File

@ -0,0 +1,58 @@
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 ExternalDocumentLoader(BaseLoader):
def __init__(
self,
file_path,
url: str,
api_key: str,
mime_type=None,
**kwargs,
) -> None:
self.url = url
self.api_key = api_key
self.file_path = file_path
self.mime_type = mime_type
def load(self) -> list[Document]:
with open(self.file_path, "rb") as f:
data = f.read()
headers = {}
if self.mime_type is not None:
headers["Content-Type"] = self.mime_type
if self.api_key is not None:
headers["Authorization"] = f"Bearer {self.api_key}"
url = self.url
if url.endswith("/"):
url = url[:-1]
r = requests.put(f"{url}/process", data=data, headers=headers)
if r.ok:
res = r.json()
if res:
return [
Document(
page_content=res.get("page_content"),
metadata=res.get("metadata"),
)
]
else:
raise Exception("Error loading document: No content returned")
else:
raise Exception(f"Error loading document: {r.status_code} {r.text}")

View File

@ -10,7 +10,7 @@ log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"]) log.setLevel(SRC_LOG_LEVELS["RAG"])
class ExternalLoader(BaseLoader): class ExternalWebLoader(BaseLoader):
def __init__( def __init__(
self, self,
web_paths: Union[str, List[str]], web_paths: Union[str, List[str]],
@ -32,7 +32,7 @@ class ExternalLoader(BaseLoader):
response = requests.post( response = requests.post(
self.external_url, self.external_url,
headers={ headers={
"User-Agent": "Open WebUI (https://github.com/open-webui/open-webui) RAG Bot", "User-Agent": "Open WebUI (https://github.com/open-webui/open-webui) External Web Loader",
"Authorization": f"Bearer {self.external_api_key}", "Authorization": f"Bearer {self.external_api_key}",
}, },
json={ json={

View File

@ -21,6 +21,8 @@ from langchain_community.document_loaders import (
) )
from langchain_core.documents import Document from langchain_core.documents import Document
from open_webui.retrieval.loaders.external_document import ExternalDocumentLoader
from open_webui.retrieval.loaders.mistral import MistralLoader from open_webui.retrieval.loaders.mistral import MistralLoader
from open_webui.env import SRC_LOG_LEVELS, GLOBAL_LOG_LEVEL from open_webui.env import SRC_LOG_LEVELS, GLOBAL_LOG_LEVEL
@ -126,14 +128,12 @@ class TikaLoader:
class DoclingLoader: class DoclingLoader:
def __init__( def __init__(self, url, file_path=None, mime_type=None, params=None):
self, url, file_path=None, mime_type=None, ocr_engine=None, ocr_lang=None
):
self.url = url.rstrip("/") self.url = url.rstrip("/")
self.file_path = file_path self.file_path = file_path
self.mime_type = mime_type self.mime_type = mime_type
self.ocr_engine = ocr_engine
self.ocr_lang = ocr_lang self.params = params or {}
def load(self) -> list[Document]: def load(self) -> list[Document]:
with open(self.file_path, "rb") as f: with open(self.file_path, "rb") as f:
@ -150,10 +150,18 @@ class DoclingLoader:
"table_mode": "accurate", "table_mode": "accurate",
} }
if self.ocr_engine and self.ocr_lang: if self.params:
params["ocr_engine"] = self.ocr_engine if self.params.get("do_picture_classification"):
params["do_picture_classification"] = self.params.get(
"do_picture_classification"
)
if self.params.get("ocr_engine") and self.params.get("ocr_lang"):
params["ocr_engine"] = self.params.get("ocr_engine")
params["ocr_lang"] = [ params["ocr_lang"] = [
lang.strip() for lang in self.ocr_lang.split(",") if lang.strip() lang.strip()
for lang in self.params.get("ocr_lang").split(",")
if lang.strip()
] ]
endpoint = f"{self.url}/v1alpha/convert/file" endpoint = f"{self.url}/v1alpha/convert/file"
@ -207,6 +215,17 @@ class Loader:
def _get_loader(self, filename: str, file_content_type: str, file_path: str): def _get_loader(self, filename: str, file_content_type: str, file_path: str):
file_ext = filename.split(".")[-1].lower() file_ext = filename.split(".")[-1].lower()
if (
self.engine == "external"
and self.kwargs.get("EXTERNAL_DOCUMENT_LOADER_URL")
and self.kwargs.get("EXTERNAL_DOCUMENT_LOADER_API_KEY")
):
loader = ExternalDocumentLoader(
file_path=file_path,
url=self.kwargs.get("EXTERNAL_DOCUMENT_LOADER_URL"),
api_key=self.kwargs.get("EXTERNAL_DOCUMENT_LOADER_API_KEY"),
mime_type=file_content_type,
)
if self.engine == "tika" and self.kwargs.get("TIKA_SERVER_URL"): if self.engine == "tika" and self.kwargs.get("TIKA_SERVER_URL"):
if self._is_text_file(file_ext, file_content_type): if self._is_text_file(file_ext, file_content_type):
loader = TextLoader(file_path, autodetect_encoding=True) loader = TextLoader(file_path, autodetect_encoding=True)
@ -225,8 +244,13 @@ class Loader:
url=self.kwargs.get("DOCLING_SERVER_URL"), url=self.kwargs.get("DOCLING_SERVER_URL"),
file_path=file_path, file_path=file_path,
mime_type=file_content_type, mime_type=file_content_type,
ocr_engine=self.kwargs.get("DOCLING_OCR_ENGINE"), params={
ocr_lang=self.kwargs.get("DOCLING_OCR_LANG"), "ocr_engine": self.kwargs.get("DOCLING_OCR_ENGINE"),
"ocr_lang": self.kwargs.get("DOCLING_OCR_LANG"),
"do_picture_classification": self.kwargs.get(
"DOCLING_DO_PICTURE_DESCRIPTION"
),
},
) )
elif ( elif (
self.engine == "document_intelligence" self.engine == "document_intelligence"
@ -258,6 +282,15 @@ class Loader:
loader = MistralLoader( loader = MistralLoader(
api_key=self.kwargs.get("MISTRAL_OCR_API_KEY"), file_path=file_path api_key=self.kwargs.get("MISTRAL_OCR_API_KEY"), file_path=file_path
) )
elif (
self.engine == "external"
and self.kwargs.get("MISTRAL_OCR_API_KEY") != ""
and file_ext
in ["pdf"] # Mistral OCR currently only supports PDF and images
):
loader = MistralLoader(
api_key=self.kwargs.get("MISTRAL_OCR_API_KEY"), file_path=file_path
)
else: else:
if file_ext == "pdf": if file_ext == "pdf":
loader = PyPDFLoader( loader = PyPDFLoader(

View File

@ -12,7 +12,7 @@ from langchain_community.retrievers import BM25Retriever
from langchain_core.documents import Document from langchain_core.documents import Document
from open_webui.config import VECTOR_DB from open_webui.config import VECTOR_DB
from open_webui.retrieval.vector.connector import VECTOR_DB_CLIENT from open_webui.retrieval.vector.factory import VECTOR_DB_CLIENT
from open_webui.models.users import UserModel from open_webui.models.users import UserModel
from open_webui.models.files import Files from open_webui.models.files import Files

View File

@ -1,30 +0,0 @@
from open_webui.config import VECTOR_DB
if VECTOR_DB == "milvus":
from open_webui.retrieval.vector.dbs.milvus import MilvusClient
VECTOR_DB_CLIENT = MilvusClient()
elif VECTOR_DB == "qdrant":
from open_webui.retrieval.vector.dbs.qdrant import QdrantClient
VECTOR_DB_CLIENT = QdrantClient()
elif VECTOR_DB == "opensearch":
from open_webui.retrieval.vector.dbs.opensearch import OpenSearchClient
VECTOR_DB_CLIENT = OpenSearchClient()
elif VECTOR_DB == "pgvector":
from open_webui.retrieval.vector.dbs.pgvector import PgvectorClient
VECTOR_DB_CLIENT = PgvectorClient()
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
VECTOR_DB_CLIENT = ChromaClient()

View File

@ -0,0 +1,712 @@
import logging
from typing import Optional, Tuple
from urllib.parse import urlparse
import grpc
from open_webui.config import (
QDRANT_API_KEY,
QDRANT_GRPC_PORT,
QDRANT_ON_DISK,
QDRANT_PREFER_GRPC,
QDRANT_URI,
)
from open_webui.env import SRC_LOG_LEVELS
from open_webui.retrieval.vector.main import (
GetResult,
SearchResult,
VectorDBBase,
VectorItem,
)
from qdrant_client import QdrantClient as Qclient
from qdrant_client.http.exceptions import UnexpectedResponse
from qdrant_client.http.models import PointStruct
from qdrant_client.models import models
NO_LIMIT = 999999999
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
class QdrantClient(VectorDBBase):
def __init__(self):
self.collection_prefix = "open-webui"
self.QDRANT_URI = QDRANT_URI
self.QDRANT_API_KEY = QDRANT_API_KEY
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)
# Main collection types for multi-tenancy
self.MEMORY_COLLECTION = f"{self.collection_prefix}_memories"
self.KNOWLEDGE_COLLECTION = f"{self.collection_prefix}_knowledge"
self.FILE_COLLECTION = f"{self.collection_prefix}_files"
self.WEB_SEARCH_COLLECTION = f"{self.collection_prefix}_web-search"
self.HASH_BASED_COLLECTION = f"{self.collection_prefix}_hash-based"
def _result_to_get_result(self, points) -> GetResult:
ids = []
documents = []
metadatas = []
for point in points:
payload = point.payload
ids.append(point.id)
documents.append(payload["text"])
metadatas.append(payload["metadata"])
return GetResult(
**{
"ids": [ids],
"documents": [documents],
"metadatas": [metadatas],
}
)
def _get_collection_and_tenant_id(self, collection_name: str) -> Tuple[str, str]:
"""
Maps the traditional collection name to multi-tenant collection and tenant ID.
Returns:
tuple: (collection_name, tenant_id)
"""
# Check for user memory collections
tenant_id = collection_name
if collection_name.startswith("user-memory-"):
return self.MEMORY_COLLECTION, tenant_id
# Check for file collections
elif collection_name.startswith("file-"):
return self.FILE_COLLECTION, tenant_id
# Check for web search collections
elif collection_name.startswith("web-search-"):
return self.WEB_SEARCH_COLLECTION, tenant_id
# Handle hash-based collections (YouTube and web URLs)
elif len(collection_name) == 63 and all(
c in "0123456789abcdef" for c in collection_name
):
return self.HASH_BASED_COLLECTION, tenant_id
else:
return self.KNOWLEDGE_COLLECTION, tenant_id
def _extract_error_message(self, exception):
"""
Extract error message from either HTTP or gRPC exceptions
Returns:
tuple: (status_code, error_message)
"""
# Check if it's an HTTP exception
if isinstance(exception, UnexpectedResponse):
try:
error_data = exception.structured()
error_msg = error_data.get("status", {}).get("error", "")
return exception.status_code, error_msg
except Exception as inner_e:
log.error(f"Failed to parse HTTP error: {inner_e}")
return exception.status_code, str(exception)
# Check if it's a gRPC exception
elif isinstance(exception, grpc.RpcError):
# Extract status code from gRPC error
status_code = None
if hasattr(exception, "code") and callable(exception.code):
status_code = exception.code().value[0]
# Extract error message
error_msg = str(exception)
if "details =" in error_msg:
# Parse the details line which contains the actual error message
try:
details_line = [
line.strip()
for line in error_msg.split("\n")
if "details =" in line
][0]
error_msg = details_line.split("details =")[1].strip(' "')
except (IndexError, AttributeError):
# Fall back to full message if parsing fails
pass
return status_code, error_msg
# For any other type of exception
return None, str(exception)
def _is_collection_not_found_error(self, exception):
"""
Check if the exception is due to collection not found, supporting both HTTP and gRPC
"""
status_code, error_msg = self._extract_error_message(exception)
# HTTP error (404)
if (
status_code == 404
and "Collection" in error_msg
and "doesn't exist" in error_msg
):
return True
# gRPC error (NOT_FOUND status)
if (
isinstance(exception, grpc.RpcError)
and exception.code() == grpc.StatusCode.NOT_FOUND
):
return True
return False
def _is_dimension_mismatch_error(self, exception):
"""
Check if the exception is due to dimension mismatch, supporting both HTTP and gRPC
"""
status_code, error_msg = self._extract_error_message(exception)
# Common patterns in both HTTP and gRPC
return (
"Vector dimension error" in error_msg
or "dimensions mismatch" in error_msg
or "invalid vector size" in error_msg
)
def _create_multi_tenant_collection_if_not_exists(
self, mt_collection_name: str, dimension: int = 384
):
"""
Creates a collection with multi-tenancy configuration if it doesn't exist.
Default dimension is set to 384 which corresponds to 'sentence-transformers/all-MiniLM-L6-v2'.
When creating collections dynamically (insert/upsert), the actual vector dimensions will be used.
"""
try:
# Try to create the collection directly - will fail if it already exists
self.client.create_collection(
collection_name=mt_collection_name,
vectors_config=models.VectorParams(
size=dimension,
distance=models.Distance.COSINE,
on_disk=self.QDRANT_ON_DISK,
),
hnsw_config=models.HnswConfigDiff(
payload_m=16, # Enable per-tenant indexing
m=0,
on_disk=self.QDRANT_ON_DISK,
),
)
# Create tenant ID payload index
self.client.create_payload_index(
collection_name=mt_collection_name,
field_name="tenant_id",
field_schema=models.KeywordIndexParams(
type=models.KeywordIndexType.KEYWORD,
is_tenant=True,
on_disk=self.QDRANT_ON_DISK,
),
wait=True,
)
log.info(
f"Multi-tenant collection {mt_collection_name} created with dimension {dimension}!"
)
except (UnexpectedResponse, grpc.RpcError) as e:
# Check for the specific error indicating collection already exists
status_code, error_msg = self._extract_error_message(e)
# HTTP status code 409 or gRPC ALREADY_EXISTS
if (isinstance(e, UnexpectedResponse) and status_code == 409) or (
isinstance(e, grpc.RpcError)
and e.code() == grpc.StatusCode.ALREADY_EXISTS
):
if "already exists" in error_msg:
log.debug(f"Collection {mt_collection_name} already exists")
return
# If it's not an already exists error, re-raise
raise e
except Exception as e:
raise e
def _create_points(self, items: list[VectorItem], tenant_id: str):
"""
Create point structs from vector items with tenant ID.
"""
return [
PointStruct(
id=item["id"],
vector=item["vector"],
payload={
"text": item["text"],
"metadata": item["metadata"],
"tenant_id": tenant_id,
},
)
for item in items
]
def has_collection(self, collection_name: str) -> bool:
"""
Check if a logical collection exists by checking for any points with the tenant ID.
"""
if not self.client:
return False
# Map to multi-tenant collection and tenant ID
mt_collection, tenant_id = self._get_collection_and_tenant_id(collection_name)
# Create tenant filter
tenant_filter = models.FieldCondition(
key="tenant_id", match=models.MatchValue(value=tenant_id)
)
try:
# Try directly querying - most of the time collection should exist
response = self.client.query_points(
collection_name=mt_collection,
query_filter=models.Filter(must=[tenant_filter]),
limit=1,
)
# Collection exists with this tenant ID if there are points
return len(response.points) > 0
except (UnexpectedResponse, grpc.RpcError) as e:
if self._is_collection_not_found_error(e):
log.debug(f"Collection {mt_collection} doesn't exist")
return False
else:
# For other API errors, log and return False
_, error_msg = self._extract_error_message(e)
log.warning(f"Unexpected Qdrant error: {error_msg}")
return False
except Exception as e:
# For any other errors, log and return False
log.debug(f"Error checking collection {mt_collection}: {e}")
return False
def delete(
self,
collection_name: str,
ids: Optional[list[str]] = None,
filter: Optional[dict] = None,
):
"""
Delete vectors by ID or filter from a collection with tenant isolation.
"""
if not self.client:
return None
# Map to multi-tenant collection and tenant ID
mt_collection, tenant_id = self._get_collection_and_tenant_id(collection_name)
# Create tenant filter
tenant_filter = models.FieldCondition(
key="tenant_id", match=models.MatchValue(value=tenant_id)
)
must_conditions = [tenant_filter]
should_conditions = []
if ids:
for id_value in ids:
should_conditions.append(
models.FieldCondition(
key="metadata.id",
match=models.MatchValue(value=id_value),
),
)
elif filter:
for key, value in filter.items():
must_conditions.append(
models.FieldCondition(
key=f"metadata.{key}",
match=models.MatchValue(value=value),
),
)
try:
# Try to delete directly - most of the time collection should exist
update_result = self.client.delete(
collection_name=mt_collection,
points_selector=models.FilterSelector(
filter=models.Filter(must=must_conditions, should=should_conditions)
),
)
return update_result
except (UnexpectedResponse, grpc.RpcError) as e:
if self._is_collection_not_found_error(e):
log.debug(
f"Collection {mt_collection} doesn't exist, nothing to delete"
)
return None
else:
# For other API errors, log and re-raise
_, error_msg = self._extract_error_message(e)
log.warning(f"Unexpected Qdrant error: {error_msg}")
raise
except Exception as e:
# For non-Qdrant exceptions, re-raise
raise
def search(
self, collection_name: str, vectors: list[list[float | int]], limit: int
) -> Optional[SearchResult]:
"""
Search for the nearest neighbor items based on the vectors with tenant isolation.
"""
if not self.client:
return None
# Map to multi-tenant collection and tenant ID
mt_collection, tenant_id = self._get_collection_and_tenant_id(collection_name)
# Get the vector dimension from the query vector
dimension = len(vectors[0]) if vectors and len(vectors) > 0 else None
try:
# Try the search operation directly - most of the time collection should exist
# Create tenant filter
tenant_filter = models.FieldCondition(
key="tenant_id", match=models.MatchValue(value=tenant_id)
)
# Ensure vector dimensions match the collection
collection_dim = self.client.get_collection(
mt_collection
).config.params.vectors.size
if collection_dim != dimension:
if collection_dim < dimension:
vectors = [vector[:collection_dim] for vector in vectors]
else:
vectors = [
vector + [0] * (collection_dim - dimension)
for vector in vectors
]
# Search with tenant filter
prefetch_query = models.Prefetch(
filter=models.Filter(must=[tenant_filter]),
limit=NO_LIMIT,
)
query_response = self.client.query_points(
collection_name=mt_collection,
query=vectors[0],
prefetch=prefetch_query,
limit=limit,
)
get_result = self._result_to_get_result(query_response.points)
return SearchResult(
ids=get_result.ids,
documents=get_result.documents,
metadatas=get_result.metadatas,
# qdrant distance is [-1, 1], normalize to [0, 1]
distances=[
[(point.score + 1.0) / 2.0 for point in query_response.points]
],
)
except (UnexpectedResponse, grpc.RpcError) as e:
if self._is_collection_not_found_error(e):
log.debug(
f"Collection {mt_collection} doesn't exist, search returns None"
)
return None
else:
# For other API errors, log and re-raise
_, error_msg = self._extract_error_message(e)
log.warning(f"Unexpected Qdrant error during search: {error_msg}")
raise
except Exception as e:
# For non-Qdrant exceptions, log and return None
log.exception(f"Error searching collection '{collection_name}': {e}")
return None
def query(self, collection_name: str, filter: dict, limit: Optional[int] = None):
"""
Query points with filters and tenant isolation.
"""
if not self.client:
return None
# Map to multi-tenant collection and tenant ID
mt_collection, tenant_id = self._get_collection_and_tenant_id(collection_name)
# Set default limit if not provided
if limit is None:
limit = NO_LIMIT
# Create tenant filter
tenant_filter = models.FieldCondition(
key="tenant_id", match=models.MatchValue(value=tenant_id)
)
# Create metadata filters
field_conditions = []
for key, value in filter.items():
field_conditions.append(
models.FieldCondition(
key=f"metadata.{key}", match=models.MatchValue(value=value)
)
)
# Combine tenant filter with metadata filters
combined_filter = models.Filter(must=[tenant_filter, *field_conditions])
try:
# Try the query directly - most of the time collection should exist
points = self.client.query_points(
collection_name=mt_collection,
query_filter=combined_filter,
limit=limit,
)
return self._result_to_get_result(points.points)
except (UnexpectedResponse, grpc.RpcError) as e:
if self._is_collection_not_found_error(e):
log.debug(
f"Collection {mt_collection} doesn't exist, query returns None"
)
return None
else:
# For other API errors, log and re-raise
_, error_msg = self._extract_error_message(e)
log.warning(f"Unexpected Qdrant error during query: {error_msg}")
raise
except Exception as e:
# For non-Qdrant exceptions, log and re-raise
log.exception(f"Error querying collection '{collection_name}': {e}")
return None
def get(self, collection_name: str) -> Optional[GetResult]:
"""
Get all items in a collection with tenant isolation.
"""
if not self.client:
return None
# Map to multi-tenant collection and tenant ID
mt_collection, tenant_id = self._get_collection_and_tenant_id(collection_name)
# Create tenant filter
tenant_filter = models.FieldCondition(
key="tenant_id", match=models.MatchValue(value=tenant_id)
)
try:
# Try to get points directly - most of the time collection should exist
points = self.client.query_points(
collection_name=mt_collection,
query_filter=models.Filter(must=[tenant_filter]),
limit=NO_LIMIT,
)
return self._result_to_get_result(points.points)
except (UnexpectedResponse, grpc.RpcError) as e:
if self._is_collection_not_found_error(e):
log.debug(f"Collection {mt_collection} doesn't exist, get returns None")
return None
else:
# For other API errors, log and re-raise
_, error_msg = self._extract_error_message(e)
log.warning(f"Unexpected Qdrant error during get: {error_msg}")
raise
except Exception as e:
# For non-Qdrant exceptions, log and return None
log.exception(f"Error getting collection '{collection_name}': {e}")
return None
def _handle_operation_with_error_retry(
self, operation_name, mt_collection, points, dimension
):
"""
Private helper to handle common error cases for insert and upsert operations.
Args:
operation_name: 'insert' or 'upsert'
mt_collection: The multi-tenant collection name
points: The vector points to insert/upsert
dimension: The dimension of the vectors
Returns:
The operation result (for upsert) or None (for insert)
"""
try:
if operation_name == "insert":
self.client.upload_points(mt_collection, points)
return None
else: # upsert
return self.client.upsert(mt_collection, points)
except (UnexpectedResponse, grpc.RpcError) as e:
# Handle collection not found
if self._is_collection_not_found_error(e):
log.info(
f"Collection {mt_collection} doesn't exist. Creating it with dimension {dimension}."
)
# Create collection with correct dimensions from our vectors
self._create_multi_tenant_collection_if_not_exists(
mt_collection_name=mt_collection, dimension=dimension
)
# Try operation again - no need for dimension adjustment since we just created with correct dimensions
if operation_name == "insert":
self.client.upload_points(mt_collection, points)
return None
else: # upsert
return self.client.upsert(mt_collection, points)
# Handle dimension mismatch
elif self._is_dimension_mismatch_error(e):
# For dimension errors, the collection must exist, so get its configuration
mt_collection_info = self.client.get_collection(mt_collection)
existing_size = mt_collection_info.config.params.vectors.size
log.info(
f"Dimension mismatch: Collection {mt_collection} expects {existing_size}, got {dimension}"
)
if existing_size < dimension:
# Truncate vectors to fit
log.info(
f"Truncating vectors from {dimension} to {existing_size} dimensions"
)
points = [
PointStruct(
id=point.id,
vector=point.vector[:existing_size],
payload=point.payload,
)
for point in points
]
elif existing_size > dimension:
# Pad vectors with zeros
log.info(
f"Padding vectors from {dimension} to {existing_size} dimensions with zeros"
)
points = [
PointStruct(
id=point.id,
vector=point.vector
+ [0] * (existing_size - len(point.vector)),
payload=point.payload,
)
for point in points
]
# Try operation again with adjusted dimensions
if operation_name == "insert":
self.client.upload_points(mt_collection, points)
return None
else: # upsert
return self.client.upsert(mt_collection, points)
else:
# Not a known error we can handle, log and re-raise
_, error_msg = self._extract_error_message(e)
log.warning(f"Unhandled Qdrant error: {error_msg}")
raise
except Exception as e:
# For non-Qdrant exceptions, re-raise
raise
def insert(self, collection_name: str, items: list[VectorItem]):
"""
Insert items with tenant ID.
"""
if not self.client or not items:
return None
# Map to multi-tenant collection and tenant ID
mt_collection, tenant_id = self._get_collection_and_tenant_id(collection_name)
# Get dimensions from the actual vectors
dimension = len(items[0]["vector"]) if items else None
# Create points with tenant ID
points = self._create_points(items, tenant_id)
# Handle the operation with error retry
return self._handle_operation_with_error_retry(
"insert", mt_collection, points, dimension
)
def upsert(self, collection_name: str, items: list[VectorItem]):
"""
Upsert items with tenant ID.
"""
if not self.client or not items:
return None
# Map to multi-tenant collection and tenant ID
mt_collection, tenant_id = self._get_collection_and_tenant_id(collection_name)
# Get dimensions from the actual vectors
dimension = len(items[0]["vector"]) if items else None
# Create points with tenant ID
points = self._create_points(items, tenant_id)
# Handle the operation with error retry
return self._handle_operation_with_error_retry(
"upsert", mt_collection, points, dimension
)
def reset(self):
"""
Reset the database by deleting all collections.
"""
if not self.client:
return None
collection_names = self.client.get_collections().collections
for collection_name in collection_names:
if collection_name.name.startswith(self.collection_prefix):
self.client.delete_collection(collection_name=collection_name.name)
def delete_collection(self, collection_name: str):
"""
Delete a collection.
"""
if not self.client:
return None
# Map to multi-tenant collection and tenant ID
mt_collection, tenant_id = self._get_collection_and_tenant_id(collection_name)
tenant_filter = models.FieldCondition(
key="tenant_id", match=models.MatchValue(value=tenant_id)
)
field_conditions = [tenant_filter]
update_result = self.client.delete(
collection_name=mt_collection,
points_selector=models.FilterSelector(
filter=models.Filter(must=field_conditions)
),
)
if self.client.get_collection(mt_collection).points_count == 0:
self.client.delete_collection(mt_collection)
return update_result

View File

@ -0,0 +1,55 @@
from open_webui.retrieval.vector.main import VectorDBBase
from open_webui.retrieval.vector.type import VectorType
from open_webui.config import VECTOR_DB, ENABLE_QDRANT_MULTITENANCY_MODE
class Vector:
@staticmethod
def get_vector(vector_type: str) -> VectorDBBase:
"""
get vector db instance by vector type
"""
match vector_type:
case VectorType.MILVUS:
from open_webui.retrieval.vector.dbs.milvus import MilvusClient
return MilvusClient()
case VectorType.QDRANT:
if ENABLE_QDRANT_MULTITENANCY_MODE:
from open_webui.retrieval.vector.dbs.qdrant_multitenancy import (
QdrantClient,
)
return QdrantClient()
else:
from open_webui.retrieval.vector.dbs.qdrant import QdrantClient
return QdrantClient()
case VectorType.PINECONE:
from open_webui.retrieval.vector.dbs.pinecone import PineconeClient
return PineconeClient()
case VectorType.OPENSEARCH:
from open_webui.retrieval.vector.dbs.opensearch import OpenSearchClient
return OpenSearchClient()
case VectorType.PGVECTOR:
from open_webui.retrieval.vector.dbs.pgvector import PgvectorClient
return PgvectorClient()
case VectorType.ELASTICSEARCH:
from open_webui.retrieval.vector.dbs.elasticsearch import (
ElasticsearchClient,
)
return ElasticsearchClient()
case VectorType.CHROMA:
from open_webui.retrieval.vector.dbs.chroma import ChromaClient
return ChromaClient()
case _:
raise ValueError(f"Unsupported vector type: {vector_type}")
VECTOR_DB_CLIENT = Vector.get_vector(VECTOR_DB)

View File

@ -0,0 +1,11 @@
from enum import StrEnum
class VectorType(StrEnum):
MILVUS = "milvus"
QDRANT = "qdrant"
CHROMA = "chroma"
PINECONE = "pinecone"
ELASTICSEARCH = "elasticsearch"
OPENSEARCH = "opensearch"
PGVECTOR = "pgvector"

View File

@ -25,7 +25,7 @@ from langchain_community.document_loaders.firecrawl import FireCrawlLoader
from langchain_community.document_loaders.base import BaseLoader from langchain_community.document_loaders.base import BaseLoader
from langchain_core.documents import Document from langchain_core.documents import Document
from open_webui.retrieval.loaders.tavily import TavilyLoader from open_webui.retrieval.loaders.tavily import TavilyLoader
from open_webui.retrieval.loaders.external import ExternalLoader from open_webui.retrieval.loaders.external_web import ExternalWebLoader
from open_webui.constants import ERROR_MESSAGES from open_webui.constants import ERROR_MESSAGES
from open_webui.config import ( from open_webui.config import (
ENABLE_RAG_LOCAL_WEB_FETCH, ENABLE_RAG_LOCAL_WEB_FETCH,
@ -39,7 +39,7 @@ from open_webui.config import (
EXTERNAL_WEB_LOADER_URL, EXTERNAL_WEB_LOADER_URL,
EXTERNAL_WEB_LOADER_API_KEY, EXTERNAL_WEB_LOADER_API_KEY,
) )
from open_webui.env import SRC_LOG_LEVELS from open_webui.env import SRC_LOG_LEVELS, AIOHTTP_CLIENT_SESSION_SSL
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"]) log.setLevel(SRC_LOG_LEVELS["RAG"])
@ -515,7 +515,9 @@ class SafeWebBaseLoader(WebBaseLoader):
kwargs["ssl"] = False kwargs["ssl"] = False
async with session.get( async with session.get(
url, **(self.requests_kwargs | kwargs) url,
**(self.requests_kwargs | kwargs),
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as response: ) as response:
if self.raise_for_status: if self.raise_for_status:
response.raise_for_status() response.raise_for_status()
@ -628,7 +630,7 @@ def get_web_loader(
web_loader_args["extract_depth"] = TAVILY_EXTRACT_DEPTH.value web_loader_args["extract_depth"] = TAVILY_EXTRACT_DEPTH.value
if WEB_LOADER_ENGINE.value == "external": if WEB_LOADER_ENGINE.value == "external":
WebLoaderClass = ExternalLoader WebLoaderClass = ExternalWebLoader
web_loader_args["external_url"] = EXTERNAL_WEB_LOADER_URL.value web_loader_args["external_url"] = EXTERNAL_WEB_LOADER_URL.value
web_loader_args["external_api_key"] = EXTERNAL_WEB_LOADER_API_KEY.value web_loader_args["external_api_key"] = EXTERNAL_WEB_LOADER_API_KEY.value

View File

@ -7,6 +7,7 @@ from functools import lru_cache
from pathlib import Path from pathlib import Path
from pydub import AudioSegment from pydub import AudioSegment
from pydub.silence import split_on_silence from pydub.silence import split_on_silence
from concurrent.futures import ThreadPoolExecutor
import aiohttp import aiohttp
import aiofiles import aiofiles
@ -38,6 +39,7 @@ from open_webui.config import (
from open_webui.constants import ERROR_MESSAGES from open_webui.constants import ERROR_MESSAGES
from open_webui.env import ( from open_webui.env import (
AIOHTTP_CLIENT_SESSION_SSL,
AIOHTTP_CLIENT_TIMEOUT, AIOHTTP_CLIENT_TIMEOUT,
ENV, ENV,
SRC_LOG_LEVELS, SRC_LOG_LEVELS,
@ -49,7 +51,7 @@ from open_webui.env import (
router = APIRouter() router = APIRouter()
# Constants # Constants
MAX_FILE_SIZE_MB = 25 MAX_FILE_SIZE_MB = 20
MAX_FILE_SIZE = MAX_FILE_SIZE_MB * 1024 * 1024 # Convert MB to bytes MAX_FILE_SIZE = MAX_FILE_SIZE_MB * 1024 * 1024 # Convert MB to bytes
AZURE_MAX_FILE_SIZE_MB = 200 AZURE_MAX_FILE_SIZE_MB = 200
AZURE_MAX_FILE_SIZE = AZURE_MAX_FILE_SIZE_MB * 1024 * 1024 # Convert MB to bytes AZURE_MAX_FILE_SIZE = AZURE_MAX_FILE_SIZE_MB * 1024 * 1024 # Convert MB to bytes
@ -86,8 +88,6 @@ def get_audio_convert_format(file_path):
and info.get("codec_tag_string") == "mp4a" and info.get("codec_tag_string") == "mp4a"
): ):
return "mp4" return "mp4"
elif info.get("format_name") == "ogg":
return "ogg"
except Exception as e: except Exception as e:
log.error(f"Error getting audio format: {e}") log.error(f"Error getting audio format: {e}")
return False return False
@ -326,6 +326,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
else {} else {}
), ),
}, },
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as r: ) as r:
r.raise_for_status() r.raise_for_status()
@ -381,6 +382,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
"Content-Type": "application/json", "Content-Type": "application/json",
"xi-api-key": request.app.state.config.TTS_API_KEY, "xi-api-key": request.app.state.config.TTS_API_KEY,
}, },
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as r: ) as r:
r.raise_for_status() r.raise_for_status()
@ -439,6 +441,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
"X-Microsoft-OutputFormat": output_format, "X-Microsoft-OutputFormat": output_format,
}, },
data=data, data=data,
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as r: ) as r:
r.raise_for_status() r.raise_for_status()
@ -507,8 +510,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
return FileResponse(file_path) return FileResponse(file_path)
def transcribe(request: Request, file_path): def transcription_handler(request, file_path):
log.info(f"transcribe: {file_path}")
filename = os.path.basename(file_path) filename = os.path.basename(file_path)
file_dir = os.path.dirname(file_path) file_dir = os.path.dirname(file_path)
id = filename.split(".")[0] id = filename.split(".")[0]
@ -771,24 +773,119 @@ def transcribe(request: Request, file_path):
) )
def transcribe(request: Request, file_path):
log.info(f"transcribe: {file_path}")
try:
file_path = compress_audio(file_path)
except Exception as e:
log.exception(e)
# Always produce a list of chunk paths (could be one entry if small)
try:
chunk_paths = split_audio(file_path, MAX_FILE_SIZE)
print(f"Chunk paths: {chunk_paths}")
except Exception as e:
log.exception(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
)
results = []
try:
with ThreadPoolExecutor() as executor:
# Submit tasks for each chunk_path
futures = [
executor.submit(transcription_handler, request, chunk_path)
for chunk_path in chunk_paths
]
# Gather results as they complete
for future in futures:
try:
results.append(future.result())
except Exception as transcribe_exc:
log.exception(f"Error transcribing chunk: {transcribe_exc}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Error during transcription.",
)
finally:
# Clean up only the temporary chunks, never the original file
for chunk_path in chunk_paths:
if chunk_path != file_path and os.path.isfile(chunk_path):
try:
os.remove(chunk_path)
except Exception:
pass
return {
"text": " ".join([result["text"] for result in results]),
}
def compress_audio(file_path): def compress_audio(file_path):
if os.path.getsize(file_path) > MAX_FILE_SIZE: if os.path.getsize(file_path) > MAX_FILE_SIZE:
id = os.path.splitext(os.path.basename(file_path))[
0
] # Handles names with multiple dots
file_dir = os.path.dirname(file_path) file_dir = os.path.dirname(file_path)
audio = AudioSegment.from_file(file_path) audio = AudioSegment.from_file(file_path)
audio = audio.set_frame_rate(16000).set_channels(1) # Compress audio audio = audio.set_frame_rate(16000).set_channels(1) # Compress audio
compressed_path = f"{file_dir}/{id}_compressed.opus"
audio.export(compressed_path, format="opus", bitrate="32k")
log.debug(f"Compressed audio to {compressed_path}")
if ( compressed_path = os.path.join(file_dir, f"{id}_compressed.mp3")
os.path.getsize(compressed_path) > MAX_FILE_SIZE audio.export(compressed_path, format="mp3", bitrate="32k")
): # Still larger than MAX_FILE_SIZE after compression # log.debug(f"Compressed audio to {compressed_path}") # Uncomment if log is defined
raise Exception(ERROR_MESSAGES.FILE_TOO_LARGE(size=f"{MAX_FILE_SIZE_MB}MB"))
return compressed_path return compressed_path
else: else:
return file_path return file_path
def split_audio(file_path, max_bytes, format="mp3", bitrate="32k"):
"""
Splits audio into chunks not exceeding max_bytes.
Returns a list of chunk file paths. If audio fits, returns list with original path.
"""
file_size = os.path.getsize(file_path)
if file_size <= max_bytes:
return [file_path] # Nothing to split
audio = AudioSegment.from_file(file_path)
duration_ms = len(audio)
orig_size = file_size
approx_chunk_ms = max(int(duration_ms * (max_bytes / orig_size)) - 1000, 1000)
chunks = []
start = 0
i = 0
base, _ = os.path.splitext(file_path)
while start < duration_ms:
end = min(start + approx_chunk_ms, duration_ms)
chunk = audio[start:end]
chunk_path = f"{base}_chunk_{i}.{format}"
chunk.export(chunk_path, format=format, bitrate=bitrate)
# Reduce chunk duration if still too large
while os.path.getsize(chunk_path) > max_bytes and (end - start) > 5000:
end = start + ((end - start) // 2)
chunk = audio[start:end]
chunk.export(chunk_path, format=format, bitrate=bitrate)
if os.path.getsize(chunk_path) > max_bytes:
os.remove(chunk_path)
raise Exception("Audio chunk cannot be reduced below max file size.")
chunks.append(chunk_path)
start = end
i += 1
return chunks
@router.post("/transcriptions") @router.post("/transcriptions")
def transcription( def transcription(
request: Request, request: Request,
@ -803,6 +900,7 @@ def transcription(
"audio/ogg", "audio/ogg",
"audio/x-m4a", "audio/x-m4a",
"audio/webm", "audio/webm",
"video/webm",
) )
if not file.content_type.startswith(supported_filetypes): if not file.content_type.startswith(supported_filetypes):
@ -826,19 +924,13 @@ def transcription(
f.write(contents) f.write(contents)
try: try:
try: result = transcribe(request, file_path)
file_path = compress_audio(file_path)
except Exception as e:
log.exception(e)
raise HTTPException( return {
status_code=status.HTTP_400_BAD_REQUEST, **result,
detail=ERROR_MESSAGES.DEFAULT(e), "filename": os.path.basename(file_path),
) }
data = transcribe(request, file_path)
file_path = file_path.split("/")[-1]
return {**data, "filename": file_path}
except Exception as e: except Exception as e:
log.exception(e) log.exception(e)

View File

@ -31,7 +31,7 @@ from open_webui.env import (
SRC_LOG_LEVELS, SRC_LOG_LEVELS,
) )
from fastapi import APIRouter, Depends, HTTPException, Request, status from fastapi import APIRouter, Depends, HTTPException, Request, status
from fastapi.responses import RedirectResponse, Response from fastapi.responses import RedirectResponse, Response, JSONResponse
from open_webui.config import OPENID_PROVIDER_URL, ENABLE_OAUTH_SIGNUP, ENABLE_LDAP from open_webui.config import OPENID_PROVIDER_URL, ENABLE_OAUTH_SIGNUP, ENABLE_LDAP
from pydantic import BaseModel from pydantic import BaseModel
@ -51,7 +51,7 @@ from open_webui.utils.access_control import get_permissions
from typing import Optional, List from typing import Optional, List
from ssl import CERT_REQUIRED, PROTOCOL_TLS from ssl import CERT_NONE, CERT_REQUIRED, PROTOCOL_TLS
if ENABLE_LDAP.value: if ENABLE_LDAP.value:
from ldap3 import Server, Connection, NONE, Tls from ldap3 import Server, Connection, NONE, Tls
@ -186,6 +186,9 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
LDAP_APP_PASSWORD = request.app.state.config.LDAP_APP_PASSWORD LDAP_APP_PASSWORD = request.app.state.config.LDAP_APP_PASSWORD
LDAP_USE_TLS = request.app.state.config.LDAP_USE_TLS LDAP_USE_TLS = request.app.state.config.LDAP_USE_TLS
LDAP_CA_CERT_FILE = request.app.state.config.LDAP_CA_CERT_FILE LDAP_CA_CERT_FILE = request.app.state.config.LDAP_CA_CERT_FILE
LDAP_VALIDATE_CERT = (
CERT_REQUIRED if request.app.state.config.LDAP_VALIDATE_CERT else CERT_NONE
)
LDAP_CIPHERS = ( LDAP_CIPHERS = (
request.app.state.config.LDAP_CIPHERS request.app.state.config.LDAP_CIPHERS
if request.app.state.config.LDAP_CIPHERS if request.app.state.config.LDAP_CIPHERS
@ -197,7 +200,7 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
try: try:
tls = Tls( tls = Tls(
validate=CERT_REQUIRED, validate=LDAP_VALIDATE_CERT,
version=PROTOCOL_TLS, version=PROTOCOL_TLS,
ca_certs_file=LDAP_CA_CERT_FILE, ca_certs_file=LDAP_CA_CERT_FILE,
ciphers=LDAP_CIPHERS, ciphers=LDAP_CIPHERS,
@ -478,10 +481,6 @@ async def signup(request: Request, response: Response, form_data: SignupForm):
"admin" if user_count == 0 else request.app.state.config.DEFAULT_USER_ROLE "admin" if user_count == 0 else request.app.state.config.DEFAULT_USER_ROLE
) )
if user_count == 0:
# Disable signup after the first user is created
request.app.state.config.ENABLE_SIGNUP = False
# The password passed to bcrypt must be 72 bytes or fewer. If it is longer, it will be truncated before hashing. # The password passed to bcrypt must be 72 bytes or fewer. If it is longer, it will be truncated before hashing.
if len(form_data.password.encode("utf-8")) > 72: if len(form_data.password.encode("utf-8")) > 72:
raise HTTPException( raise HTTPException(
@ -541,6 +540,10 @@ async def signup(request: Request, response: Response, form_data: SignupForm):
user.id, request.app.state.config.USER_PERMISSIONS user.id, request.app.state.config.USER_PERMISSIONS
) )
if user_count == 0:
# Disable signup after the first user is created
request.app.state.config.ENABLE_SIGNUP = False
return { return {
"token": token, "token": token,
"token_type": "Bearer", "token_type": "Bearer",
@ -574,9 +577,14 @@ async def signout(request: Request, response: Response):
logout_url = openid_data.get("end_session_endpoint") logout_url = openid_data.get("end_session_endpoint")
if logout_url: if logout_url:
response.delete_cookie("oauth_id_token") response.delete_cookie("oauth_id_token")
return RedirectResponse(
return JSONResponse(
status_code=200,
content={
"status": True,
"redirect_url": f"{logout_url}?id_token_hint={oauth_id_token}",
},
headers=response.headers, headers=response.headers,
url=f"{logout_url}?id_token_hint={oauth_id_token}",
) )
else: else:
raise HTTPException( raise HTTPException(
@ -591,12 +599,18 @@ async def signout(request: Request, response: Response):
) )
if WEBUI_AUTH_SIGNOUT_REDIRECT_URL: if WEBUI_AUTH_SIGNOUT_REDIRECT_URL:
return RedirectResponse( return JSONResponse(
status_code=200,
content={
"status": True,
"redirect_url": WEBUI_AUTH_SIGNOUT_REDIRECT_URL,
},
headers=response.headers, headers=response.headers,
url=WEBUI_AUTH_SIGNOUT_REDIRECT_URL,
) )
return {"status": True} return JSONResponse(
status_code=200, content={"status": True}, headers=response.headers
)
############################ ############################
@ -696,6 +710,9 @@ async def get_admin_config(request: Request, user=Depends(get_admin_user)):
"ENABLE_CHANNELS": request.app.state.config.ENABLE_CHANNELS, "ENABLE_CHANNELS": request.app.state.config.ENABLE_CHANNELS,
"ENABLE_NOTES": request.app.state.config.ENABLE_NOTES, "ENABLE_NOTES": request.app.state.config.ENABLE_NOTES,
"ENABLE_USER_WEBHOOKS": request.app.state.config.ENABLE_USER_WEBHOOKS, "ENABLE_USER_WEBHOOKS": request.app.state.config.ENABLE_USER_WEBHOOKS,
"PENDING_USER_OVERLAY_TITLE": request.app.state.config.PENDING_USER_OVERLAY_TITLE,
"PENDING_USER_OVERLAY_CONTENT": request.app.state.config.PENDING_USER_OVERLAY_CONTENT,
"RESPONSE_WATERMARK": request.app.state.config.RESPONSE_WATERMARK,
} }
@ -713,6 +730,9 @@ class AdminConfig(BaseModel):
ENABLE_CHANNELS: bool ENABLE_CHANNELS: bool
ENABLE_NOTES: bool ENABLE_NOTES: bool
ENABLE_USER_WEBHOOKS: bool ENABLE_USER_WEBHOOKS: bool
PENDING_USER_OVERLAY_TITLE: Optional[str] = None
PENDING_USER_OVERLAY_CONTENT: Optional[str] = None
RESPONSE_WATERMARK: Optional[str] = None
@router.post("/admin/config") @router.post("/admin/config")
@ -750,6 +770,15 @@ async def update_admin_config(
request.app.state.config.ENABLE_USER_WEBHOOKS = form_data.ENABLE_USER_WEBHOOKS request.app.state.config.ENABLE_USER_WEBHOOKS = form_data.ENABLE_USER_WEBHOOKS
request.app.state.config.PENDING_USER_OVERLAY_TITLE = (
form_data.PENDING_USER_OVERLAY_TITLE
)
request.app.state.config.PENDING_USER_OVERLAY_CONTENT = (
form_data.PENDING_USER_OVERLAY_CONTENT
)
request.app.state.config.RESPONSE_WATERMARK = form_data.RESPONSE_WATERMARK
return { return {
"SHOW_ADMIN_DETAILS": request.app.state.config.SHOW_ADMIN_DETAILS, "SHOW_ADMIN_DETAILS": request.app.state.config.SHOW_ADMIN_DETAILS,
"WEBUI_URL": request.app.state.config.WEBUI_URL, "WEBUI_URL": request.app.state.config.WEBUI_URL,
@ -764,6 +793,9 @@ async def update_admin_config(
"ENABLE_CHANNELS": request.app.state.config.ENABLE_CHANNELS, "ENABLE_CHANNELS": request.app.state.config.ENABLE_CHANNELS,
"ENABLE_NOTES": request.app.state.config.ENABLE_NOTES, "ENABLE_NOTES": request.app.state.config.ENABLE_NOTES,
"ENABLE_USER_WEBHOOKS": request.app.state.config.ENABLE_USER_WEBHOOKS, "ENABLE_USER_WEBHOOKS": request.app.state.config.ENABLE_USER_WEBHOOKS,
"PENDING_USER_OVERLAY_TITLE": request.app.state.config.PENDING_USER_OVERLAY_TITLE,
"PENDING_USER_OVERLAY_CONTENT": request.app.state.config.PENDING_USER_OVERLAY_CONTENT,
"RESPONSE_WATERMARK": request.app.state.config.RESPONSE_WATERMARK,
} }
@ -779,6 +811,7 @@ class LdapServerConfig(BaseModel):
search_filters: str = "" search_filters: str = ""
use_tls: bool = True use_tls: bool = True
certificate_path: Optional[str] = None certificate_path: Optional[str] = None
validate_cert: bool = True
ciphers: Optional[str] = "ALL" ciphers: Optional[str] = "ALL"
@ -796,6 +829,7 @@ async def get_ldap_server(request: Request, user=Depends(get_admin_user)):
"search_filters": request.app.state.config.LDAP_SEARCH_FILTERS, "search_filters": request.app.state.config.LDAP_SEARCH_FILTERS,
"use_tls": request.app.state.config.LDAP_USE_TLS, "use_tls": request.app.state.config.LDAP_USE_TLS,
"certificate_path": request.app.state.config.LDAP_CA_CERT_FILE, "certificate_path": request.app.state.config.LDAP_CA_CERT_FILE,
"validate_cert": request.app.state.config.LDAP_VALIDATE_CERT,
"ciphers": request.app.state.config.LDAP_CIPHERS, "ciphers": request.app.state.config.LDAP_CIPHERS,
} }
@ -831,6 +865,7 @@ async def update_ldap_server(
request.app.state.config.LDAP_SEARCH_FILTERS = form_data.search_filters request.app.state.config.LDAP_SEARCH_FILTERS = form_data.search_filters
request.app.state.config.LDAP_USE_TLS = form_data.use_tls request.app.state.config.LDAP_USE_TLS = form_data.use_tls
request.app.state.config.LDAP_CA_CERT_FILE = form_data.certificate_path request.app.state.config.LDAP_CA_CERT_FILE = form_data.certificate_path
request.app.state.config.LDAP_VALIDATE_CERT = form_data.validate_cert
request.app.state.config.LDAP_CIPHERS = form_data.ciphers request.app.state.config.LDAP_CIPHERS = form_data.ciphers
return { return {
@ -845,6 +880,7 @@ async def update_ldap_server(
"search_filters": request.app.state.config.LDAP_SEARCH_FILTERS, "search_filters": request.app.state.config.LDAP_SEARCH_FILTERS,
"use_tls": request.app.state.config.LDAP_USE_TLS, "use_tls": request.app.state.config.LDAP_USE_TLS,
"certificate_path": request.app.state.config.LDAP_CA_CERT_FILE, "certificate_path": request.app.state.config.LDAP_CA_CERT_FILE,
"validate_cert": request.app.state.config.LDAP_VALIDATE_CERT,
"ciphers": request.app.state.config.LDAP_CIPHERS, "ciphers": request.app.state.config.LDAP_CIPHERS,
} }

View File

@ -74,13 +74,17 @@ class FeedbackUserResponse(FeedbackResponse):
@router.get("/feedbacks/all", response_model=list[FeedbackUserResponse]) @router.get("/feedbacks/all", response_model=list[FeedbackUserResponse])
async def get_all_feedbacks(user=Depends(get_admin_user)): async def get_all_feedbacks(user=Depends(get_admin_user)):
feedbacks = Feedbacks.get_all_feedbacks() feedbacks = Feedbacks.get_all_feedbacks()
return [
feedback_list = []
for feedback in feedbacks:
user = Users.get_user_by_id(feedback.user_id)
feedback_list.append(
FeedbackUserResponse( FeedbackUserResponse(
**feedback.model_dump(), **feedback.model_dump(),
user=UserResponse(**Users.get_user_by_id(feedback.user_id).model_dump()), user=UserResponse(**user.model_dump()) if user else None,
) )
for feedback in feedbacks )
] return feedback_list
@router.delete("/feedbacks/all") @router.delete("/feedbacks/all")
@ -92,12 +96,7 @@ async def delete_all_feedbacks(user=Depends(get_admin_user)):
@router.get("/feedbacks/all/export", response_model=list[FeedbackModel]) @router.get("/feedbacks/all/export", response_model=list[FeedbackModel])
async def get_all_feedbacks(user=Depends(get_admin_user)): async def get_all_feedbacks(user=Depends(get_admin_user)):
feedbacks = Feedbacks.get_all_feedbacks() feedbacks = Feedbacks.get_all_feedbacks()
return [ return feedbacks
FeedbackModel(
**feedback.model_dump(), user=Users.get_user_by_id(feedback.user_id)
)
for feedback in feedbacks
]
@router.get("/feedbacks/user", response_model=list[FeedbackUserResponse]) @router.get("/feedbacks/user", response_model=list[FeedbackUserResponse])

View File

@ -95,6 +95,20 @@ def upload_file(
unsanitized_filename = file.filename unsanitized_filename = file.filename
filename = os.path.basename(unsanitized_filename) filename = os.path.basename(unsanitized_filename)
file_extension = os.path.splitext(filename)[1]
if request.app.state.config.ALLOWED_FILE_EXTENSIONS:
request.app.state.config.ALLOWED_FILE_EXTENSIONS = [
ext for ext in request.app.state.config.ALLOWED_FILE_EXTENSIONS if ext
]
if file_extension not in request.app.state.config.ALLOWED_FILE_EXTENSIONS:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(
f"File type {file_extension} is not allowed"
),
)
# replace filename with uuid # replace filename with uuid
id = str(uuid.uuid4()) id = str(uuid.uuid4())
name = filename name = filename
@ -125,7 +139,7 @@ def upload_file(
) )
if process: if process:
try: try:
if file.content_type:
if file.content_type.startswith( if file.content_type.startswith(
( (
"audio/mpeg", "audio/mpeg",
@ -153,6 +167,11 @@ def upload_file(
"video/quicktime", "video/quicktime",
]: ]:
process_file(request, ProcessFileForm(file_id=id), user=user) process_file(request, ProcessFileForm(file_id=id), user=user)
else:
log.info(
f"File type {file.content_type} is not provided, but trying to process anyway"
)
process_file(request, ProcessFileForm(file_id=id), user=user)
file_item = Files.get_file_by_id(id=id) file_item = Files.get_file_by_id(id=id)
except Exception as e: except Exception as e:

View File

@ -10,7 +10,7 @@ from open_webui.models.knowledge import (
KnowledgeUserResponse, KnowledgeUserResponse,
) )
from open_webui.models.files import Files, FileModel, FileMetadataResponse from open_webui.models.files import Files, FileModel, FileMetadataResponse
from open_webui.retrieval.vector.connector import VECTOR_DB_CLIENT from open_webui.retrieval.vector.factory import VECTOR_DB_CLIENT
from open_webui.routers.retrieval import ( from open_webui.routers.retrieval import (
process_file, process_file,
ProcessFileForm, ProcessFileForm,

View File

@ -4,7 +4,7 @@ import logging
from typing import Optional from typing import Optional
from open_webui.models.memories import Memories, MemoryModel from open_webui.models.memories import Memories, MemoryModel
from open_webui.retrieval.vector.connector import VECTOR_DB_CLIENT from open_webui.retrieval.vector.factory import VECTOR_DB_CLIENT
from open_webui.utils.auth import get_verified_user from open_webui.utils.auth import get_verified_user
from open_webui.env import SRC_LOG_LEVELS from open_webui.env import SRC_LOG_LEVELS

View File

@ -340,6 +340,8 @@ async def get_all_models(request: Request, user: UserModel = None):
), # Legacy support ), # Legacy support
) )
connection_type = api_config.get("connection_type", "local")
prefix_id = api_config.get("prefix_id", None) prefix_id = api_config.get("prefix_id", None)
tags = api_config.get("tags", []) tags = api_config.get("tags", [])
model_ids = api_config.get("model_ids", []) model_ids = api_config.get("model_ids", [])
@ -352,14 +354,16 @@ async def get_all_models(request: Request, user: UserModel = None):
) )
) )
if prefix_id:
for model in response.get("models", []): for model in response.get("models", []):
if prefix_id:
model["model"] = f"{prefix_id}.{model['model']}" model["model"] = f"{prefix_id}.{model['model']}"
if tags: if tags:
for model in response.get("models", []):
model["tags"] = tags model["tags"] = tags
if connection_type:
model["connection_type"] = connection_type
def merge_models_lists(model_lists): def merge_models_lists(model_lists):
merged_models = {} merged_models = {}
@ -1585,7 +1589,9 @@ async def upload_model(
if url_idx is None: if url_idx is None:
url_idx = 0 url_idx = 0
ollama_url = request.app.state.config.OLLAMA_BASE_URLS[url_idx] ollama_url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
file_path = os.path.join(UPLOAD_DIR, file.filename)
filename = os.path.basename(file.filename)
file_path = os.path.join(UPLOAD_DIR, filename)
os.makedirs(UPLOAD_DIR, exist_ok=True) os.makedirs(UPLOAD_DIR, exist_ok=True)
# --- P1: save file locally --- # --- P1: save file locally ---
@ -1630,13 +1636,13 @@ async def upload_model(
os.remove(file_path) os.remove(file_path)
# Create model in ollama # Create model in ollama
model_name, ext = os.path.splitext(file.filename) model_name, ext = os.path.splitext(filename)
log.info(f"Created Model: {model_name}") # DEBUG log.info(f"Created Model: {model_name}") # DEBUG
create_payload = { create_payload = {
"model": model_name, "model": model_name,
# Reference the file by its original name => the uploaded blob's digest # Reference the file by its original name => the uploaded blob's digest
"files": {file.filename: f"sha256:{file_hash}"}, "files": {filename: f"sha256:{file_hash}"},
} }
log.info(f"Model Payload: {create_payload}") # DEBUG log.info(f"Model Payload: {create_payload}") # DEBUG
@ -1653,7 +1659,7 @@ async def upload_model(
done_msg = { done_msg = {
"done": True, "done": True,
"blob": f"sha256:{file_hash}", "blob": f"sha256:{file_hash}",
"name": file.filename, "name": filename,
"model_created": model_name, "model_created": model_name,
} }
yield f"data: {json.dumps(done_msg)}\n\n" yield f"data: {json.dumps(done_msg)}\n\n"

View File

@ -353,21 +353,22 @@ async def get_all_models_responses(request: Request, user: UserModel) -> list:
), # Legacy support ), # Legacy support
) )
connection_type = api_config.get("connection_type", "external")
prefix_id = api_config.get("prefix_id", None) prefix_id = api_config.get("prefix_id", None)
tags = api_config.get("tags", []) tags = api_config.get("tags", [])
if prefix_id:
for model in ( for model in (
response if isinstance(response, list) else response.get("data", []) response if isinstance(response, list) else response.get("data", [])
): ):
if prefix_id:
model["id"] = f"{prefix_id}.{model['id']}" model["id"] = f"{prefix_id}.{model['id']}"
if tags: if tags:
for model in (
response if isinstance(response, list) else response.get("data", [])
):
model["tags"] = tags model["tags"] = tags
if connection_type:
model["connection_type"] = connection_type
log.debug(f"get_all_models:responses() {responses}") log.debug(f"get_all_models:responses() {responses}")
return responses return responses
@ -415,6 +416,7 @@ async def get_all_models(request: Request, user: UserModel) -> dict[str, list]:
"name": model.get("name", model["id"]), "name": model.get("name", model["id"]),
"owned_by": "openai", "owned_by": "openai",
"openai": model, "openai": model,
"connection_type": model.get("connection_type", "external"),
"urlIdx": idx, "urlIdx": idx,
} }
for model in models for model in models

View File

@ -18,7 +18,7 @@ from pydantic import BaseModel
from starlette.responses import FileResponse from starlette.responses import FileResponse
from typing import Optional from typing import Optional
from open_webui.env import SRC_LOG_LEVELS from open_webui.env import SRC_LOG_LEVELS, AIOHTTP_CLIENT_SESSION_SSL
from open_webui.config import CACHE_DIR from open_webui.config import CACHE_DIR
from open_webui.constants import ERROR_MESSAGES from open_webui.constants import ERROR_MESSAGES
@ -69,7 +69,10 @@ async def process_pipeline_inlet_filter(request, payload, user, models):
async with aiohttp.ClientSession(trust_env=True) as session: async with aiohttp.ClientSession(trust_env=True) as session:
for filter in sorted_filters: for filter in sorted_filters:
urlIdx = filter.get("urlIdx") urlIdx = filter.get("urlIdx")
if urlIdx is None:
try:
urlIdx = int(urlIdx)
except:
continue continue
url = request.app.state.config.OPENAI_API_BASE_URLS[urlIdx] url = request.app.state.config.OPENAI_API_BASE_URLS[urlIdx]
@ -89,6 +92,7 @@ async def process_pipeline_inlet_filter(request, payload, user, models):
f"{url}/{filter['id']}/filter/inlet", f"{url}/{filter['id']}/filter/inlet",
headers=headers, headers=headers,
json=request_data, json=request_data,
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as response: ) as response:
payload = await response.json() payload = await response.json()
response.raise_for_status() response.raise_for_status()
@ -118,7 +122,10 @@ async def process_pipeline_outlet_filter(request, payload, user, models):
async with aiohttp.ClientSession(trust_env=True) as session: async with aiohttp.ClientSession(trust_env=True) as session:
for filter in sorted_filters: for filter in sorted_filters:
urlIdx = filter.get("urlIdx") urlIdx = filter.get("urlIdx")
if urlIdx is None:
try:
urlIdx = int(urlIdx)
except:
continue continue
url = request.app.state.config.OPENAI_API_BASE_URLS[urlIdx] url = request.app.state.config.OPENAI_API_BASE_URLS[urlIdx]
@ -138,6 +145,7 @@ async def process_pipeline_outlet_filter(request, payload, user, models):
f"{url}/{filter['id']}/filter/outlet", f"{url}/{filter['id']}/filter/outlet",
headers=headers, headers=headers,
json=request_data, json=request_data,
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as response: ) as response:
payload = await response.json() payload = await response.json()
response.raise_for_status() response.raise_for_status()
@ -197,8 +205,10 @@ async def upload_pipeline(
user=Depends(get_admin_user), user=Depends(get_admin_user),
): ):
log.info(f"upload_pipeline: urlIdx={urlIdx}, filename={file.filename}") log.info(f"upload_pipeline: urlIdx={urlIdx}, filename={file.filename}")
filename = os.path.basename(file.filename)
# Check if the uploaded file is a python file # Check if the uploaded file is a python file
if not (file.filename and file.filename.endswith(".py")): if not (filename and filename.endswith(".py")):
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
detail="Only Python (.py) files are allowed.", detail="Only Python (.py) files are allowed.",
@ -206,7 +216,7 @@ async def upload_pipeline(
upload_folder = f"{CACHE_DIR}/pipelines" upload_folder = f"{CACHE_DIR}/pipelines"
os.makedirs(upload_folder, exist_ok=True) os.makedirs(upload_folder, exist_ok=True)
file_path = os.path.join(upload_folder, file.filename) file_path = os.path.join(upload_folder, filename)
r = None r = None
try: try:

View File

@ -36,7 +36,7 @@ from open_webui.models.knowledge import Knowledges
from open_webui.storage.provider import Storage from open_webui.storage.provider import Storage
from open_webui.retrieval.vector.connector import VECTOR_DB_CLIENT from open_webui.retrieval.vector.factory import VECTOR_DB_CLIENT
# Document loaders # Document loaders
from open_webui.retrieval.loaders.main import Loader from open_webui.retrieval.loaders.main import Loader
@ -352,10 +352,13 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)):
# Content extraction settings # Content extraction settings
"CONTENT_EXTRACTION_ENGINE": request.app.state.config.CONTENT_EXTRACTION_ENGINE, "CONTENT_EXTRACTION_ENGINE": request.app.state.config.CONTENT_EXTRACTION_ENGINE,
"PDF_EXTRACT_IMAGES": request.app.state.config.PDF_EXTRACT_IMAGES, "PDF_EXTRACT_IMAGES": request.app.state.config.PDF_EXTRACT_IMAGES,
"EXTERNAL_DOCUMENT_LOADER_URL": request.app.state.config.EXTERNAL_DOCUMENT_LOADER_URL,
"EXTERNAL_DOCUMENT_LOADER_API_KEY": request.app.state.config.EXTERNAL_DOCUMENT_LOADER_API_KEY,
"TIKA_SERVER_URL": request.app.state.config.TIKA_SERVER_URL, "TIKA_SERVER_URL": request.app.state.config.TIKA_SERVER_URL,
"DOCLING_SERVER_URL": request.app.state.config.DOCLING_SERVER_URL, "DOCLING_SERVER_URL": request.app.state.config.DOCLING_SERVER_URL,
"DOCLING_OCR_ENGINE": request.app.state.config.DOCLING_OCR_ENGINE, "DOCLING_OCR_ENGINE": request.app.state.config.DOCLING_OCR_ENGINE,
"DOCLING_OCR_LANG": request.app.state.config.DOCLING_OCR_LANG, "DOCLING_OCR_LANG": request.app.state.config.DOCLING_OCR_LANG,
"DOCLING_DO_PICTURE_DESCRIPTION": request.app.state.config.DOCLING_DO_PICTURE_DESCRIPTION,
"DOCUMENT_INTELLIGENCE_ENDPOINT": request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT, "DOCUMENT_INTELLIGENCE_ENDPOINT": request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT,
"DOCUMENT_INTELLIGENCE_KEY": request.app.state.config.DOCUMENT_INTELLIGENCE_KEY, "DOCUMENT_INTELLIGENCE_KEY": request.app.state.config.DOCUMENT_INTELLIGENCE_KEY,
"MISTRAL_OCR_API_KEY": request.app.state.config.MISTRAL_OCR_API_KEY, "MISTRAL_OCR_API_KEY": request.app.state.config.MISTRAL_OCR_API_KEY,
@ -371,6 +374,7 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)):
# File upload settings # File upload settings
"FILE_MAX_SIZE": request.app.state.config.FILE_MAX_SIZE, "FILE_MAX_SIZE": request.app.state.config.FILE_MAX_SIZE,
"FILE_MAX_COUNT": request.app.state.config.FILE_MAX_COUNT, "FILE_MAX_COUNT": request.app.state.config.FILE_MAX_COUNT,
"ALLOWED_FILE_EXTENSIONS": request.app.state.config.ALLOWED_FILE_EXTENSIONS,
# Integration settings # Integration settings
"ENABLE_GOOGLE_DRIVE_INTEGRATION": request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION, "ENABLE_GOOGLE_DRIVE_INTEGRATION": request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION,
"ENABLE_ONEDRIVE_INTEGRATION": request.app.state.config.ENABLE_ONEDRIVE_INTEGRATION, "ENABLE_ONEDRIVE_INTEGRATION": request.app.state.config.ENABLE_ONEDRIVE_INTEGRATION,
@ -492,10 +496,14 @@ class ConfigForm(BaseModel):
# Content extraction settings # Content extraction settings
CONTENT_EXTRACTION_ENGINE: Optional[str] = None CONTENT_EXTRACTION_ENGINE: Optional[str] = None
PDF_EXTRACT_IMAGES: Optional[bool] = None PDF_EXTRACT_IMAGES: Optional[bool] = None
EXTERNAL_DOCUMENT_LOADER_URL: Optional[str] = None
EXTERNAL_DOCUMENT_LOADER_API_KEY: Optional[str] = None
TIKA_SERVER_URL: Optional[str] = None TIKA_SERVER_URL: Optional[str] = None
DOCLING_SERVER_URL: Optional[str] = None DOCLING_SERVER_URL: Optional[str] = None
DOCLING_OCR_ENGINE: Optional[str] = None DOCLING_OCR_ENGINE: Optional[str] = None
DOCLING_OCR_LANG: Optional[str] = None DOCLING_OCR_LANG: Optional[str] = None
DOCLING_DO_PICTURE_DESCRIPTION: Optional[bool] = None
DOCUMENT_INTELLIGENCE_ENDPOINT: Optional[str] = None DOCUMENT_INTELLIGENCE_ENDPOINT: Optional[str] = None
DOCUMENT_INTELLIGENCE_KEY: Optional[str] = None DOCUMENT_INTELLIGENCE_KEY: Optional[str] = None
MISTRAL_OCR_API_KEY: Optional[str] = None MISTRAL_OCR_API_KEY: Optional[str] = None
@ -514,6 +522,7 @@ class ConfigForm(BaseModel):
# File upload settings # File upload settings
FILE_MAX_SIZE: Optional[int] = None FILE_MAX_SIZE: Optional[int] = None
FILE_MAX_COUNT: Optional[int] = None FILE_MAX_COUNT: Optional[int] = None
ALLOWED_FILE_EXTENSIONS: Optional[List[str]] = None
# Integration settings # Integration settings
ENABLE_GOOGLE_DRIVE_INTEGRATION: Optional[bool] = None ENABLE_GOOGLE_DRIVE_INTEGRATION: Optional[bool] = None
@ -581,6 +590,16 @@ async def update_rag_config(
if form_data.PDF_EXTRACT_IMAGES is not None if form_data.PDF_EXTRACT_IMAGES is not None
else request.app.state.config.PDF_EXTRACT_IMAGES else request.app.state.config.PDF_EXTRACT_IMAGES
) )
request.app.state.config.EXTERNAL_DOCUMENT_LOADER_URL = (
form_data.EXTERNAL_DOCUMENT_LOADER_URL
if form_data.EXTERNAL_DOCUMENT_LOADER_URL is not None
else request.app.state.config.EXTERNAL_DOCUMENT_LOADER_URL
)
request.app.state.config.EXTERNAL_DOCUMENT_LOADER_API_KEY = (
form_data.EXTERNAL_DOCUMENT_LOADER_API_KEY
if form_data.EXTERNAL_DOCUMENT_LOADER_API_KEY is not None
else request.app.state.config.EXTERNAL_DOCUMENT_LOADER_API_KEY
)
request.app.state.config.TIKA_SERVER_URL = ( request.app.state.config.TIKA_SERVER_URL = (
form_data.TIKA_SERVER_URL form_data.TIKA_SERVER_URL
if form_data.TIKA_SERVER_URL is not None if form_data.TIKA_SERVER_URL is not None
@ -601,6 +620,13 @@ async def update_rag_config(
if form_data.DOCLING_OCR_LANG is not None if form_data.DOCLING_OCR_LANG is not None
else request.app.state.config.DOCLING_OCR_LANG else request.app.state.config.DOCLING_OCR_LANG
) )
request.app.state.config.DOCLING_DO_PICTURE_DESCRIPTION = (
form_data.DOCLING_DO_PICTURE_DESCRIPTION
if form_data.DOCLING_DO_PICTURE_DESCRIPTION is not None
else request.app.state.config.DOCLING_DO_PICTURE_DESCRIPTION
)
request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT = ( request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT = (
form_data.DOCUMENT_INTELLIGENCE_ENDPOINT form_data.DOCUMENT_INTELLIGENCE_ENDPOINT
if form_data.DOCUMENT_INTELLIGENCE_ENDPOINT is not None if form_data.DOCUMENT_INTELLIGENCE_ENDPOINT is not None
@ -688,6 +714,11 @@ async def update_rag_config(
if form_data.FILE_MAX_COUNT is not None if form_data.FILE_MAX_COUNT is not None
else request.app.state.config.FILE_MAX_COUNT else request.app.state.config.FILE_MAX_COUNT
) )
request.app.state.config.ALLOWED_FILE_EXTENSIONS = (
form_data.ALLOWED_FILE_EXTENSIONS
if form_data.ALLOWED_FILE_EXTENSIONS is not None
else request.app.state.config.ALLOWED_FILE_EXTENSIONS
)
# Integration settings # Integration settings
request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION = ( request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION = (
@ -809,10 +840,13 @@ async def update_rag_config(
# Content extraction settings # Content extraction settings
"CONTENT_EXTRACTION_ENGINE": request.app.state.config.CONTENT_EXTRACTION_ENGINE, "CONTENT_EXTRACTION_ENGINE": request.app.state.config.CONTENT_EXTRACTION_ENGINE,
"PDF_EXTRACT_IMAGES": request.app.state.config.PDF_EXTRACT_IMAGES, "PDF_EXTRACT_IMAGES": request.app.state.config.PDF_EXTRACT_IMAGES,
"EXTERNAL_DOCUMENT_LOADER_URL": request.app.state.config.EXTERNAL_DOCUMENT_LOADER_URL,
"EXTERNAL_DOCUMENT_LOADER_API_KEY": request.app.state.config.EXTERNAL_DOCUMENT_LOADER_API_KEY,
"TIKA_SERVER_URL": request.app.state.config.TIKA_SERVER_URL, "TIKA_SERVER_URL": request.app.state.config.TIKA_SERVER_URL,
"DOCLING_SERVER_URL": request.app.state.config.DOCLING_SERVER_URL, "DOCLING_SERVER_URL": request.app.state.config.DOCLING_SERVER_URL,
"DOCLING_OCR_ENGINE": request.app.state.config.DOCLING_OCR_ENGINE, "DOCLING_OCR_ENGINE": request.app.state.config.DOCLING_OCR_ENGINE,
"DOCLING_OCR_LANG": request.app.state.config.DOCLING_OCR_LANG, "DOCLING_OCR_LANG": request.app.state.config.DOCLING_OCR_LANG,
"DOCLING_DO_PICTURE_DESCRIPTION": request.app.state.config.DOCLING_DO_PICTURE_DESCRIPTION,
"DOCUMENT_INTELLIGENCE_ENDPOINT": request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT, "DOCUMENT_INTELLIGENCE_ENDPOINT": request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT,
"DOCUMENT_INTELLIGENCE_KEY": request.app.state.config.DOCUMENT_INTELLIGENCE_KEY, "DOCUMENT_INTELLIGENCE_KEY": request.app.state.config.DOCUMENT_INTELLIGENCE_KEY,
"MISTRAL_OCR_API_KEY": request.app.state.config.MISTRAL_OCR_API_KEY, "MISTRAL_OCR_API_KEY": request.app.state.config.MISTRAL_OCR_API_KEY,
@ -828,6 +862,7 @@ async def update_rag_config(
# File upload settings # File upload settings
"FILE_MAX_SIZE": request.app.state.config.FILE_MAX_SIZE, "FILE_MAX_SIZE": request.app.state.config.FILE_MAX_SIZE,
"FILE_MAX_COUNT": request.app.state.config.FILE_MAX_COUNT, "FILE_MAX_COUNT": request.app.state.config.FILE_MAX_COUNT,
"ALLOWED_FILE_EXTENSIONS": request.app.state.config.ALLOWED_FILE_EXTENSIONS,
# Integration settings # Integration settings
"ENABLE_GOOGLE_DRIVE_INTEGRATION": request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION, "ENABLE_GOOGLE_DRIVE_INTEGRATION": request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION,
"ENABLE_ONEDRIVE_INTEGRATION": request.app.state.config.ENABLE_ONEDRIVE_INTEGRATION, "ENABLE_ONEDRIVE_INTEGRATION": request.app.state.config.ENABLE_ONEDRIVE_INTEGRATION,
@ -1129,10 +1164,13 @@ def process_file(
file_path = Storage.get_file(file_path) file_path = Storage.get_file(file_path)
loader = Loader( loader = Loader(
engine=request.app.state.config.CONTENT_EXTRACTION_ENGINE, engine=request.app.state.config.CONTENT_EXTRACTION_ENGINE,
EXTERNAL_DOCUMENT_LOADER_URL=request.app.state.config.EXTERNAL_DOCUMENT_LOADER_URL,
EXTERNAL_DOCUMENT_LOADER_API_KEY=request.app.state.config.EXTERNAL_DOCUMENT_LOADER_API_KEY,
TIKA_SERVER_URL=request.app.state.config.TIKA_SERVER_URL, TIKA_SERVER_URL=request.app.state.config.TIKA_SERVER_URL,
DOCLING_SERVER_URL=request.app.state.config.DOCLING_SERVER_URL, DOCLING_SERVER_URL=request.app.state.config.DOCLING_SERVER_URL,
DOCLING_OCR_ENGINE=request.app.state.config.DOCLING_OCR_ENGINE, DOCLING_OCR_ENGINE=request.app.state.config.DOCLING_OCR_ENGINE,
DOCLING_OCR_LANG=request.app.state.config.DOCLING_OCR_LANG, DOCLING_OCR_LANG=request.app.state.config.DOCLING_OCR_LANG,
DOCLING_DO_PICTURE_DESCRIPTION=request.app.state.config.DOCLING_DO_PICTURE_DESCRIPTION,
PDF_EXTRACT_IMAGES=request.app.state.config.PDF_EXTRACT_IMAGES, PDF_EXTRACT_IMAGES=request.app.state.config.PDF_EXTRACT_IMAGES,
DOCUMENT_INTELLIGENCE_ENDPOINT=request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT, DOCUMENT_INTELLIGENCE_ENDPOINT=request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT,
DOCUMENT_INTELLIGENCE_KEY=request.app.state.config.DOCUMENT_INTELLIGENCE_KEY, DOCUMENT_INTELLIGENCE_KEY=request.app.state.config.DOCUMENT_INTELLIGENCE_KEY,

View File

@ -20,10 +20,7 @@ from open_webui.utils.auth import get_admin_user, get_verified_user
from open_webui.constants import TASKS from open_webui.constants import TASKS
from open_webui.routers.pipelines import process_pipeline_inlet_filter from open_webui.routers.pipelines import process_pipeline_inlet_filter
from open_webui.utils.filter import (
get_sorted_filter_ids,
process_filter_functions,
)
from open_webui.utils.task import get_task_model_id from open_webui.utils.task import get_task_model_id
from open_webui.config import ( from open_webui.config import (

View File

@ -13,6 +13,8 @@ import pytz
from pytz import UTC from pytz import UTC
from typing import Optional, Union, List, Dict from typing import Optional, Union, List, Dict
from opentelemetry import trace
from open_webui.models.users import Users from open_webui.models.users import Users
from open_webui.constants import ERROR_MESSAGES from open_webui.constants import ERROR_MESSAGES
@ -194,7 +196,17 @@ def get_current_user(
status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.API_KEY_NOT_ALLOWED status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.API_KEY_NOT_ALLOWED
) )
return get_current_user_by_api_key(token) user = get_current_user_by_api_key(token)
# Add user info to current span
current_span = trace.get_current_span()
if current_span:
current_span.set_attribute("client.user.id", user.id)
current_span.set_attribute("client.user.email", user.email)
current_span.set_attribute("client.user.role", user.role)
current_span.set_attribute("client.auth.type", "api_key")
return user
# auth by jwt token # auth by jwt token
try: try:
@ -213,6 +225,14 @@ def get_current_user(
detail=ERROR_MESSAGES.INVALID_TOKEN, detail=ERROR_MESSAGES.INVALID_TOKEN,
) )
else: else:
# Add user info to current span
current_span = trace.get_current_span()
if current_span:
current_span.set_attribute("client.user.id", user.id)
current_span.set_attribute("client.user.email", user.email)
current_span.set_attribute("client.user.role", user.role)
current_span.set_attribute("client.auth.type", "jwt")
# Refresh the user's last active timestamp asynchronously # Refresh the user's last active timestamp asynchronously
# to prevent blocking the request # to prevent blocking the request
if background_tasks: if background_tasks:
@ -234,6 +254,14 @@ def get_current_user_by_api_key(api_key: str):
detail=ERROR_MESSAGES.INVALID_TOKEN, detail=ERROR_MESSAGES.INVALID_TOKEN,
) )
else: else:
# Add user info to current span
current_span = trace.get_current_span()
if current_span:
current_span.set_attribute("client.user.id", user.id)
current_span.set_attribute("client.user.email", user.email)
current_span.set_attribute("client.user.role", user.role)
current_span.set_attribute("client.auth.type", "api_key")
Users.update_user_last_active_by_id(user.id) Users.update_user_last_active_by_id(user.id)
return user return user

View File

@ -309,6 +309,7 @@ async def chat_completed(request: Request, form_data: dict, user: Any):
metadata = { metadata = {
"chat_id": data["chat_id"], "chat_id": data["chat_id"],
"message_id": data["id"], "message_id": data["id"],
"filter_ids": data.get("filter_ids", []),
"session_id": data["session_id"], "session_id": data["session_id"],
"user_id": user.id, "user_id": user.id,
} }
@ -330,7 +331,9 @@ async def chat_completed(request: Request, form_data: dict, user: Any):
try: try:
filter_functions = [ filter_functions = [
Functions.get_function_by_id(filter_id) Functions.get_function_by_id(filter_id)
for filter_id in get_sorted_filter_ids(model) for filter_id in get_sorted_filter_ids(
request, model, metadata.get("filter_ids", [])
)
] ]
result, _ = await process_filter_functions( result, _ = await process_filter_functions(

View File

@ -9,7 +9,20 @@ log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MAIN"]) log.setLevel(SRC_LOG_LEVELS["MAIN"])
def get_sorted_filter_ids(model: dict): def get_function_module(request, function_id):
"""
Get the function module by its ID.
"""
if function_id in request.app.state.FUNCTIONS:
function_module = request.app.state.FUNCTIONS[function_id]
else:
function_module, _, _ = load_function_module_by_id(function_id)
request.app.state.FUNCTIONS[function_id] = function_module
return function_module
def get_sorted_filter_ids(request, model: dict, enabled_filter_ids: list = None):
def get_priority(function_id): def get_priority(function_id):
function = Functions.get_function_by_id(function_id) function = Functions.get_function_by_id(function_id)
if function is not None: if function is not None:
@ -21,14 +34,23 @@ def get_sorted_filter_ids(model: dict):
if "info" in model and "meta" in model["info"]: if "info" in model and "meta" in model["info"]:
filter_ids.extend(model["info"]["meta"].get("filterIds", [])) filter_ids.extend(model["info"]["meta"].get("filterIds", []))
filter_ids = list(set(filter_ids)) filter_ids = list(set(filter_ids))
active_filter_ids = [
enabled_filter_ids = [
function.id function.id
for function in Functions.get_functions_by_type("filter", active_only=True) for function in Functions.get_functions_by_type("filter", active_only=True)
] ]
filter_ids = [fid for fid in filter_ids if fid in enabled_filter_ids] for filter_id in active_filter_ids:
function_module = get_function_module(request, filter_id)
if getattr(function_module, "toggle", None) and (
filter_id not in enabled_filter_ids
):
active_filter_ids.remove(filter_id)
continue
filter_ids = [fid for fid in filter_ids if fid in active_filter_ids]
filter_ids.sort(key=get_priority) filter_ids.sort(key=get_priority)
return filter_ids return filter_ids
@ -43,12 +65,7 @@ async def process_filter_functions(
if not filter: if not filter:
continue continue
if filter_id in request.app.state.FUNCTIONS: function_module = get_function_module(request, filter_id)
function_module = request.app.state.FUNCTIONS[filter_id]
else:
function_module, _, _ = load_function_module_by_id(filter_id)
request.app.state.FUNCTIONS[filter_id] = function_module
# Prepare handler function # Prepare handler function
handler = getattr(function_module, filter_type, None) handler = getattr(function_module, filter_type, None)
if not handler: if not handler:

View File

@ -340,6 +340,11 @@ async def chat_web_search_handler(
log.exception(e) log.exception(e)
queries = [user_message] queries = [user_message]
# Check if generated queries are empty
if len(queries) == 1 and queries[0].strip() == "":
queries = [user_message]
# Check if queries are not found
if len(queries) == 0: if len(queries) == 0:
await event_emitter( await event_emitter(
{ {
@ -651,7 +656,7 @@ def apply_params_to_form_data(form_data, model):
convert_logit_bias_input_to_json(params["logit_bias"]) convert_logit_bias_input_to_json(params["logit_bias"])
) )
except Exception as e: except Exception as e:
print(f"Error parsing logit_bias: {e}") log.exception(f"Error parsing logit_bias: {e}")
return form_data return form_data
@ -749,9 +754,12 @@ async def process_chat_payload(request, form_data, user, metadata, model):
raise e raise e
try: try:
filter_functions = [ filter_functions = [
Functions.get_function_by_id(filter_id) Functions.get_function_by_id(filter_id)
for filter_id in get_sorted_filter_ids(model) for filter_id in get_sorted_filter_ids(
request, model, metadata.get("filter_ids", [])
)
] ]
form_data, flags = await process_filter_functions( form_data, flags = await process_filter_functions(
@ -942,21 +950,35 @@ async def process_chat_response(
message = message_map.get(metadata["message_id"]) if message_map else None message = message_map.get(metadata["message_id"]) if message_map else None
if message: if message:
messages = get_message_list(message_map, message.get("id")) message_list = get_message_list(message_map, message.get("id"))
# Remove reasoning details and files from the messages. # Remove details tags and files from the messages.
# as get_message_list creates a new list, it does not affect # as get_message_list creates a new list, it does not affect
# the original messages outside of this handler # the original messages outside of this handler
for message in messages:
message["content"] = re.sub( messages = []
r"<details\s+type=\"reasoning\"[^>]*>.*?<\/details>", for message in message_list:
content = message.get("content", "")
if isinstance(content, list):
for item in content:
if item.get("type") == "text":
content = item["text"]
break
if isinstance(content, str):
content = re.sub(
r"<details\b[^>]*>.*?<\/details>",
"", "",
message["content"], content,
flags=re.S, flags=re.S | re.I,
).strip() ).strip()
if message.get("files"): messages.append(
message["files"] = [] {
"role": message["role"],
"content": content,
}
)
if tasks and messages: if tasks and messages:
if TASKS.TITLE_GENERATION in tasks: if TASKS.TITLE_GENERATION in tasks:
@ -1169,7 +1191,9 @@ async def process_chat_response(
} }
filter_functions = [ filter_functions = [
Functions.get_function_by_id(filter_id) Functions.get_function_by_id(filter_id)
for filter_id in get_sorted_filter_ids(model) for filter_id in get_sorted_filter_ids(
request, model, metadata.get("filter_ids", [])
)
] ]
# Streaming response # Streaming response

View File

@ -49,6 +49,7 @@ async def get_all_base_models(request: Request, user: UserModel = None):
"created": int(time.time()), "created": int(time.time()),
"owned_by": "ollama", "owned_by": "ollama",
"ollama": model, "ollama": model,
"connection_type": model.get("connection_type", "local"),
"tags": model.get("tags", []), "tags": model.get("tags", []),
} }
for model in ollama_models["models"] for model in ollama_models["models"]
@ -110,6 +111,14 @@ async def get_all_models(request, user: UserModel = None):
for function in Functions.get_functions_by_type("action", active_only=True) for function in Functions.get_functions_by_type("action", active_only=True)
] ]
global_filter_ids = [
function.id for function in Functions.get_global_filter_functions()
]
enabled_filter_ids = [
function.id
for function in Functions.get_functions_by_type("filter", active_only=True)
]
custom_models = Models.get_all_models() custom_models = Models.get_all_models()
for custom_model in custom_models: for custom_model in custom_models:
if custom_model.base_model_id is None: if custom_model.base_model_id is None:
@ -125,13 +134,20 @@ async def get_all_models(request, user: UserModel = None):
model["name"] = custom_model.name model["name"] = custom_model.name
model["info"] = custom_model.model_dump() model["info"] = custom_model.model_dump()
# Set action_ids and filter_ids
action_ids = [] action_ids = []
filter_ids = []
if "info" in model and "meta" in model["info"]: if "info" in model and "meta" in model["info"]:
action_ids.extend( action_ids.extend(
model["info"]["meta"].get("actionIds", []) model["info"]["meta"].get("actionIds", [])
) )
filter_ids.extend(
model["info"]["meta"].get("filterIds", [])
)
model["action_ids"] = action_ids model["action_ids"] = action_ids
model["filter_ids"] = filter_ids
else: else:
models.remove(model) models.remove(model)
@ -140,7 +156,9 @@ async def get_all_models(request, user: UserModel = None):
): ):
owned_by = "openai" owned_by = "openai"
pipe = None pipe = None
action_ids = [] action_ids = []
filter_ids = []
for model in models: for model in models:
if ( if (
@ -154,9 +172,13 @@ async def get_all_models(request, user: UserModel = None):
if custom_model.meta: if custom_model.meta:
meta = custom_model.meta.model_dump() meta = custom_model.meta.model_dump()
if "actionIds" in meta: if "actionIds" in meta:
action_ids.extend(meta["actionIds"]) action_ids.extend(meta["actionIds"])
if "filterIds" in meta:
filter_ids.extend(meta["filterIds"])
models.append( models.append(
{ {
"id": f"{custom_model.id}", "id": f"{custom_model.id}",
@ -168,6 +190,7 @@ async def get_all_models(request, user: UserModel = None):
"preset": True, "preset": True,
**({"pipe": pipe} if pipe is not None else {}), **({"pipe": pipe} if pipe is not None else {}),
"action_ids": action_ids, "action_ids": action_ids,
"filter_ids": filter_ids,
} }
) )
@ -181,8 +204,11 @@ async def get_all_models(request, user: UserModel = None):
"id": f"{function.id}.{action['id']}", "id": f"{function.id}.{action['id']}",
"name": action.get("name", f"{function.name} ({action['id']})"), "name": action.get("name", f"{function.name} ({action['id']})"),
"description": function.meta.description, "description": function.meta.description,
"icon_url": action.get( "icon": action.get(
"icon_url", function.meta.manifest.get("icon_url", None) "icon_url",
function.meta.manifest.get("icon_url", None)
or getattr(module, "icon_url", None)
or getattr(module, "icon", None),
), ),
} }
for action in actions for action in actions
@ -193,7 +219,22 @@ async def get_all_models(request, user: UserModel = None):
"id": function.id, "id": function.id,
"name": function.name, "name": function.name,
"description": function.meta.description, "description": function.meta.description,
"icon_url": function.meta.manifest.get("icon_url", None), "icon": function.meta.manifest.get("icon_url", None)
or getattr(module, "icon_url", None)
or getattr(module, "icon", None),
}
]
# Process filter_ids to get the filters
def get_filter_items_from_module(function, module):
return [
{
"id": function.id,
"name": function.name,
"description": function.meta.description,
"icon": function.meta.manifest.get("icon_url", None)
or getattr(module, "icon_url", None)
or getattr(module, "icon", None),
} }
] ]
@ -211,6 +252,11 @@ async def get_all_models(request, user: UserModel = None):
for action_id in list(set(model.pop("action_ids", []) + global_action_ids)) for action_id in list(set(model.pop("action_ids", []) + global_action_ids))
if action_id in enabled_action_ids if action_id in enabled_action_ids
] ]
filter_ids = [
filter_id
for filter_id in list(set(model.pop("filter_ids", []) + global_filter_ids))
if filter_id in enabled_filter_ids
]
model["actions"] = [] model["actions"] = []
for action_id in action_ids: for action_id in action_ids:
@ -222,6 +268,20 @@ async def get_all_models(request, user: UserModel = None):
model["actions"].extend( model["actions"].extend(
get_action_items_from_module(action_function, function_module) get_action_items_from_module(action_function, function_module)
) )
model["filters"] = []
for filter_id in filter_ids:
filter_function = Functions.get_function_by_id(filter_id)
if filter_function is None:
raise Exception(f"Filter not found: {filter_id}")
function_module = get_function_module_by_id(filter_id)
if getattr(function_module, "toggle", None):
model["filters"].extend(
get_filter_items_from_module(filter_function, function_module)
)
log.debug(f"get_all_models() returned {len(models)} models") log.debug(f"get_all_models() returned {len(models)} models")
request.app.state.MODELS = {model["id"]: model for model in models} request.app.state.MODELS = {model["id"]: model for model in models}

View File

@ -41,6 +41,7 @@ from open_webui.config import (
) )
from open_webui.constants import ERROR_MESSAGES, WEBHOOK_MESSAGES from open_webui.constants import ERROR_MESSAGES, WEBHOOK_MESSAGES
from open_webui.env import ( from open_webui.env import (
AIOHTTP_CLIENT_SESSION_SSL,
WEBUI_NAME, WEBUI_NAME,
WEBUI_AUTH_COOKIE_SAME_SITE, WEBUI_AUTH_COOKIE_SAME_SITE,
WEBUI_AUTH_COOKIE_SECURE, WEBUI_AUTH_COOKIE_SECURE,
@ -305,8 +306,10 @@ class OAuthManager:
get_kwargs["headers"] = { get_kwargs["headers"] = {
"Authorization": f"Bearer {access_token}", "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: async with session.get(
picture_url, **get_kwargs, ssl=AIOHTTP_CLIENT_SESSION_SSL
) as resp:
if resp.ok: if resp.ok:
picture = await resp.read() picture = await resp.read()
base64_encoded_picture = base64.b64encode(picture).decode( base64_encoded_picture = base64.b64encode(picture).decode(
@ -371,7 +374,9 @@ class OAuthManager:
headers = {"Authorization": f"Bearer {access_token}"} headers = {"Authorization": f"Bearer {access_token}"}
async with aiohttp.ClientSession(trust_env=True) as session: async with aiohttp.ClientSession(trust_env=True) as session:
async with session.get( async with session.get(
"https://api.github.com/user/emails", headers=headers "https://api.github.com/user/emails",
headers=headers,
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as resp: ) as resp:
if resp.ok: if resp.ok:
emails = await resp.json() emails = await resp.json()

View File

@ -37,6 +37,7 @@ from open_webui.models.tools import Tools
from open_webui.models.users import UserModel from open_webui.models.users import UserModel
from open_webui.utils.plugin import load_tool_module_by_id from open_webui.utils.plugin import load_tool_module_by_id
from open_webui.env import ( from open_webui.env import (
SRC_LOG_LEVELS,
AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA, AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA,
AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL, AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL,
) )
@ -44,6 +45,7 @@ from open_webui.env import (
import copy import copy
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
def get_async_tool_function_and_apply_extra_params( def get_async_tool_function_and_apply_extra_params(
@ -477,7 +479,7 @@ async def get_tool_server_data(token: str, url: str) -> Dict[str, Any]:
"specs": convert_openapi_to_tool_payload(res), "specs": convert_openapi_to_tool_payload(res),
} }
print("Fetched data:", data) log.info("Fetched data:", data)
return data return data
@ -510,7 +512,7 @@ async def get_tool_servers_data(
results = [] results = []
for (idx, server, url, _), response in zip(server_entries, responses): for (idx, server, url, _), response in zip(server_entries, responses):
if isinstance(response, Exception): if isinstance(response, Exception):
print(f"Failed to connect to {url} OpenAPI tool server") log.error(f"Failed to connect to {url} OpenAPI tool server")
continue continue
results.append( results.append(
@ -620,5 +622,5 @@ async def execute_tool_server(
except Exception as err: except Exception as err:
error = str(err) error = str(err)
print("API Request Error:", error) log.exception("API Request Error:", error)
return {"error": error} return {"error": error}

View File

@ -37,7 +37,8 @@ asgiref==3.8.1
# AI libraries # AI libraries
openai openai
anthropic anthropic
google-generativeai==0.8.4 google-genai==1.15.0
google-generativeai==0.8.5
tiktoken tiktoken
langchain==0.3.24 langchain==0.3.24
@ -98,7 +99,7 @@ pytube==15.0.0
extract_msg extract_msg
pydub pydub
duckduckgo-search~=8.0.0 duckduckgo-search==8.0.2
## Google Drive ## Google Drive
google-api-python-client google-api-python-client

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "open-webui", "name": "open-webui",
"version": "0.6.9", "version": "0.6.10",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "open-webui", "name": "open-webui",
"version": "0.6.9", "version": "0.6.10",
"dependencies": { "dependencies": {
"@azure/msal-browser": "^4.5.0", "@azure/msal-browser": "^4.5.0",
"@codemirror/lang-javascript": "^6.2.2", "@codemirror/lang-javascript": "^6.2.2",

View File

@ -1,11 +1,12 @@
{ {
"name": "open-webui", "name": "open-webui",
"version": "0.6.9", "version": "0.6.10",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "npm run pyodide:fetch && vite dev --host", "dev": "npm run pyodide:fetch && vite dev --host",
"dev:5050": "npm run pyodide:fetch && vite dev --port 5050", "dev:5050": "npm run pyodide:fetch && vite dev --port 5050",
"build": "npm run pyodide:fetch && vite build", "build": "npm run pyodide:fetch && vite build",
"build:watch": "npm run pyodide:fetch && vite build --watch",
"preview": "vite preview", "preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",

View File

@ -45,7 +45,8 @@ dependencies = [
"openai", "openai",
"anthropic", "anthropic",
"google-generativeai==0.8.4", "google-genai==1.15.0",
"google-generativeai==0.8.5",
"tiktoken", "tiktoken",
"langchain==0.3.24", "langchain==0.3.24",
@ -105,7 +106,7 @@ dependencies = [
"extract_msg", "extract_msg",
"pydub", "pydub",
"duckduckgo-search~=8.0.0", "duckduckgo-search==8.0.2",
"google-api-python-client", "google-api-python-client",
"google-auth-httplib2", "google-auth-httplib2",

View File

@ -314,12 +314,20 @@ input[type='number'] {
.ProseMirror p.is-editor-empty:first-child::before { .ProseMirror p.is-editor-empty:first-child::before {
content: attr(data-placeholder); content: attr(data-placeholder);
float: left; float: left;
color: #adb5bd; /* Below color is from tailwind, and has the proper contrast
text-gray-600 from: https://tailwindcss.com/docs/color */
color: #676767;
pointer-events: none; pointer-events: none;
@apply line-clamp-1 absolute; @apply line-clamp-1 absolute;
} }
@media (prefers-color-scheme: dark) {
.ProseMirror p.is-editor-empty:first-child::before {
color: #757575;
}
}
.ai-autocompletion::after { .ai-autocompletion::after {
color: #a0a0a0; color: #a0a0a0;

View File

@ -15,7 +15,7 @@ export const getAudioConfig = async (token: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@ -52,7 +52,7 @@ export const updateAudioConfig = async (token: string, payload: OpenAIConfigForm
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@ -83,7 +83,7 @@ export const transcribeAudio = async (token: string, file: File) => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -120,7 +120,7 @@ export const synthesizeOpenAISpeech = async (
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -152,7 +152,7 @@ export const getModels = async (token: string = ''): Promise<AvailableModelsResp
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -180,7 +180,7 @@ export const getVoices = async (token: string = '') => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });

View File

@ -15,7 +15,7 @@ export const getAdminDetails = async (token: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@ -42,7 +42,7 @@ export const getAdminConfig = async (token: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@ -70,7 +70,7 @@ export const updateAdminConfig = async (token: string, body: object) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@ -98,7 +98,7 @@ export const getSessionUser = async (token: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@ -129,7 +129,7 @@ export const ldapUserSignIn = async (user: string, password: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
@ -157,7 +157,7 @@ export const getLdapConfig = async (token: string = '') => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@ -187,7 +187,7 @@ export const updateLdapConfig = async (token: string = '', enable_ldap: boolean)
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@ -214,7 +214,7 @@ export const getLdapServer = async (token: string = '') => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@ -242,7 +242,7 @@ export const updateLdapServer = async (token: string = '', body: object) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@ -273,7 +273,7 @@ export const userSignIn = async (email: string, password: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
@ -312,7 +312,7 @@ export const userSignUp = async (
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@ -339,7 +339,7 @@ export const userSignOut = async () => {
return res; return res;
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@ -347,6 +347,7 @@ export const userSignOut = async () => {
if (error) { if (error) {
throw error; throw error;
} }
return res;
}; };
export const addUser = async ( export const addUser = async (
@ -378,7 +379,7 @@ export const addUser = async (
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@ -409,7 +410,7 @@ export const updateUserProfile = async (token: string, name: string, profileImag
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@ -440,7 +441,7 @@ export const updateUserPassword = async (token: string, password: string, newPas
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@ -467,7 +468,7 @@ export const getSignUpEnabledStatus = async (token: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@ -494,7 +495,7 @@ export const getDefaultUserRole = async (token: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@ -524,7 +525,7 @@ export const updateDefaultUserRole = async (token: string, role: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@ -551,7 +552,7 @@ export const toggleSignUpEnabledStatus = async (token: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@ -578,7 +579,7 @@ export const getJWTExpiresDuration = async (token: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@ -608,7 +609,7 @@ export const updateJWTExpiresDuration = async (token: string, duration: string)
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@ -635,7 +636,7 @@ export const createAPIKey = async (token: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@ -660,7 +661,7 @@ export const getAPIKey = async (token: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@ -685,7 +686,7 @@ export const deleteAPIKey = async (token: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });

View File

@ -28,7 +28,7 @@ export const createNewChannel = async (token: string = '', channel: ChannelForm)
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -59,7 +59,7 @@ export const getChannels = async (token: string = '') => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -90,7 +90,7 @@ export const getChannelById = async (token: string = '', channel_id: string) =>
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -126,7 +126,7 @@ export const updateChannelById = async (
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -157,7 +157,7 @@ export const deleteChannelById = async (token: string = '', channel_id: string)
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -196,7 +196,7 @@ export const getChannelMessages = async (
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -236,7 +236,7 @@ export const getChannelThreadMessages = async (
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -275,7 +275,7 @@ export const sendMessage = async (token: string = '', channel_id: string, messag
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -315,7 +315,7 @@ export const updateMessage = async (
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -355,7 +355,7 @@ export const addReaction = async (
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -395,7 +395,7 @@ export const removeReaction = async (
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -429,7 +429,7 @@ export const deleteMessage = async (token: string = '', channel_id: string, mess
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });

View File

@ -21,7 +21,7 @@ export const createNewChat = async (token: string, chat: object) => {
}) })
.catch((err) => { .catch((err) => {
error = err; error = err;
console.log(err); console.error(err);
return null; return null;
}); });
@ -61,7 +61,7 @@ export const importChat = async (
}) })
.catch((err) => { .catch((err) => {
error = err; error = err;
console.log(err); console.error(err);
return null; return null;
}); });
@ -97,7 +97,7 @@ export const getChatList = async (token: string = '', page: number | null = null
}) })
.catch((err) => { .catch((err) => {
error = err; error = err;
console.log(err); console.error(err);
return null; return null;
}); });
@ -131,7 +131,7 @@ export const getChatListByUserId = async (token: string = '', userId: string) =>
}) })
.catch((err) => { .catch((err) => {
error = err; error = err;
console.log(err); console.error(err);
return null; return null;
}); });
@ -165,7 +165,7 @@ export const getArchivedChatList = async (token: string = '') => {
}) })
.catch((err) => { .catch((err) => {
error = err; error = err;
console.log(err); console.error(err);
return null; return null;
}); });
@ -196,7 +196,7 @@ export const getAllChats = async (token: string) => {
}) })
.catch((err) => { .catch((err) => {
error = err; error = err;
console.log(err); console.error(err);
return null; return null;
}); });
@ -231,7 +231,7 @@ export const getChatListBySearchText = async (token: string, text: string, page:
}) })
.catch((err) => { .catch((err) => {
error = err; error = err;
console.log(err); console.error(err);
return null; return null;
}); });
@ -265,7 +265,7 @@ export const getChatsByFolderId = async (token: string, folderId: string) => {
}) })
.catch((err) => { .catch((err) => {
error = err; error = err;
console.log(err); console.error(err);
return null; return null;
}); });
@ -296,7 +296,7 @@ export const getAllArchivedChats = async (token: string) => {
}) })
.catch((err) => { .catch((err) => {
error = err; error = err;
console.log(err); console.error(err);
return null; return null;
}); });
@ -327,7 +327,7 @@ export const getAllUserChats = async (token: string) => {
}) })
.catch((err) => { .catch((err) => {
error = err; error = err;
console.log(err); console.error(err);
return null; return null;
}); });
@ -358,7 +358,7 @@ export const getAllTags = async (token: string) => {
}) })
.catch((err) => { .catch((err) => {
error = err; error = err;
console.log(err); console.error(err);
return null; return null;
}); });
@ -389,7 +389,7 @@ export const getPinnedChatList = async (token: string = '') => {
}) })
.catch((err) => { .catch((err) => {
error = err; error = err;
console.log(err); console.error(err);
return null; return null;
}); });
@ -426,7 +426,7 @@ export const getChatListByTagName = async (token: string = '', tagName: string)
}) })
.catch((err) => { .catch((err) => {
error = err; error = err;
console.log(err); console.error(err);
return null; return null;
}); });
@ -461,7 +461,7 @@ export const getChatById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -493,7 +493,7 @@ export const getChatByShareId = async (token: string, share_id: string) => {
.catch((err) => { .catch((err) => {
error = err; error = err;
console.log(err); console.error(err);
return null; return null;
}); });
@ -531,7 +531,7 @@ export const getChatPinnedStatusById = async (token: string, id: string) => {
error = err; error = err;
} }
console.log(err); console.error(err);
return null; return null;
}); });
@ -569,7 +569,7 @@ export const toggleChatPinnedStatusById = async (token: string, id: string) => {
error = err; error = err;
} }
console.log(err); console.error(err);
return null; return null;
}); });
@ -610,7 +610,7 @@ export const cloneChatById = async (token: string, id: string, title?: string) =
error = err; error = err;
} }
console.log(err); console.error(err);
return null; return null;
}); });
@ -648,7 +648,7 @@ export const cloneSharedChatById = async (token: string, id: string) => {
error = err; error = err;
} }
console.log(err); console.error(err);
return null; return null;
}); });
@ -680,7 +680,7 @@ export const shareChatById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err; error = err;
console.log(err); console.error(err);
return null; return null;
}); });
@ -715,7 +715,7 @@ export const updateChatFolderIdById = async (token: string, id: string, folderId
.catch((err) => { .catch((err) => {
error = err; error = err;
console.log(err); console.error(err);
return null; return null;
}); });
@ -747,7 +747,7 @@ export const archiveChatById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err; error = err;
console.log(err); console.error(err);
return null; return null;
}); });
@ -779,7 +779,7 @@ export const deleteSharedChatById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err; error = err;
console.log(err); console.error(err);
return null; return null;
}); });
@ -814,7 +814,7 @@ export const updateChatById = async (token: string, id: string, chat: object) =>
.catch((err) => { .catch((err) => {
error = err; error = err;
console.log(err); console.error(err);
return null; return null;
}); });
@ -846,7 +846,7 @@ export const deleteChatById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -878,7 +878,7 @@ export const getTagsById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err; error = err;
console.log(err); console.error(err);
return null; return null;
}); });
@ -912,7 +912,7 @@ export const addTagById = async (token: string, id: string, tagName: string) =>
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -947,7 +947,7 @@ export const deleteTagById = async (token: string, id: string, tagName: string)
.catch((err) => { .catch((err) => {
error = err; error = err;
console.log(err); console.error(err);
return null; return null;
}); });
@ -978,7 +978,7 @@ export const deleteTagsById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err; error = err;
console.log(err); console.error(err);
return null; return null;
}); });
@ -1010,7 +1010,7 @@ export const deleteAllChats = async (token: string) => {
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -1042,7 +1042,7 @@ export const archiveAllChats = async (token: string) => {
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });

View File

@ -19,7 +19,7 @@ export const importConfig = async (token: string, config) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@ -46,7 +46,7 @@ export const exportConfig = async (token: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@ -73,7 +73,7 @@ export const getDirectConnectionsConfig = async (token: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@ -103,7 +103,7 @@ export const setDirectConnectionsConfig = async (token: string, config: object)
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@ -130,7 +130,7 @@ export const getToolServerConnections = async (token: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@ -160,7 +160,7 @@ export const setToolServerConnections = async (token: string, connections: objec
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@ -190,7 +190,7 @@ export const verifyToolServerConnection = async (token: string, connection: obje
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@ -217,7 +217,7 @@ export const getCodeExecutionConfig = async (token: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@ -247,7 +247,7 @@ export const setCodeExecutionConfig = async (token: string, config: object) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@ -274,7 +274,7 @@ export const getModelsConfig = async (token: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@ -304,7 +304,7 @@ export const setModelsConfig = async (token: string, config: object) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@ -334,7 +334,7 @@ export const setDefaultPromptSuggestions = async (token: string, promptSuggestio
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@ -361,7 +361,7 @@ export const getBanners = async (token: string): Promise<Banner[]> => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@ -391,7 +391,7 @@ export const setBanners = async (token: string, banners: Banner[]) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });

View File

@ -20,7 +20,7 @@ export const getConfig = async (token: string = '') => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -51,7 +51,7 @@ export const updateConfig = async (token: string, config: object) => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -82,7 +82,7 @@ export const getAllFeedbacks = async (token: string = '') => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -113,7 +113,7 @@ export const exportAllFeedbacks = async (token: string = '') => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -144,7 +144,7 @@ export const createNewFeedback = async (token: string, feedback: object) => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -175,7 +175,7 @@ export const getFeedbackById = async (token: string, feedbackId: string) => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -206,7 +206,7 @@ export const updateFeedbackById = async (token: string, feedbackId: string, feed
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -234,7 +234,7 @@ export const deleteFeedbackById = async (token: string, feedbackId: string) => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });

View File

@ -19,7 +19,7 @@ export const uploadFile = async (token: string, file: File) => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -76,7 +76,7 @@ export const getFiles = async (token: string = '') => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -107,7 +107,7 @@ export const getFileById = async (token: string, id: string) => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -141,7 +141,7 @@ export const updateFileDataContentById = async (token: string, id: string, conte
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -168,7 +168,7 @@ export const getFileContentById = async (id: string) => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -200,7 +200,7 @@ export const deleteFileById = async (token: string, id: string) => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -231,7 +231,7 @@ export const deleteAllFiles = async (token: string) => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });

View File

@ -50,7 +50,7 @@ export const getFolders = async (token: string = '') => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -81,7 +81,7 @@ export const getFolderById = async (token: string, id: string) => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -115,7 +115,7 @@ export const updateFolderNameById = async (token: string, id: string, name: stri
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -153,7 +153,7 @@ export const updateFolderIsExpandedById = async (
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -187,7 +187,7 @@ export const updateFolderParentIdById = async (token: string, id: string, parent
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -226,7 +226,7 @@ export const updateFolderItemsById = async (token: string, id: string, items: Fo
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -257,7 +257,7 @@ export const deleteFolderById = async (token: string, id: string) => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });

View File

@ -20,7 +20,7 @@ export const createNewFunction = async (token: string, func: object) => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -51,7 +51,7 @@ export const getFunctions = async (token: string = '') => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -82,7 +82,7 @@ export const exportFunctions = async (token: string = '') => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -114,7 +114,7 @@ export const getFunctionById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -149,7 +149,7 @@ export const updateFunctionById = async (token: string, id: string, func: object
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -181,7 +181,7 @@ export const deleteFunctionById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -213,7 +213,7 @@ export const toggleFunctionById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -245,7 +245,7 @@ export const toggleGlobalById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -277,7 +277,7 @@ export const getFunctionValvesById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -309,7 +309,7 @@ export const getFunctionValvesSpecById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -344,7 +344,7 @@ export const updateFunctionValvesById = async (token: string, id: string, valves
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -376,7 +376,7 @@ export const getUserValvesById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -408,7 +408,7 @@ export const getUserValvesSpecById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -443,7 +443,7 @@ export const updateUserValvesById = async (token: string, id: string, valves: ob
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });

View File

@ -20,7 +20,7 @@ export const createNewGroup = async (token: string, group: object) => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -51,7 +51,7 @@ export const getGroups = async (token: string = '') => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -83,7 +83,7 @@ export const getGroupById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -118,7 +118,7 @@ export const updateGroupById = async (token: string, id: string, group: object)
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -150,7 +150,7 @@ export const deleteGroupById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });

View File

@ -16,7 +16,7 @@ export const getConfig = async (token: string = '') => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} else { } else {
@ -51,7 +51,7 @@ export const updateConfig = async (token: string = '', config: object) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} else { } else {
@ -83,7 +83,7 @@ export const verifyConfigUrl = async (token: string = '') => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} else { } else {
@ -115,7 +115,7 @@ export const getImageGenerationConfig = async (token: string = '') => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} else { } else {
@ -148,7 +148,7 @@ export const updateImageGenerationConfig = async (token: string = '', config: ob
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} else { } else {
@ -180,7 +180,7 @@ export const getImageGenerationModels = async (token: string = '') => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} else { } else {
@ -215,7 +215,7 @@ export const imageGenerations = async (token: string = '', prompt: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} else { } else {

View File

@ -25,7 +25,7 @@ export const getModels = async (
}) })
.catch((err) => { .catch((err) => {
error = err; error = err;
console.log(err); console.error(err);
return null; return null;
}); });
@ -173,7 +173,7 @@ export const chatCompleted = async (token: string, body: ChatCompletedForm) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} else { } else {
@ -212,7 +212,7 @@ export const chatAction = async (token: string, action_id: string, body: ChatAct
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} else { } else {
@ -244,7 +244,7 @@ export const stopTask = async (token: string, id: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} else { } else {
@ -276,7 +276,7 @@ export const getTaskIdsByChatId = async (token: string, chat_id: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} else { } else {
@ -315,7 +315,7 @@ export const getToolServerData = async (token: string, url: string) => {
} }
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} else { } else {
@ -491,7 +491,7 @@ export const getTaskConfig = async (token: string = '') => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err; error = err;
return null; return null;
}); });
@ -520,7 +520,7 @@ export const updateTaskConfig = async (token: string, config: object) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} else { } else {
@ -562,7 +562,7 @@ export const generateTitle = async (
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} }
@ -634,7 +634,7 @@ export const generateTags = async (
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} }
@ -706,7 +706,7 @@ export const generateEmoji = async (
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} }
@ -756,7 +756,7 @@ export const generateQueries = async (
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} }
@ -828,7 +828,7 @@ export const generateAutoCompletion = async (
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} }
@ -892,7 +892,7 @@ export const generateMoACompletion = async (
stream: true stream: true
}) })
}).catch((err) => { }).catch((err) => {
console.log(err); console.error(err);
error = err; error = err;
return null; return null;
}); });
@ -920,7 +920,7 @@ export const getPipelinesList = async (token: string = '') => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err; error = err;
return null; return null;
}); });
@ -954,7 +954,7 @@ export const uploadPipeline = async (token: string, file: File, urlIdx: string)
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} else { } else {
@ -990,7 +990,7 @@ export const downloadPipeline = async (token: string, url: string, urlIdx: strin
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} else { } else {
@ -1026,7 +1026,7 @@ export const deletePipeline = async (token: string, id: string, urlIdx: string)
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} else { } else {
@ -1063,7 +1063,7 @@ export const getPipelines = async (token: string, urlIdx?: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err; error = err;
return null; return null;
}); });
@ -1100,7 +1100,7 @@ export const getPipelineValves = async (token: string, pipeline_id: string, urlI
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err; error = err;
return null; return null;
}); });
@ -1136,7 +1136,7 @@ export const getPipelineValvesSpec = async (token: string, pipeline_id: string,
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err; error = err;
return null; return null;
}); });
@ -1178,7 +1178,7 @@ export const updatePipelineValves = async (
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
@ -1210,7 +1210,7 @@ export const getBackendConfig = async () => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err; error = err;
return null; return null;
}); });
@ -1236,7 +1236,7 @@ export const getChangelog = async () => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err; error = err;
return null; return null;
}); });
@ -1263,7 +1263,7 @@ export const getVersionUpdates = async (token: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err; error = err;
return null; return null;
}); });
@ -1290,7 +1290,7 @@ export const getModelFilterConfig = async (token: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err; error = err;
return null; return null;
}); });
@ -1325,7 +1325,7 @@ export const updateModelFilterConfig = async (
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err; error = err;
return null; return null;
}); });
@ -1352,7 +1352,7 @@ export const getWebhookUrl = async (token: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err; error = err;
return null; return null;
}); });
@ -1382,7 +1382,7 @@ export const updateWebhookUrl = async (token: string, url: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err; error = err;
return null; return null;
}); });
@ -1409,7 +1409,7 @@ export const getCommunitySharingEnabledStatus = async (token: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err; error = err;
return null; return null;
}); });
@ -1436,7 +1436,7 @@ export const toggleCommunitySharingEnabledStatus = async (token: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@ -1463,7 +1463,7 @@ export const getModelConfig = async (token: string): Promise<GlobalModelConfig>
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err; error = err;
return null; return null;
}); });
@ -1511,7 +1511,7 @@ export const updateModelConfig = async (token: string, config: GlobalModelConfig
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err; error = err;
return null; return null;
}); });

View File

@ -27,7 +27,7 @@ export const createNewKnowledge = async (
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -58,7 +58,7 @@ export const getKnowledgeBases = async (token: string = '') => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -89,7 +89,7 @@ export const getKnowledgeBaseList = async (token: string = '') => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -121,7 +121,7 @@ export const getKnowledgeById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -166,7 +166,7 @@ export const updateKnowledgeById = async (token: string, id: string, form: Knowl
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -201,7 +201,7 @@ export const addFileToKnowledgeById = async (token: string, id: string, fileId:
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -236,7 +236,7 @@ export const updateFileFromKnowledgeById = async (token: string, id: string, fil
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -271,7 +271,7 @@ export const removeFileFromKnowledgeById = async (token: string, id: string, fil
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -303,7 +303,7 @@ export const resetKnowledgeById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -335,7 +335,7 @@ export const deleteKnowledgeById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -363,7 +363,7 @@ export const reindexKnowledgeFiles = async (token: string) => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });

View File

@ -17,7 +17,7 @@ export const getMemories = async (token: string) => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -48,7 +48,7 @@ export const addNewMemory = async (token: string, content: string) => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -79,7 +79,7 @@ export const updateMemoryById = async (token: string, id: string, content: strin
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -110,7 +110,7 @@ export const queryMemory = async (token: string, content: string) => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -142,7 +142,7 @@ export const deleteMemoryById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -174,7 +174,7 @@ export const deleteMemoriesByUserId = async (token: string) => {
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });

View File

@ -20,7 +20,7 @@ export const getModels = async (token: string = '') => {
}) })
.catch((err) => { .catch((err) => {
error = err; error = err;
console.log(err); console.error(err);
return null; return null;
}); });
@ -51,7 +51,7 @@ export const getBaseModels = async (token: string = '') => {
}) })
.catch((err) => { .catch((err) => {
error = err; error = err;
console.log(err); console.error(err);
return null; return null;
}); });
@ -80,7 +80,7 @@ export const createNewModel = async (token: string, model: object) => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -115,7 +115,7 @@ export const getModelById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err; error = err;
console.log(err); console.error(err);
return null; return null;
}); });
@ -150,7 +150,7 @@ export const toggleModelById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err; error = err;
console.log(err); console.error(err);
return null; return null;
}); });
@ -186,7 +186,7 @@ export const updateModelById = async (token: string, id: string, model: object)
.catch((err) => { .catch((err) => {
error = err; error = err;
console.log(err); console.error(err);
return null; return null;
}); });
@ -221,7 +221,7 @@ export const deleteModelById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -253,7 +253,7 @@ export const deleteAllModels = async (token: string) => {
.catch((err) => { .catch((err) => {
error = err; error = err;
console.log(err); console.error(err);
return null; return null;
}); });

View File

@ -28,7 +28,7 @@ export const createNewNote = async (token: string, note: NoteItem) => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -59,7 +59,7 @@ export const getNotes = async (token: string = '') => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -108,7 +108,7 @@ export const getNoteById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -143,7 +143,7 @@ export const updateNoteById = async (token: string, id: string, note: NoteItem)
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -175,7 +175,7 @@ export const deleteNoteById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });

View File

@ -51,7 +51,7 @@ export const getOllamaConfig = async (token: string = '') => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} else { } else {
@ -92,7 +92,7 @@ export const updateOllamaConfig = async (token: string = '', config: OllamaConfi
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} else { } else {
@ -124,7 +124,7 @@ export const getOllamaUrls = async (token: string = '') => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} else { } else {
@ -159,7 +159,7 @@ export const updateOllamaUrls = async (token: string = '', urls: string[]) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} else { } else {
@ -191,7 +191,7 @@ export const getOllamaVersion = async (token: string, urlIdx?: number) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} else { } else {
@ -223,7 +223,7 @@ export const getOllamaModels = async (token: string = '', urlIdx: null | number
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} else { } else {
@ -268,7 +268,7 @@ export const generatePrompt = async (token: string = '', model: string, conversa
` `
}) })
}).catch((err) => { }).catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} }
@ -408,11 +408,11 @@ export const deleteModel = async (token: string, tagName: string, urlIdx: string
return res.json(); return res.json();
}) })
.then((json) => { .then((json) => {
console.log(json); console.debug(json);
return true; return true;
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err; error = err;
if ('detail' in err) { if ('detail' in err) {
@ -445,7 +445,7 @@ export const pullModel = async (token: string, tagName: string, urlIdx: number |
name: tagName name: tagName
}) })
}).catch((err) => { }).catch((err) => {
console.log(err); console.error(err);
error = err; error = err;
if ('detail' in err) { if ('detail' in err) {
@ -481,7 +481,7 @@ export const downloadModel = async (
}) })
} }
).catch((err) => { ).catch((err) => {
console.log(err); console.error(err);
error = err; error = err;
if ('detail' in err) { if ('detail' in err) {
@ -512,7 +512,7 @@ export const uploadModel = async (token: string, file: File, urlIdx: string | nu
body: formData body: formData
} }
).catch((err) => { ).catch((err) => {
console.log(err); console.error(err);
error = err; error = err;
if ('detail' in err) { if ('detail' in err) {

View File

@ -16,7 +16,7 @@ export const getOpenAIConfig = async (token: string = '') => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} else { } else {
@ -58,7 +58,7 @@ export const updateOpenAIConfig = async (token: string = '', config: OpenAIConfi
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} else { } else {
@ -90,7 +90,7 @@ export const getOpenAIUrls = async (token: string = '') => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} else { } else {
@ -125,7 +125,7 @@ export const updateOpenAIUrls = async (token: string = '', urls: string[]) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} else { } else {
@ -157,7 +157,7 @@ export const getOpenAIKeys = async (token: string = '') => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} else { } else {
@ -192,7 +192,7 @@ export const updateOpenAIKeys = async (token: string = '', keys: string[]) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
if ('detail' in err) { if ('detail' in err) {
error = err.detail; error = err.detail;
} else { } else {
@ -346,7 +346,7 @@ export const chatCompletion = async (
}, },
body: JSON.stringify(body) body: JSON.stringify(body)
}).catch((err) => { }).catch((err) => {
console.log(err); console.error(err);
error = err; error = err;
return null; return null;
}); });
@ -409,7 +409,7 @@ export const synthesizeOpenAISpeech = async (
voice: speaker voice: speaker
}) })
}).catch((err) => { }).catch((err) => {
console.log(err); console.error(err);
error = err; error = err;
return null; return null;
}); });

View File

@ -28,7 +28,7 @@ export const createNewPrompt = async (token: string, prompt: PromptItem) => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -59,7 +59,7 @@ export const getPrompts = async (token: string = '') => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -90,7 +90,7 @@ export const getPromptList = async (token: string = '') => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -122,7 +122,7 @@ export const getPromptByCommand = async (token: string, command: string) => {
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -158,7 +158,7 @@ export const updatePromptByCommand = async (token: string, prompt: PromptItem) =
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -192,7 +192,7 @@ export const deletePromptByCommand = async (token: string, command: string) => {
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });

View File

@ -15,7 +15,7 @@ export const getRAGConfig = async (token: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@ -77,7 +77,7 @@ export const updateRAGConfig = async (token: string, payload: RAGConfigForm) =>
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@ -104,7 +104,7 @@ export const getQuerySettings = async (token: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@ -140,7 +140,7 @@ export const updateQuerySettings = async (token: string, settings: QuerySettings
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@ -167,7 +167,7 @@ export const getEmbeddingConfig = async (token: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@ -209,7 +209,7 @@ export const updateEmbeddingConfig = async (token: string, payload: EmbeddingMod
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@ -236,7 +236,7 @@ export const getRerankingConfig = async (token: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@ -270,7 +270,7 @@ export const updateRerankingConfig = async (token: string, payload: RerankingMod
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@ -313,7 +313,7 @@ export const processFile = async (
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -344,7 +344,7 @@ export const processYoutubeVideo = async (token: string, url: string) => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -376,7 +376,7 @@ export const processWeb = async (token: string, collection_name: string, url: st
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -410,7 +410,7 @@ export const processWebSearch = async (
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });

View File

@ -20,7 +20,7 @@ export const createNewTool = async (token: string, tool: object) => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -51,7 +51,7 @@ export const getTools = async (token: string = '') => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -82,7 +82,7 @@ export const getToolList = async (token: string = '') => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -113,7 +113,7 @@ export const exportTools = async (token: string = '') => {
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -145,7 +145,7 @@ export const getToolById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -180,7 +180,7 @@ export const updateToolById = async (token: string, id: string, tool: object) =>
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -212,7 +212,7 @@ export const deleteToolById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -244,7 +244,7 @@ export const getToolValvesById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -276,7 +276,7 @@ export const getToolValvesSpecById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -311,7 +311,7 @@ export const updateToolValvesById = async (token: string, id: string, valves: ob
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -343,7 +343,7 @@ export const getUserValvesById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -375,7 +375,7 @@ export const getUserValvesSpecById = async (token: string, id: string) => {
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });
@ -410,7 +410,7 @@ export const updateUserValvesById = async (token: string, id: string, valves: ob
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.log(err); console.error(err);
return null; return null;
}); });

View File

@ -16,7 +16,7 @@ export const getUserGroups = async (token: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@ -43,7 +43,7 @@ export const getUserDefaultPermissions = async (token: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@ -73,7 +73,7 @@ export const updateUserDefaultPermissions = async (token: string, permissions: o
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@ -104,7 +104,7 @@ export const updateUserRole = async (token: string, id: string, role: string) =>
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@ -154,7 +154,7 @@ export const getUsers = async (
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@ -182,7 +182,7 @@ export const getAllUsers = async (token: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@ -208,7 +208,7 @@ export const getUserSettings = async (token: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@ -238,7 +238,7 @@ export const updateUserSettings = async (token: string, settings: object) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@ -265,7 +265,7 @@ export const getUserById = async (token: string, userId: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@ -291,7 +291,7 @@ export const getUserInfo = async (token: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@ -321,7 +321,7 @@ export const updateUserInfo = async (token: string, info: object) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@ -335,7 +335,7 @@ export const updateUserInfo = async (token: string, info: object) => {
export const getAndUpdateUserLocation = async (token: string) => { export const getAndUpdateUserLocation = async (token: string) => {
const location = await getUserPosition().catch((err) => { const location = await getUserPosition().catch((err) => {
console.log(err); console.error(err);
return null; return null;
}); });
@ -343,7 +343,7 @@ export const getAndUpdateUserLocation = async (token: string) => {
await updateUserInfo(token, { location: location }); await updateUserInfo(token, { location: location });
return location; return location;
} else { } else {
console.log('Failed to get user location'); console.info('Failed to get user location');
return null; return null;
} }
}; };
@ -363,7 +363,7 @@ export const deleteUserById = async (token: string, userId: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@ -403,7 +403,7 @@ export const updateUserById = async (token: string, userId: string, user: UserUp
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });

View File

@ -15,7 +15,7 @@ export const getGravatarUrl = async (token: string, email: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err; error = err;
return null; return null;
}); });
@ -41,7 +41,7 @@ export const executeCode = async (token: string, code: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err; error = err;
if (err.detail) { if (err.detail) {
@ -75,7 +75,7 @@ export const formatPythonCode = async (token: string, code: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err; error = err;
if (err.detail) { if (err.detail) {
@ -110,7 +110,7 @@ export const downloadChatAsPDF = async (token: string, title: string, messages:
return res.blob(); return res.blob();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err; error = err;
return null; return null;
}); });
@ -136,7 +136,7 @@ export const getHTMLFromMarkdown = async (token: string, md: string) => {
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err; error = err;
return null; return null;
}); });
@ -170,7 +170,7 @@ export const downloadDatabase = async (token: string) => {
window.URL.revokeObjectURL(url); window.URL.revokeObjectURL(url);
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });
@ -206,7 +206,7 @@ export const downloadLiteLLMConfig = async (token: string) => {
window.URL.revokeObjectURL(url); window.URL.revokeObjectURL(url);
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
error = err.detail; error = err.detail;
return null; return null;
}); });

View File

@ -30,6 +30,9 @@
let url = ''; let url = '';
let key = ''; let key = '';
let connectionType = 'external';
let azure = false;
let prefixId = ''; let prefixId = '';
let enable = true; let enable = true;
let tags = []; let tags = [];
@ -95,7 +98,9 @@
enable: enable, enable: enable,
tags: tags, tags: tags,
prefix_id: prefixId, prefix_id: prefixId,
model_ids: modelIds model_ids: modelIds,
connection_type: connectionType,
...(!ollama && azure ? { azure: true } : {})
} }
}; };
@ -120,6 +125,13 @@
tags = connection.config?.tags ?? []; tags = connection.config?.tags ?? [];
prefixId = connection.config?.prefix_id ?? ''; prefixId = connection.config?.prefix_id ?? '';
modelIds = connection.config?.model_ids ?? []; modelIds = connection.config?.model_ids ?? [];
if (ollama) {
connectionType = connection.config?.connection_type ?? 'local';
} else {
connectionType = connection.config?.connection_type ?? 'external';
azure = connection.config?.azure ?? false;
}
} }
}; };
@ -134,7 +146,7 @@
<Modal size="sm" bind:show> <Modal size="sm" bind:show>
<div> <div>
<div class=" flex justify-between dark:text-gray-100 px-5 pt-4 pb-2"> <div class=" flex justify-between dark:text-gray-100 px-5 pt-4 pb-1.5">
<div class=" text-lg font-medium self-center font-primary"> <div class=" text-lg font-medium self-center font-primary">
{#if edit} {#if edit}
{$i18n.t('Edit Connection')} {$i18n.t('Edit Connection')}
@ -172,6 +184,28 @@
> >
<div class="px-1"> <div class="px-1">
<div class="flex gap-2"> <div class="flex gap-2">
<div class="flex w-full justify-between items-center">
<div class=" text-xs text-gray-500">{$i18n.t('Connection Type')}</div>
<div class="">
<button
on:click={() => {
connectionType = connectionType === 'local' ? 'external' : 'local';
}}
type="button"
class=" text-xs text-gray-700 dark:text-gray-300"
>
{#if connectionType === 'local'}
{$i18n.t('Local')}
{:else}
{$i18n.t('External')}
{/if}
</button>
</div>
</div>
</div>
<div class="flex gap-2 mt-1.5">
<div class="flex flex-col w-full"> <div class="flex flex-col w-full">
<div class=" mb-0.5 text-xs text-gray-500">{$i18n.t('URL')}</div> <div class=" mb-0.5 text-xs text-gray-500">{$i18n.t('URL')}</div>

View File

@ -277,7 +277,7 @@ class Pipe:
await tick(); await tick();
if (res) { if (res) {
console.log('Code formatted successfully'); console.info('Code formatted successfully');
saveHandler(); saveHandler();
} }

View File

@ -91,7 +91,7 @@
return; return;
} }
console.log('Update embedding model attempt:', embeddingModel); console.debug('Update embedding model attempt:', embeddingModel);
updateEmbeddingModelLoading = true; updateEmbeddingModelLoading = true;
const res = await updateEmbeddingConfig(localStorage.token, { const res = await updateEmbeddingConfig(localStorage.token, {
@ -114,7 +114,7 @@
updateEmbeddingModelLoading = false; updateEmbeddingModelLoading = false;
if (res) { if (res) {
console.log('embeddingModelUpdateHandler:', res); console.debug('embeddingModelUpdateHandler:', res);
if (res.status === true) { if (res.status === true) {
toast.success($i18n.t('Embedding model set to "{{embedding_model}}"', res), { toast.success($i18n.t('Embedding model set to "{{embedding_model}}"', res), {
duration: 1000 * 10 duration: 1000 * 10
@ -124,6 +124,13 @@
}; };
const submitHandler = async () => { const submitHandler = async () => {
if (
RAGConfig.CONTENT_EXTRACTION_ENGINE === 'external' &&
RAGConfig.EXTERNAL_DOCUMENT_LOADER_URL === ''
) {
toast.error($i18n.t('External Document Loader URL required.'));
return;
}
if (RAGConfig.CONTENT_EXTRACTION_ENGINE === 'tika' && RAGConfig.TIKA_SERVER_URL === '') { if (RAGConfig.CONTENT_EXTRACTION_ENGINE === 'tika' && RAGConfig.TIKA_SERVER_URL === '') {
toast.error($i18n.t('Tika Server URL required.')); toast.error($i18n.t('Tika Server URL required.'));
return; return;
@ -163,6 +170,10 @@
await embeddingModelUpdateHandler(); await embeddingModelUpdateHandler();
} }
RAGConfig.ALLOWED_FILE_EXTENSIONS = RAGConfig.ALLOWED_FILE_EXTENSIONS.split(',')
.map((ext) => ext.trim())
.filter((ext) => ext !== '');
const res = await updateRAGConfig(localStorage.token, RAGConfig); const res = await updateRAGConfig(localStorage.token, RAGConfig);
dispatch('save'); dispatch('save');
}; };
@ -185,7 +196,10 @@
onMount(async () => { onMount(async () => {
await setEmbeddingConfig(); await setEmbeddingConfig();
RAGConfig = await getRAGConfig(localStorage.token); const config = await getRAGConfig(localStorage.token);
config.ALLOWED_FILE_EXTENSIONS = config.ALLOWED_FILE_EXTENSIONS.join(', ');
RAGConfig = config;
}); });
</script> </script>
@ -246,7 +260,7 @@
<hr class=" border-gray-100 dark:border-gray-850 my-2" /> <hr class=" border-gray-100 dark:border-gray-850 my-2" />
<div class="mb-2.5 flex flex-col w-full justify-between"> <div class="mb-2.5 flex flex-col w-full justify-between">
<div class="flex w-full justify-between"> <div class="flex w-full justify-between mb-1">
<div class="self-center text-xs font-medium"> <div class="self-center text-xs font-medium">
{$i18n.t('Content Extraction Engine')} {$i18n.t('Content Extraction Engine')}
</div> </div>
@ -256,6 +270,7 @@
bind:value={RAGConfig.CONTENT_EXTRACTION_ENGINE} bind:value={RAGConfig.CONTENT_EXTRACTION_ENGINE}
> >
<option value="">{$i18n.t('Default')}</option> <option value="">{$i18n.t('Default')}</option>
<option value="external">{$i18n.t('External')}</option>
<option value="tika">{$i18n.t('Tika')}</option> <option value="tika">{$i18n.t('Tika')}</option>
<option value="docling">{$i18n.t('Docling')}</option> <option value="docling">{$i18n.t('Docling')}</option>
<option value="document_intelligence">{$i18n.t('Document Intelligence')}</option> <option value="document_intelligence">{$i18n.t('Document Intelligence')}</option>
@ -275,11 +290,24 @@
</div> </div>
</div> </div>
</div> </div>
{:else if RAGConfig.CONTENT_EXTRACTION_ENGINE === 'external'}
<div class="my-0.5 flex gap-2 pr-2">
<input
class="flex-1 w-full text-sm bg-transparent outline-hidden"
placeholder={$i18n.t('Enter External Document Loader URL')}
bind:value={RAGConfig.EXTERNAL_DOCUMENT_LOADER_URL}
/>
<SensitiveInput
placeholder={$i18n.t('Enter External Document Loader API Key')}
required={false}
bind:value={RAGConfig.EXTERNAL_DOCUMENT_LOADER_API_KEY}
/>
</div>
{:else if RAGConfig.CONTENT_EXTRACTION_ENGINE === 'tika'} {:else if RAGConfig.CONTENT_EXTRACTION_ENGINE === 'tika'}
<div class="flex w-full mt-1"> <div class="flex w-full mt-1">
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<input <input
class="flex-1 w-full rounded-lg text-sm bg-transparent outline-hidden" class="flex-1 w-full text-sm bg-transparent outline-hidden"
placeholder={$i18n.t('Enter Tika Server URL')} placeholder={$i18n.t('Enter Tika Server URL')}
bind:value={RAGConfig.TIKA_SERVER_URL} bind:value={RAGConfig.TIKA_SERVER_URL}
/> />
@ -288,27 +316,38 @@
{:else if RAGConfig.CONTENT_EXTRACTION_ENGINE === 'docling'} {:else if RAGConfig.CONTENT_EXTRACTION_ENGINE === 'docling'}
<div class="flex w-full mt-1"> <div class="flex w-full mt-1">
<input <input
class="flex-1 w-full rounded-lg text-sm bg-transparent outline-hidden" class="flex-1 w-full text-sm bg-transparent outline-hidden"
placeholder={$i18n.t('Enter Docling Server URL')} placeholder={$i18n.t('Enter Docling Server URL')}
bind:value={RAGConfig.DOCLING_SERVER_URL} bind:value={RAGConfig.DOCLING_SERVER_URL}
/> />
</div> </div>
<div class="flex w-full mt-2"> <div class="flex w-full mt-2">
<input <input
class="flex-1 w-full rounded-lg text-sm bg-transparent outline-hidden" class="flex-1 w-full text-sm bg-transparent outline-hidden"
placeholder={$i18n.t('Enter Docling OCR Engine')} placeholder={$i18n.t('Enter Docling OCR Engine')}
bind:value={RAGConfig.DOCLING_OCR_ENGINE} bind:value={RAGConfig.DOCLING_OCR_ENGINE}
/> />
<input <input
class="flex-1 w-full rounded-lg text-sm bg-transparent outline-hidden" class="flex-1 w-full text-sm bg-transparent outline-hidden"
placeholder={$i18n.t('Enter Docling OCR Language(s)')} placeholder={$i18n.t('Enter Docling OCR Language(s)')}
bind:value={RAGConfig.DOCLING_OCR_LANG} bind:value={RAGConfig.DOCLING_OCR_LANG}
/> />
</div> </div>
<div class="flex w-full mt-2">
<div class="flex-1 flex justify-between">
<div class=" self-center text-xs font-medium">
{$i18n.t('Describe Pictures in Documents')}
</div>
<div class="flex items-center relative">
<Switch bind:state={RAGConfig.DOCLING_DO_PICTURE_DESCRIPTION} />
</div>
</div>
</div>
{:else if RAGConfig.CONTENT_EXTRACTION_ENGINE === 'document_intelligence'} {:else if RAGConfig.CONTENT_EXTRACTION_ENGINE === 'document_intelligence'}
<div class="my-0.5 flex gap-2 pr-2"> <div class="my-0.5 flex gap-2 pr-2">
<input <input
class="flex-1 w-full rounded-lg text-sm bg-transparent outline-hidden" class="flex-1 w-full text-sm bg-transparent outline-hidden"
placeholder={$i18n.t('Enter Document Intelligence Endpoint')} placeholder={$i18n.t('Enter Document Intelligence Endpoint')}
bind:value={RAGConfig.DOCUMENT_INTELLIGENCE_ENDPOINT} bind:value={RAGConfig.DOCUMENT_INTELLIGENCE_ENDPOINT}
/> />
@ -437,7 +476,7 @@
{#if embeddingEngine === 'openai'} {#if embeddingEngine === 'openai'}
<div class="my-0.5 flex gap-2 pr-2"> <div class="my-0.5 flex gap-2 pr-2">
<input <input
class="flex-1 w-full rounded-lg text-sm bg-transparent outline-hidden" class="flex-1 w-full text-sm bg-transparent outline-hidden"
placeholder={$i18n.t('API Base URL')} placeholder={$i18n.t('API Base URL')}
bind:value={OpenAIUrl} bind:value={OpenAIUrl}
required required
@ -448,7 +487,7 @@
{:else if embeddingEngine === 'ollama'} {:else if embeddingEngine === 'ollama'}
<div class="my-0.5 flex gap-2 pr-2"> <div class="my-0.5 flex gap-2 pr-2">
<input <input
class="flex-1 w-full rounded-lg text-sm bg-transparent outline-hidden" class="flex-1 w-full text-sm bg-transparent outline-hidden"
placeholder={$i18n.t('API Base URL')} placeholder={$i18n.t('API Base URL')}
bind:value={OllamaUrl} bind:value={OllamaUrl}
required required
@ -471,7 +510,7 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<input <input
class="flex-1 w-full rounded-lg text-sm bg-transparent outline-hidden" class="flex-1 w-full text-sm bg-transparent outline-hidden"
bind:value={embeddingModel} bind:value={embeddingModel}
placeholder={$i18n.t('Set embedding model')} placeholder={$i18n.t('Set embedding model')}
required required
@ -482,7 +521,7 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<input <input
class="flex-1 w-full rounded-lg text-sm bg-transparent outline-hidden" class="flex-1 w-full text-sm bg-transparent outline-hidden"
placeholder={$i18n.t('Set embedding model (e.g. {{model}})', { placeholder={$i18n.t('Set embedding model (e.g. {{model}})', {
model: embeddingModel.slice(-40) model: embeddingModel.slice(-40)
})} })}
@ -639,7 +678,7 @@
{#if RAGConfig.RAG_RERANKING_ENGINE === 'external'} {#if RAGConfig.RAG_RERANKING_ENGINE === 'external'}
<div class="my-0.5 flex gap-2 pr-2"> <div class="my-0.5 flex gap-2 pr-2">
<input <input
class="flex-1 w-full rounded-lg text-sm bg-transparent outline-hidden" class="flex-1 w-full text-sm bg-transparent outline-hidden"
placeholder={$i18n.t('API Base URL')} placeholder={$i18n.t('API Base URL')}
bind:value={RAGConfig.RAG_EXTERNAL_RERANKER_URL} bind:value={RAGConfig.RAG_EXTERNAL_RERANKER_URL}
required required
@ -661,7 +700,7 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<input <input
class="flex-1 w-full rounded-lg text-sm bg-transparent outline-hidden" class="flex-1 w-full text-sm bg-transparent outline-hidden"
placeholder={$i18n.t('Set reranking model (e.g. {{model}})', { placeholder={$i18n.t('Set reranking model (e.g. {{model}})', {
model: 'BAAI/bge-reranker-v2-m3' model: 'BAAI/bge-reranker-v2-m3'
})} })}
@ -677,7 +716,7 @@
<div class=" self-center text-xs font-medium">{$i18n.t('Top K')}</div> <div class=" self-center text-xs font-medium">{$i18n.t('Top K')}</div>
<div class="flex items-center relative"> <div class="flex items-center relative">
<input <input
class="flex-1 w-full rounded-lg text-sm bg-transparent outline-hidden" class="flex-1 w-full text-sm bg-transparent outline-hidden"
type="number" type="number"
placeholder={$i18n.t('Enter Top K')} placeholder={$i18n.t('Enter Top K')}
bind:value={RAGConfig.TOP_K} bind:value={RAGConfig.TOP_K}
@ -692,7 +731,7 @@
<div class="self-center text-xs font-medium">{$i18n.t('Top K Reranker')}</div> <div class="self-center text-xs font-medium">{$i18n.t('Top K Reranker')}</div>
<div class="flex items-center relative"> <div class="flex items-center relative">
<input <input
class="flex-1 w-full rounded-lg text-sm bg-transparent outline-hidden" class="flex-1 w-full text-sm bg-transparent outline-hidden"
type="number" type="number"
placeholder={$i18n.t('Enter Top K Reranker')} placeholder={$i18n.t('Enter Top K Reranker')}
bind:value={RAGConfig.TOP_K_RERANKER} bind:value={RAGConfig.TOP_K_RERANKER}
@ -711,7 +750,7 @@
</div> </div>
<div class="flex items-center relative"> <div class="flex items-center relative">
<input <input
class="flex-1 w-full rounded-lg text-sm bg-transparent outline-hidden" class="flex-1 w-full text-sm bg-transparent outline-hidden"
type="number" type="number"
step="0.01" step="0.01"
placeholder={$i18n.t('Enter Score')} placeholder={$i18n.t('Enter Score')}
@ -760,6 +799,26 @@
<hr class=" border-gray-100 dark:border-gray-850 my-2" /> <hr class=" border-gray-100 dark:border-gray-850 my-2" />
<div class=" mb-2.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Allowed File Extensions')}</div>
<div class="flex items-center relative">
<Tooltip
content={$i18n.t(
'Allowed file extensions for upload. Separate multiple extensions with commas. Leave empty for all file types.'
)}
placement="top-start"
>
<input
class="flex-1 w-full text-sm bg-transparent outline-hidden"
type="text"
placeholder={$i18n.t('e.g. pdf, docx, txt')}
bind:value={RAGConfig.ALLOWED_FILE_EXTENSIONS}
autocomplete="off"
/>
</Tooltip>
</div>
</div>
<div class=" mb-2.5 flex w-full justify-between"> <div class=" mb-2.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Max Upload Size')}</div> <div class=" self-center text-xs font-medium">{$i18n.t('Max Upload Size')}</div>
<div class="flex items-center relative"> <div class="flex items-center relative">
@ -770,7 +829,7 @@
placement="top-start" placement="top-start"
> >
<input <input
class="flex-1 w-full rounded-lg text-sm bg-transparent outline-hidden" class="flex-1 w-full text-sm bg-transparent outline-hidden"
type="number" type="number"
placeholder={$i18n.t('Leave empty for unlimited')} placeholder={$i18n.t('Leave empty for unlimited')}
bind:value={RAGConfig.FILE_MAX_SIZE} bind:value={RAGConfig.FILE_MAX_SIZE}
@ -791,7 +850,7 @@
placement="top-start" placement="top-start"
> >
<input <input
class="flex-1 w-full rounded-lg text-sm bg-transparent outline-hidden" class="flex-1 w-full text-sm bg-transparent outline-hidden"
type="number" type="number"
placeholder={$i18n.t('Leave empty for unlimited')} placeholder={$i18n.t('Leave empty for unlimited')}
bind:value={RAGConfig.FILE_MAX_COUNT} bind:value={RAGConfig.FILE_MAX_COUNT}

View File

@ -18,6 +18,7 @@
import { compareVersion } from '$lib/utils'; import { compareVersion } from '$lib/utils';
import { onMount, getContext } from 'svelte'; import { onMount, getContext } from 'svelte';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import Textarea from '$lib/components/common/Textarea.svelte';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
@ -58,10 +59,10 @@
}; };
}); });
console.log(version); console.info(version);
updateAvailable = compareVersion(version.latest, version.current); updateAvailable = compareVersion(version.latest, version.current);
console.log(updateAvailable); console.info(updateAvailable);
}; };
const updateLdapServerHandler = async () => { const updateLdapServerHandler = async () => {
@ -305,6 +306,31 @@
<Switch bind:state={adminConfig.SHOW_ADMIN_DETAILS} /> <Switch bind:state={adminConfig.SHOW_ADMIN_DETAILS} />
</div> </div>
<div class="mb-2.5">
<div class=" self-center text-xs font-medium mb-2">
{$i18n.t('Pending User Overlay Title')}
</div>
<Textarea
rows={2}
placeholder={$i18n.t(
'Enter a title for the pending user info overlay. Leave empty for default.'
)}
bind:value={adminConfig.PENDING_USER_OVERLAY_TITLE}
/>
</div>
<div class="mb-2.5">
<div class=" self-center text-xs font-medium mb-2">
{$i18n.t('Pending User Overlay Content')}
</div>
<Textarea
placeholder={$i18n.t(
'Enter content for the pending user info overlay. Leave empty for default.'
)}
bind:value={adminConfig.PENDING_USER_OVERLAY_CONTENT}
/>
</div>
<div class="mb-2.5 flex w-full justify-between pr-2"> <div class="mb-2.5 flex w-full justify-between pr-2">
<div class=" self-center text-xs font-medium">{$i18n.t('Enable API Key')}</div> <div class=" self-center text-xs font-medium">{$i18n.t('Enable API Key')}</div>
@ -559,6 +585,13 @@
/> />
</div> </div>
</div> </div>
<div class="flex justify-between items-center text-xs">
<div class=" font-medium">Validate certificate</div>
<div class="mt-1">
<Switch bind:state={LDAP_SERVER.validate_cert} />
</div>
</div>
<div class="flex w-full gap-2"> <div class="flex w-full gap-2">
<div class="w-full"> <div class="w-full">
<div class=" self-center text-xs font-medium min-w-fit mb-1"> <div class=" self-center text-xs font-medium min-w-fit mb-1">
@ -625,6 +658,16 @@
<Switch bind:state={adminConfig.ENABLE_USER_WEBHOOKS} /> <Switch bind:state={adminConfig.ENABLE_USER_WEBHOOKS} />
</div> </div>
<div class="mb-2.5">
<div class=" self-center text-xs font-medium mb-2">
{$i18n.t('Response Watermark')}
</div>
<Textarea
placeholder={$i18n.t('Enter a watermark for the response. Leave empty for none.')}
bind:value={adminConfig.RESPONSE_WATERMARK}
/>
</div>
<div class="mb-2.5 w-full justify-between"> <div class="mb-2.5 w-full justify-between">
<div class="flex w-full justify-between"> <div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('WebUI URL')}</div> <div class=" self-center text-xs font-medium">{$i18n.t('WebUI URL')}</div>

View File

@ -198,14 +198,14 @@
2 2
); );
} catch (e) { } catch (e) {
console.log(e); console.error(e);
} }
} }
requiredWorkflowNodes = requiredWorkflowNodes.map((node) => { requiredWorkflowNodes = requiredWorkflowNodes.map((node) => {
const n = config.comfyui.COMFYUI_WORKFLOW_NODES.find((n) => n.type === node.type) ?? node; const n = config.comfyui.COMFYUI_WORKFLOW_NODES.find((n) => n.type === node.type) ?? node;
console.log(n); console.debug(n);
return { return {
type: n.type, type: n.type,

View File

@ -90,7 +90,7 @@
} }
}); });
console.log('models', models); console.debug('models', models);
}; };
</script> </script>
@ -108,8 +108,8 @@
<hr class=" border-gray-100 dark:border-gray-850 my-2" /> <hr class=" border-gray-100 dark:border-gray-850 my-2" />
<div class=" mb-1 font-medium flex items-center"> <div class=" mb-2 font-medium flex items-center">
<div class=" text-xs mr-1">{$i18n.t('Set Task Model')}</div> <div class=" text-xs mr-1">{$i18n.t('Task Model')}</div>
<Tooltip <Tooltip
content={$i18n.t( content={$i18n.t(
'A task model is used when performing tasks such as generating titles for chats and web search queries' 'A task model is used when performing tasks such as generating titles for chats and web search queries'
@ -134,7 +134,7 @@
<div class=" mb-2.5 flex w-full gap-2"> <div class=" mb-2.5 flex w-full gap-2">
<div class="flex-1"> <div class="flex-1">
<div class=" text-xs mb-1">{$i18n.t('Local Models')}</div> <div class=" text-xs mb-1">{$i18n.t('Local Task Model')}</div>
<select <select
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
bind:value={taskConfig.TASK_MODEL} bind:value={taskConfig.TASK_MODEL}
@ -159,7 +159,7 @@
}} }}
> >
<option value="" selected>{$i18n.t('Current Model')}</option> <option value="" selected>{$i18n.t('Current Model')}</option>
{#each models.filter((m) => m.owned_by === 'ollama') as model} {#each models.filter((m) => m.connection_type === 'local') as model}
<option value={model.id} class="bg-gray-100 dark:bg-gray-700"> <option value={model.id} class="bg-gray-100 dark:bg-gray-700">
{model.name} {model.name}
</option> </option>
@ -168,7 +168,7 @@
</div> </div>
<div class="flex-1"> <div class="flex-1">
<div class=" text-xs mb-1">{$i18n.t('External Models')}</div> <div class=" text-xs mb-1">{$i18n.t('External Task Model')}</div>
<select <select
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden" class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
bind:value={taskConfig.TASK_MODEL_EXTERNAL} bind:value={taskConfig.TASK_MODEL_EXTERNAL}

View File

@ -166,7 +166,7 @@
hidden: !(model?.meta?.hidden ?? false) hidden: !(model?.meta?.hidden ?? false)
}; };
console.log(model); console.debug(model);
toast.success( toast.success(
model.meta.hidden model.meta.hidden

View File

@ -66,7 +66,7 @@
const updateModelsHandler = async () => { const updateModelsHandler = async () => {
for (const model of ollamaModels) { for (const model of ollamaModels) {
console.log(model); console.debug(model);
updateModelId = model.id; updateModelId = model.id;
const [res, controller] = await pullModel(localStorage.token, model.id, urlIdx).catch( const [res, controller] = await pullModel(localStorage.token, model.id, urlIdx).catch(
@ -114,8 +114,8 @@
} }
} }
} }
} catch (error) { } catch (err) {
console.log(error); console.error(err);
} }
} }
} }
@ -215,13 +215,13 @@
} }
} }
} }
} catch (error) { } catch (err) {
console.log(error); console.error(err);
if (typeof error !== 'string') { if (typeof err !== 'string') {
error = error.message; err = err.message;
} }
toast.error(`${error}`); toast.error(`${err}`);
// opts.callback({ success: false, error, modelName: opts.modelName }); // opts.callback({ success: false, error, modelName: opts.modelName });
} }
} }
@ -319,8 +319,8 @@
} }
} }
} }
} catch (error) { } catch (err) {
console.log(error); console.error(err);
} }
} }
} else { } else {
@ -382,9 +382,9 @@
} }
} }
} }
} catch (error) { } catch (err) {
console.log(error); console.error(err);
toast.error(`${error}`); toast.error(`${err}`);
} }
} }
} }
@ -514,9 +514,9 @@
} }
} }
} }
} catch (error) { } catch (err) {
console.log(error); console.error(err);
toast.error(`${error}`); toast.error(`${err}`);
} }
} }
} }

View File

@ -104,7 +104,7 @@
valves_spec = null; valves_spec = null;
if (PIPELINES_LIST.length > 0) { if (PIPELINES_LIST.length > 0) {
console.log(selectedPipelinesUrlIdx); console.debug(selectedPipelinesUrlIdx);
pipelines = await getPipelines(localStorage.token, selectedPipelinesUrlIdx); pipelines = await getPipelines(localStorage.token, selectedPipelinesUrlIdx);
if (pipelines.length > 0) { if (pipelines.length > 0) {
@ -151,7 +151,7 @@
const res = await uploadPipeline(localStorage.token, file, selectedPipelinesUrlIdx).catch( const res = await uploadPipeline(localStorage.token, file, selectedPipelinesUrlIdx).catch(
(error) => { (error) => {
console.log(error); console.error(error);
toast.error('Something went wrong :/'); toast.error('Something went wrong :/');
return null; return null;
} }

View File

@ -106,7 +106,7 @@
}; };
const updateDefaultPermissionsHandler = async (group) => { const updateDefaultPermissionsHandler = async (group) => {
console.log(group.permissions); console.debug(group.permissions);
const res = await updateUserDefaultPermissions(localStorage.token, group.permissions).catch( const res = await updateUserDefaultPermissions(localStorage.token, group.permissions).catch(
(error) => { (error) => {

View File

@ -31,10 +31,6 @@
description = ''; description = '';
userIds = []; userIds = [];
}; };
onMount(() => {
console.log('mounted');
});
</script> </script>
<Modal size="sm" bind:show> <Modal size="sm" bind:show>

View File

@ -91,7 +91,6 @@
} }
onMount(() => { onMount(() => {
console.log(tabs);
selectedTab = tabs[0]; selectedTab = tabs[0];
init(); init();
}); });

View File

@ -73,7 +73,7 @@
for (const [idx, row] of rows.entries()) { for (const [idx, row] of rows.entries()) {
const columns = row.split(',').map((col) => col.trim()); const columns = row.split(',').map((col) => col.trim());
console.log(idx, columns); console.debug(idx, columns);
if (idx > 0) { if (idx > 0) {
if ( if (

View File

@ -80,7 +80,7 @@
const inputFilesHandler = async (inputFiles) => { const inputFilesHandler = async (inputFiles) => {
inputFiles.forEach((file) => { inputFiles.forEach((file) => {
console.log('Processing file:', { console.info('Processing file:', {
name: file.name, name: file.name,
type: file.type, type: file.type,
size: file.size, size: file.size,
@ -91,7 +91,7 @@
($config?.file?.max_size ?? null) !== null && ($config?.file?.max_size ?? null) !== null &&
file.size > ($config?.file?.max_size ?? 0) * 1024 * 1024 file.size > ($config?.file?.max_size ?? 0) * 1024 * 1024
) { ) {
console.log('File exceeds max size limit:', { console.error('File exceeds max size limit:', {
fileSize: file.size, fileSize: file.size,
maxSize: ($config?.file?.max_size ?? 0) * 1024 * 1024 maxSize: ($config?.file?.max_size ?? 0) * 1024 * 1024
}); });
@ -163,14 +163,14 @@
const uploadedFile = await uploadFile(localStorage.token, file); const uploadedFile = await uploadFile(localStorage.token, file);
if (uploadedFile) { if (uploadedFile) {
console.log('File upload completed:', { console.info('File upload completed:', {
id: uploadedFile.id, id: uploadedFile.id,
name: fileItem.name, name: fileItem.name,
collection: uploadedFile?.meta?.collection_name collection: uploadedFile?.meta?.collection_name
}); });
if (uploadedFile.error) { if (uploadedFile.error) {
console.warn('File upload warning:', uploadedFile.error); console.error('File upload warning:', uploadedFile.error);
toast.warning(uploadedFile.error); toast.warning(uploadedFile.error);
} }
@ -193,7 +193,6 @@
const handleKeyDown = (event: KeyboardEvent) => { const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') { if (event.key === 'Escape') {
console.log('Escape');
draggedOver = false; draggedOver = false;
} }
}; };
@ -215,7 +214,6 @@
const onDrop = async (e) => { const onDrop = async (e) => {
e.preventDefault(); e.preventDefault();
console.log(e);
if (e.dataTransfer?.files) { if (e.dataTransfer?.files) {
const inputFiles = Array.from(e.dataTransfer?.files); const inputFiles = Array.from(e.dataTransfer?.files);
@ -270,7 +268,6 @@
}); });
onDestroy(() => { onDestroy(() => {
console.log('destroy');
window.removeEventListener('keydown', handleKeyDown); window.removeEventListener('keydown', handleKeyDown);
const dropzoneElement = document.getElementById('channel-container'); const dropzoneElement = document.getElementById('channel-container');
@ -479,12 +476,12 @@
} }
if (e.key === 'Escape') { if (e.key === 'Escape') {
console.log('Escape'); console.info('Escape');
} }
}} }}
on:paste={async (e) => { on:paste={async (e) => {
e = e.detail.event; e = e.detail.event;
console.log(e); console.info(e);
}} }}
/> />
</div> </div>

View File

@ -51,7 +51,7 @@
{#if !top} {#if !top}
<Loader <Loader
on:visible={(e) => { on:visible={(e) => {
console.log('visible'); console.info('visible');
if (!messagesLoading) { if (!messagesLoading) {
loadMoreMessages(); loadMoreMessages();
} }

View File

@ -54,7 +54,7 @@
}; };
const channelEventHandler = async (event) => { const channelEventHandler = async (event) => {
console.log(event); console.debug(event);
if (event.channel_id === channel.id) { if (event.channel_id === channel.id) {
const type = event?.data?.type ?? null; const type = event?.data?.type ?? null;
const data = event?.data?.data ?? null; const data = event?.data?.data ?? null;

View File

@ -4,13 +4,15 @@
const i18n = getContext('i18n'); const i18n = getContext('i18n');
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
import { chatId, settings, showArtifacts, showControls } from '$lib/stores'; import { artifactCode, chatId, settings, showArtifacts, showControls } from '$lib/stores';
import XMark from '../icons/XMark.svelte';
import { copyToClipboard, createMessagesList } from '$lib/utils'; import { copyToClipboard, createMessagesList } from '$lib/utils';
import XMark from '../icons/XMark.svelte';
import ArrowsPointingOut from '../icons/ArrowsPointingOut.svelte'; import ArrowsPointingOut from '../icons/ArrowsPointingOut.svelte';
import Tooltip from '../common/Tooltip.svelte'; import Tooltip from '../common/Tooltip.svelte';
import SvgPanZoom from '../common/SVGPanZoom.svelte'; import SvgPanZoom from '../common/SVGPanZoom.svelte';
import ArrowLeft from '../icons/ArrowLeft.svelte'; import ArrowLeft from '../icons/ArrowLeft.svelte';
import ArrowDownTray from '../icons/ArrowDownTray.svelte';
export let overlay = false; export let overlay = false;
export let history; export let history;
@ -154,7 +156,7 @@
url.pathname + url.search + url.hash url.pathname + url.search + url.hash
); );
} else { } else {
console.log('External navigation blocked:', url.href); console.info('External navigation blocked:', url.href);
} }
} }
}, },
@ -180,7 +182,26 @@
} }
}; };
onMount(() => {}); const downloadArtifact = () => {
const blob = new Blob([contents[selectedContentIdx].content], { type: 'text/html' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `artifact-${$chatId}-${selectedContentIdx}.html`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
onMount(() => {
artifactCode.subscribe((value) => {
if (contents) {
const codeIdx = contents.findIndex((content) => content.content.includes(value));
selectedContentIdx = codeIdx !== -1 ? codeIdx : 0;
}
});
});
</script> </script>
<div class=" w-full h-full relative flex flex-col bg-gray-50 dark:bg-gray-850"> <div class=" w-full h-full relative flex flex-col bg-gray-50 dark:bg-gray-850">
@ -198,7 +219,7 @@
<ArrowLeft className="size-3.5 text-gray-900 dark:text-white" /> <ArrowLeft className="size-3.5 text-gray-900 dark:text-white" />
</button> </button>
<div class="flex-1 flex items-center justify-between"> <div class="flex-1 flex items-center justify-between pr-1">
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<div class="flex items-center gap-0.5 self-center min-w-fit" dir="ltr"> <div class="flex items-center gap-0.5 self-center min-w-fit" dir="ltr">
<button <button
@ -252,7 +273,7 @@
</div> </div>
</div> </div>
<div class="flex items-center gap-1"> <div class="flex items-center gap-1.5">
<button <button
class="copy-code-button bg-none border-none text-xs bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-md px-1.5 py-0.5" class="copy-code-button bg-none border-none text-xs bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-md px-1.5 py-0.5"
on:click={() => { on:click={() => {
@ -265,6 +286,15 @@
}}>{copied ? $i18n.t('Copied') : $i18n.t('Copy')}</button }}>{copied ? $i18n.t('Copied') : $i18n.t('Copy')}</button
> >
<Tooltip content={$i18n.t('Download')}>
<button
class=" bg-none border-none text-xs bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-md p-0.5"
on:click={downloadArtifact}
>
<ArrowDownTray className="size-3.5" />
</button>
</Tooltip>
{#if contents[selectedContentIdx].type === 'iframe'} {#if contents[selectedContentIdx].type === 'iframe'}
<Tooltip content={$i18n.t('Open in full screen')}> <Tooltip content={$i18n.t('Open in full screen')}>
<button <button

View File

@ -119,6 +119,7 @@
$: selectedModelIds = atSelectedModel !== undefined ? [atSelectedModel.id] : selectedModels; $: selectedModelIds = atSelectedModel !== undefined ? [atSelectedModel.id] : selectedModels;
let selectedToolIds = []; let selectedToolIds = [];
let selectedFilterIds = [];
let imageGenerationEnabled = false; let imageGenerationEnabled = false;
let webSearchEnabled = false; let webSearchEnabled = false;
let codeInterpreterEnabled = false; let codeInterpreterEnabled = false;
@ -146,6 +147,7 @@
prompt = ''; prompt = '';
files = []; files = [];
selectedToolIds = []; selectedToolIds = [];
selectedFilterIds = [];
webSearchEnabled = false; webSearchEnabled = false;
imageGenerationEnabled = false; imageGenerationEnabled = false;
@ -159,6 +161,7 @@
prompt = input.prompt; prompt = input.prompt;
files = input.files; files = input.files;
selectedToolIds = input.selectedToolIds; selectedToolIds = input.selectedToolIds;
selectedFilterIds = input.selectedFilterIds;
webSearchEnabled = input.webSearchEnabled; webSearchEnabled = input.webSearchEnabled;
imageGenerationEnabled = input.imageGenerationEnabled; imageGenerationEnabled = input.imageGenerationEnabled;
codeInterpreterEnabled = input.codeInterpreterEnabled; codeInterpreterEnabled = input.codeInterpreterEnabled;
@ -192,10 +195,12 @@
$: if (selectedModels) { $: if (selectedModels) {
setToolIds(); setToolIds();
setFilterIds();
} }
$: if (atSelectedModel || selectedModels) { $: if (atSelectedModel || selectedModels) {
setToolIds(); setToolIds();
setFilterIds();
} }
const setToolIds = async () => { const setToolIds = async () => {
@ -209,9 +214,19 @@
const model = atSelectedModel ?? $models.find((m) => m.id === selectedModels[0]); const model = atSelectedModel ?? $models.find((m) => m.id === selectedModels[0]);
if (model) { if (model) {
selectedToolIds = (model?.info?.meta?.toolIds ?? []).filter((id) => selectedToolIds = [
...new Set(
[...selectedToolIds, ...(model?.info?.meta?.toolIds ?? [])].filter((id) =>
$tools.find((t) => t.id === id) $tools.find((t) => t.id === id)
); )
)
];
}
};
const setFilterIds = async () => {
if (selectedModels.length !== 1 && !atSelectedModel) {
selectedFilterIds = [];
} }
}; };
@ -424,6 +439,7 @@
prompt = ''; prompt = '';
files = []; files = [];
selectedToolIds = []; selectedToolIds = [];
selectedFilterIds = [];
webSearchEnabled = false; webSearchEnabled = false;
imageGenerationEnabled = false; imageGenerationEnabled = false;
codeInterpreterEnabled = false; codeInterpreterEnabled = false;
@ -437,6 +453,7 @@
prompt = input.prompt; prompt = input.prompt;
files = input.files; files = input.files;
selectedToolIds = input.selectedToolIds; selectedToolIds = input.selectedToolIds;
selectedFilterIds = input.selectedFilterIds;
webSearchEnabled = input.webSearchEnabled; webSearchEnabled = input.webSearchEnabled;
imageGenerationEnabled = input.imageGenerationEnabled; imageGenerationEnabled = input.imageGenerationEnabled;
codeInterpreterEnabled = input.codeInterpreterEnabled; codeInterpreterEnabled = input.codeInterpreterEnabled;
@ -881,6 +898,7 @@
...(m.usage ? { usage: m.usage } : {}), ...(m.usage ? { usage: m.usage } : {}),
...(m.sources ? { sources: m.sources } : {}) ...(m.sources ? { sources: m.sources } : {})
})), })),
filter_ids: selectedFilterIds.length > 0 ? selectedFilterIds : undefined,
model_item: $models.find((m) => m.id === modelId), model_item: $models.find((m) => m.id === modelId),
chat_id: chatId, chat_id: chatId,
session_id: $socket?.id, session_id: $socket?.id,
@ -1617,6 +1635,8 @@
}, },
files: (files?.length ?? 0) > 0 ? files : undefined, files: (files?.length ?? 0) > 0 ? files : undefined,
filter_ids: selectedFilterIds.length > 0 ? selectedFilterIds : undefined,
tool_ids: selectedToolIds.length > 0 ? selectedToolIds : undefined, tool_ids: selectedToolIds.length > 0 ? selectedToolIds : undefined,
tool_servers: $toolServers, tool_servers: $toolServers,
@ -2058,6 +2078,7 @@
bind:prompt bind:prompt
bind:autoScroll bind:autoScroll
bind:selectedToolIds bind:selectedToolIds
bind:selectedFilterIds
bind:imageGenerationEnabled bind:imageGenerationEnabled
bind:codeInterpreterEnabled bind:codeInterpreterEnabled
bind:webSearchEnabled bind:webSearchEnabled
@ -2114,6 +2135,7 @@
bind:prompt bind:prompt
bind:autoScroll bind:autoScroll
bind:selectedToolIds bind:selectedToolIds
bind:selectedFilterIds
bind:imageGenerationEnabled bind:imageGenerationEnabled
bind:codeInterpreterEnabled bind:codeInterpreterEnabled
bind:webSearchEnabled bind:webSearchEnabled

View File

@ -38,6 +38,7 @@
import VoiceRecording from './MessageInput/VoiceRecording.svelte'; import VoiceRecording from './MessageInput/VoiceRecording.svelte';
import FilesOverlay from './MessageInput/FilesOverlay.svelte'; import FilesOverlay from './MessageInput/FilesOverlay.svelte';
import Commands from './MessageInput/Commands.svelte'; import Commands from './MessageInput/Commands.svelte';
import ToolServersModal from './ToolServersModal.svelte';
import RichTextInput from '../common/RichTextInput.svelte'; import RichTextInput from '../common/RichTextInput.svelte';
import Tooltip from '../common/Tooltip.svelte'; import Tooltip from '../common/Tooltip.svelte';
@ -47,12 +48,12 @@
import XMark from '../icons/XMark.svelte'; import XMark from '../icons/XMark.svelte';
import Headphone from '../icons/Headphone.svelte'; import Headphone from '../icons/Headphone.svelte';
import GlobeAlt from '../icons/GlobeAlt.svelte'; import GlobeAlt from '../icons/GlobeAlt.svelte';
import PhotoSolid from '../icons/PhotoSolid.svelte';
import Photo from '../icons/Photo.svelte'; import Photo from '../icons/Photo.svelte';
import CommandLine from '../icons/CommandLine.svelte';
import { KokoroWorker } from '$lib/workers/KokoroWorker';
import ToolServersModal from './ToolServersModal.svelte';
import Wrench from '../icons/Wrench.svelte'; import Wrench from '../icons/Wrench.svelte';
import CommandLine from '../icons/CommandLine.svelte';
import Sparkles from '../icons/Sparkles.svelte';
import { KokoroWorker } from '$lib/workers/KokoroWorker';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
@ -79,6 +80,7 @@
export let toolServers = []; export let toolServers = [];
export let selectedToolIds = []; export let selectedToolIds = [];
export let selectedFilterIds = [];
export let imageGenerationEnabled = false; export let imageGenerationEnabled = false;
export let webSearchEnabled = false; export let webSearchEnabled = false;
@ -88,6 +90,7 @@
prompt, prompt,
files: files.filter((file) => file.type !== 'image'), files: files.filter((file) => file.type !== 'image'),
selectedToolIds, selectedToolIds,
selectedFilterIds,
imageGenerationEnabled, imageGenerationEnabled,
webSearchEnabled, webSearchEnabled,
codeInterpreterEnabled codeInterpreterEnabled
@ -113,10 +116,41 @@
export let placeholder = ''; export let placeholder = '';
let visionCapableModels = []; let visionCapableModels = [];
$: visionCapableModels = [...(atSelectedModel ? [atSelectedModel] : selectedModels)].filter( $: visionCapableModels = (atSelectedModel?.id ? [atSelectedModel.id] : selectedModels).filter(
(model) => $models.find((m) => m.id === model)?.info?.meta?.capabilities?.vision ?? true (model) => $models.find((m) => m.id === model)?.info?.meta?.capabilities?.vision ?? true
); );
let fileUploadCapableModels = [];
$: fileUploadCapableModels = (atSelectedModel?.id ? [atSelectedModel.id] : selectedModels).filter(
(model) => $models.find((m) => m.id === model)?.info?.meta?.capabilities?.file_upload ?? true
);
let webSearchCapableModels = [];
$: webSearchCapableModels = (atSelectedModel?.id ? [atSelectedModel.id] : selectedModels).filter(
(model) => $models.find((m) => m.id === model)?.info?.meta?.capabilities?.web_search ?? true
);
let imageGenerationCapableModels = [];
$: imageGenerationCapableModels = (
atSelectedModel?.id ? [atSelectedModel.id] : selectedModels
).filter(
(model) =>
$models.find((m) => m.id === model)?.info?.meta?.capabilities?.image_generation ?? true
);
let codeInterpreterCapableModels = [];
$: codeInterpreterCapableModels = (
atSelectedModel?.id ? [atSelectedModel.id] : selectedModels
).filter(
(model) =>
$models.find((m) => m.id === model)?.info?.meta?.capabilities?.code_interpreter ?? true
);
let toggleFilters = [];
$: toggleFilters = (atSelectedModel?.id ? [atSelectedModel.id] : selectedModels)
.map((id) => ($models.find((model) => model.id === id) || {})?.filters ?? [])
.reduce((acc, filters) => acc.filter((f1) => filters.some((f2) => f2.id === f1.id)));
const scrollToBottom = () => { const scrollToBottom = () => {
const element = document.getElementById('messages-container'); const element = document.getElementById('messages-container');
element.scrollTo({ element.scrollTo({
@ -355,7 +389,6 @@
</script> </script>
<FilesOverlay show={dragged} /> <FilesOverlay show={dragged} />
<ToolServersModal bind:show={showTools} {selectedToolIds} /> <ToolServersModal bind:show={showTools} {selectedToolIds} />
{#if loaded} {#if loaded}
@ -581,12 +614,16 @@
dismissible={true} dismissible={true}
edit={true} edit={true}
on:dismiss={async () => { on:dismiss={async () => {
try {
if (file.type !== 'collection' && !file?.collection) { if (file.type !== 'collection' && !file?.collection) {
if (file.id) { if (file.id) {
// This will handle both file deletion and Chroma cleanup // This will handle both file deletion and Chroma cleanup
await deleteFileById(localStorage.token, file.id); await deleteFileById(localStorage.token, file.id);
} }
} }
} catch (error) {
console.error('Error deleting file:', error);
}
// Remove from UI state // Remove from UI state
files.splice(fileIdx, 1); files.splice(fileIdx, 1);
@ -771,8 +808,11 @@
console.log('Escape'); console.log('Escape');
atSelectedModel = undefined; atSelectedModel = undefined;
selectedToolIds = []; selectedToolIds = [];
selectedFilterIds = [];
webSearchEnabled = false; webSearchEnabled = false;
imageGenerationEnabled = false; imageGenerationEnabled = false;
codeInterpreterEnabled = false;
} }
}} }}
on:paste={async (e) => { on:paste={async (e) => {
@ -823,7 +863,7 @@
id="chat-input" id="chat-input"
dir="auto" dir="auto"
bind:this={chatInputElement} bind:this={chatInputElement}
class="scrollbar-hidden bg-transparent dark:text-gray-100 outline-hidden w-full pt-3 px-1 resize-none" class="scrollbar-hidden bg-transparent dark:text-gray-200 outline-hidden w-full pt-3 px-1 resize-none"
placeholder={placeholder ? placeholder : $i18n.t('Send a Message')} placeholder={placeholder ? placeholder : $i18n.t('Send a Message')}
bind:value={prompt} bind:value={prompt}
on:compositionstart={() => (isComposing = true)} on:compositionstart={() => (isComposing = true)}
@ -869,7 +909,7 @@
console.log(userMessageElement); console.log(userMessageElement);
userMessageElement.scrollIntoView({ block: 'center' }); userMessageElement?.scrollIntoView({ block: 'center' });
editButton?.click(); editButton?.click();
} }
@ -878,20 +918,38 @@
e.preventDefault(); e.preventDefault();
commandsElement.selectUp(); commandsElement.selectUp();
const container = document.getElementById('command-options-container');
const commandOptionButton = [ const commandOptionButton = [
...document.getElementsByClassName('selected-command-option-button') ...document.getElementsByClassName('selected-command-option-button')
]?.at(-1); ]?.at(-1);
commandOptionButton.scrollIntoView({ block: 'center' });
if (commandOptionButton && container) {
const elTop = commandOptionButton.offsetTop;
const elHeight = commandOptionButton.offsetHeight;
const containerHeight = container.clientHeight;
// Center the selected button in the container
container.scrollTop = elTop - containerHeight / 2 + elHeight / 2;
}
} }
if (commandsContainerElement && e.key === 'ArrowDown') { if (commandsContainerElement && e.key === 'ArrowDown') {
e.preventDefault(); e.preventDefault();
commandsElement.selectDown(); commandsElement.selectDown();
const container = document.getElementById('command-options-container');
const commandOptionButton = [ const commandOptionButton = [
...document.getElementsByClassName('selected-command-option-button') ...document.getElementsByClassName('selected-command-option-button')
]?.at(-1); ]?.at(-1);
commandOptionButton.scrollIntoView({ block: 'center' });
if (commandOptionButton && container) {
const elTop = commandOptionButton.offsetTop;
const elHeight = commandOptionButton.offsetHeight;
const containerHeight = container.clientHeight;
// Center the selected button in the container
container.scrollTop = elTop - containerHeight / 2 + elHeight / 2;
}
} }
if (commandsContainerElement && e.key === 'Enter') { if (commandsContainerElement && e.key === 'Enter') {
@ -939,8 +997,6 @@
? (e.key === 'Enter' || e.keyCode === 13) && isCtrlPressed ? (e.key === 'Enter' || e.keyCode === 13) && isCtrlPressed
: (e.key === 'Enter' || e.keyCode === 13) && !e.shiftKey; : (e.key === 'Enter' || e.keyCode === 13) && !e.shiftKey;
console.log('Enter pressed:', enterPressed);
if (enterPressed) { if (enterPressed) {
e.preventDefault(); e.preventDefault();
} }
@ -978,8 +1034,10 @@
console.log('Escape'); console.log('Escape');
atSelectedModel = undefined; atSelectedModel = undefined;
selectedToolIds = []; selectedToolIds = [];
selectedFilterIds = [];
webSearchEnabled = false; webSearchEnabled = false;
imageGenerationEnabled = false; imageGenerationEnabled = false;
codeInterpreterEnabled = false;
} }
}} }}
rows="1" rows="1"
@ -1037,6 +1095,8 @@
<div class="ml-1 self-end flex items-center flex-1 max-w-[80%] gap-0.5"> <div class="ml-1 self-end flex items-center flex-1 max-w-[80%] gap-0.5">
<InputMenu <InputMenu
bind:selectedToolIds bind:selectedToolIds
selectedModels={atSelectedModel ? [atSelectedModel.id] : selectedModels}
{fileUploadCapableModels}
{screenCaptureHandler} {screenCaptureHandler}
{inputFilesHandler} {inputFilesHandler}
uploadFilesHandler={() => { uploadFilesHandler={() => {
@ -1127,7 +1187,48 @@
{/if} {/if}
{#if $_user} {#if $_user}
{#if $config?.features?.enable_web_search && ($_user.role === 'admin' || $_user?.permissions?.features?.web_search)} {#each toggleFilters as filter, filterIdx (filter.id)}
<Tooltip content={filter?.description} placement="top">
<button
on:click|preventDefault={() => {
if (selectedFilterIds.includes(filter.id)) {
selectedFilterIds = selectedFilterIds.filter(
(id) => id !== filter.id
);
} else {
selectedFilterIds = [...selectedFilterIds, filter.id];
}
}}
type="button"
class="px-1.5 @xl:px-2.5 py-1.5 flex gap-1.5 items-center text-sm rounded-full font-medium transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden border {selectedFilterIds.includes(
filter.id
)
? 'bg-gray-50 dark:bg-gray-400/10 border-gray-100 dark:border-gray-700 text-gray-600 dark:text-gray-400'
: 'bg-transparent border-transparent text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800 '} capitalize"
>
{#if filter?.icon}
<div class="size-5 items-center flex justify-center">
<img
src={filter.icon}
class="size-4.5 {filter.icon.includes('svg')
? 'dark:invert-[80%]'
: ''}"
style="fill: currentColor;"
alt={filter.name}
/>
</div>
{:else}
<Sparkles className="size-5" strokeWidth="1.75" />
{/if}
<span
class="hidden @xl:block whitespace-nowrap overflow-hidden text-ellipsis translate-y-[0.5px]"
>{filter?.name}</span
>
</button>
</Tooltip>
{/each}
{#if (atSelectedModel?.id ? [atSelectedModel.id] : selectedModels).length === webSearchCapableModels.length && $config?.features?.enable_web_search && ($_user.role === 'admin' || $_user?.permissions?.features?.web_search)}
<Tooltip content={$i18n.t('Search the internet')} placement="top"> <Tooltip content={$i18n.t('Search the internet')} placement="top">
<button <button
on:click|preventDefault={() => (webSearchEnabled = !webSearchEnabled)} on:click|preventDefault={() => (webSearchEnabled = !webSearchEnabled)}
@ -1135,7 +1236,7 @@
class="px-1.5 @xl:px-2.5 py-1.5 flex gap-1.5 items-center text-sm rounded-full font-medium transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden border {webSearchEnabled || class="px-1.5 @xl:px-2.5 py-1.5 flex gap-1.5 items-center text-sm rounded-full font-medium transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden border {webSearchEnabled ||
($settings?.webSearch ?? false) === 'always' ($settings?.webSearch ?? false) === 'always'
? 'bg-blue-100 dark:bg-blue-500/20 border-blue-400/20 text-blue-500 dark:text-blue-400' ? 'bg-blue-100 dark:bg-blue-500/20 border-blue-400/20 text-blue-500 dark:text-blue-400'
: 'bg-transparent border-transparent text-gray-600 dark:text-gray-300 border-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800'}" : 'bg-transparent border-transparent text-gray-600 dark:text-gray-300 border-gray-200 hover:bg-gray-50 dark:hover:bg-gray-800'}"
> >
<GlobeAlt className="size-5" strokeWidth="1.75" /> <GlobeAlt className="size-5" strokeWidth="1.75" />
<span <span
@ -1146,7 +1247,7 @@
</Tooltip> </Tooltip>
{/if} {/if}
{#if $config?.features?.enable_image_generation && ($_user.role === 'admin' || $_user?.permissions?.features?.image_generation)} {#if (atSelectedModel?.id ? [atSelectedModel.id] : selectedModels).length === imageGenerationCapableModels.length && $config?.features?.enable_image_generation && ($_user.role === 'admin' || $_user?.permissions?.features?.image_generation)}
<Tooltip content={$i18n.t('Generate an image')} placement="top"> <Tooltip content={$i18n.t('Generate an image')} placement="top">
<button <button
on:click|preventDefault={() => on:click|preventDefault={() =>
@ -1154,7 +1255,7 @@
type="button" type="button"
class="px-1.5 @xl:px-2.5 py-1.5 flex gap-1.5 items-center text-sm rounded-full font-medium transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden border {imageGenerationEnabled class="px-1.5 @xl:px-2.5 py-1.5 flex gap-1.5 items-center text-sm rounded-full font-medium transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden border {imageGenerationEnabled
? 'bg-gray-50 dark:bg-gray-400/10 border-gray-100 dark:border-gray-700 text-gray-600 dark:text-gray-400' ? 'bg-gray-50 dark:bg-gray-400/10 border-gray-100 dark:border-gray-700 text-gray-600 dark:text-gray-400'
: 'bg-transparent border-transparent text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 '}" : 'bg-transparent border-transparent text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800 '}"
> >
<Photo className="size-5" strokeWidth="1.75" /> <Photo className="size-5" strokeWidth="1.75" />
<span <span
@ -1165,7 +1266,7 @@
</Tooltip> </Tooltip>
{/if} {/if}
{#if $config?.features?.enable_code_interpreter && ($_user.role === 'admin' || $_user?.permissions?.features?.code_interpreter)} {#if (atSelectedModel?.id ? [atSelectedModel.id] : selectedModels).length === codeInterpreterCapableModels.length && $config?.features?.enable_code_interpreter && ($_user.role === 'admin' || $_user?.permissions?.features?.code_interpreter)}
<Tooltip content={$i18n.t('Execute code for analysis')} placement="top"> <Tooltip content={$i18n.t('Execute code for analysis')} placement="top">
<button <button
on:click|preventDefault={() => on:click|preventDefault={() =>
@ -1173,7 +1274,7 @@
type="button" type="button"
class="px-1.5 @xl:px-2.5 py-1.5 flex gap-1.5 items-center text-sm rounded-full font-medium transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden border {codeInterpreterEnabled class="px-1.5 @xl:px-2.5 py-1.5 flex gap-1.5 items-center text-sm rounded-full font-medium transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden border {codeInterpreterEnabled
? 'bg-gray-50 dark:bg-gray-400/10 border-gray-100 dark:border-gray-700 text-gray-600 dark:text-gray-400 ' ? 'bg-gray-50 dark:bg-gray-400/10 border-gray-100 dark:border-gray-700 text-gray-600 dark:text-gray-400 '
: 'bg-transparent border-transparent text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 '}" : 'bg-transparent border-transparent text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800 '}"
> >
<CommandLine className="size-5" strokeWidth="1.75" /> <CommandLine className="size-5" strokeWidth="1.75" />
<span <span

View File

@ -170,10 +170,11 @@
class="px-2 mb-2 text-left w-full absolute bottom-0 left-0 right-0 z-10" class="px-2 mb-2 text-left w-full absolute bottom-0 left-0 right-0 z-10"
> >
<div class="flex w-full rounded-xl border border-gray-100 dark:border-gray-850"> <div class="flex w-full rounded-xl border border-gray-100 dark:border-gray-850">
<div class="flex flex-col w-full rounded-xl bg-white dark:bg-gray-900 dark:text-gray-100">
<div <div
class="max-h-60 flex flex-col w-full rounded-xl bg-white dark:bg-gray-900 dark:text-gray-100" class="m-1 overflow-y-auto p-1 rounded-r-xl space-y-0.5 scrollbar-hidden max-h-60"
id="command-options-container"
> >
<div class="m-1 overflow-y-auto p-1 rounded-r-xl space-y-0.5 scrollbar-hidden">
{#each filteredItems as item, idx} {#each filteredItems as item, idx}
<button <button
class=" px-3 py-1.5 rounded-xl w-full text-left flex justify-between items-center {idx === class=" px-3 py-1.5 rounded-xl w-full text-left flex justify-between items-center {idx ===

View File

@ -71,10 +71,11 @@
class="px-2 mb-2 text-left w-full absolute bottom-0 left-0 right-0 z-10" class="px-2 mb-2 text-left w-full absolute bottom-0 left-0 right-0 z-10"
> >
<div class="flex w-full rounded-xl border border-gray-100 dark:border-gray-850"> <div class="flex w-full rounded-xl border border-gray-100 dark:border-gray-850">
<div class="flex flex-col w-full rounded-xl bg-white dark:bg-gray-900 dark:text-gray-100">
<div <div
class="max-h-60 flex flex-col w-full rounded-xl bg-white dark:bg-gray-900 dark:text-gray-100" class="m-1 overflow-y-auto p-1 rounded-r-lg space-y-0.5 scrollbar-hidden max-h-60"
id="command-options-container"
> >
<div class="m-1 overflow-y-auto p-1 rounded-r-lg space-y-0.5 scrollbar-hidden">
{#each filteredItems as model, modelIdx} {#each filteredItems as model, modelIdx}
<button <button
class="px-3 py-1.5 rounded-xl w-full text-left {modelIdx === selectedIdx class="px-3 py-1.5 rounded-xl w-full text-left {modelIdx === selectedIdx

View File

@ -180,10 +180,11 @@
class="px-2 mb-2 text-left w-full absolute bottom-0 left-0 right-0 z-10" class="px-2 mb-2 text-left w-full absolute bottom-0 left-0 right-0 z-10"
> >
<div class="flex w-full rounded-xl border border-gray-100 dark:border-gray-850"> <div class="flex w-full rounded-xl border border-gray-100 dark:border-gray-850">
<div class="flex flex-col w-full rounded-xl bg-white dark:bg-gray-900 dark:text-gray-100">
<div <div
class="max-h-60 flex flex-col w-full rounded-xl bg-white dark:bg-gray-900 dark:text-gray-100" class="m-1 overflow-y-auto p-1 space-y-0.5 scrollbar-hidden max-h-60"
id="command-options-container"
> >
<div class="m-1 overflow-y-auto p-1 space-y-0.5 scrollbar-hidden">
{#each filteredPrompts as prompt, promptIdx} {#each filteredPrompts as prompt, promptIdx}
<button <button
class=" px-3 py-1.5 rounded-xl w-full text-left {promptIdx === selectedPromptIdx class=" px-3 py-1.5 rounded-xl w-full text-left {promptIdx === selectedPromptIdx

View File

@ -20,6 +20,11 @@
const i18n = getContext('i18n'); const i18n = getContext('i18n');
export let selectedToolIds: string[] = [];
export let selectedModels: string[] = [];
export let fileUploadCapableModels: string[] = [];
export let screenCaptureHandler: Function; export let screenCaptureHandler: Function;
export let uploadFilesHandler: Function; export let uploadFilesHandler: Function;
export let inputFilesHandler: Function; export let inputFilesHandler: Function;
@ -27,8 +32,6 @@
export let uploadGoogleDriveHandler: Function; export let uploadGoogleDriveHandler: Function;
export let uploadOneDriveHandler: Function; export let uploadOneDriveHandler: Function;
export let selectedToolIds: string[] = [];
export let onClose: Function; export let onClose: Function;
let tools = {}; let tools = {};
@ -40,7 +43,9 @@
} }
let fileUploadEnabled = true; let fileUploadEnabled = true;
$: fileUploadEnabled = $user?.role === 'admin' || $user?.permissions?.chat?.file_upload; $: fileUploadEnabled =
fileUploadCapableModels.length === selectedModels.length &&
($user?.role === 'admin' || $user?.permissions?.chat?.file_upload);
const init = async () => { const init = async () => {
if ($_tools === null) { if ($_tools === null) {
@ -169,7 +174,11 @@
{/if} {/if}
<Tooltip <Tooltip
content={!fileUploadEnabled ? $i18n.t('You do not have permission to upload files.') : ''} content={fileUploadCapableModels.length !== selectedModels.length
? $i18n.t('Model(s) do not support file upload')
: !fileUploadEnabled
? $i18n.t('You do not have permission to upload files.')
: ''}
className="w-full" className="w-full"
> >
<DropdownMenu.Item <DropdownMenu.Item
@ -196,7 +205,11 @@
</Tooltip> </Tooltip>
<Tooltip <Tooltip
content={!fileUploadEnabled ? $i18n.t('You do not have permission to upload files.') : ''} content={fileUploadCapableModels.length !== selectedModels.length
? $i18n.t('Model(s) do not support file upload')
: !fileUploadEnabled
? $i18n.t('You do not have permission to upload files.')
: ''}
className="w-full" className="w-full"
> >
<DropdownMenu.Item <DropdownMenu.Item
@ -214,6 +227,7 @@
</DropdownMenu.Item> </DropdownMenu.Item>
</Tooltip> </Tooltip>
{#if fileUploadEnabled}
{#if $config?.features?.enable_google_drive_integration} {#if $config?.features?.enable_google_drive_integration}
<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" 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"
@ -256,7 +270,12 @@
<DropdownMenu.SubTrigger <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" 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"> <svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 32 32"
class="w-5 h-5"
fill="none"
>
<mask <mask
id="mask0_87_7796" id="mask0_87_7796"
style="mask-type:alpha" style="mask-type:alpha"
@ -366,6 +385,7 @@
</DropdownMenu.SubContent> </DropdownMenu.SubContent>
</DropdownMenu.Sub> </DropdownMenu.Sub>
{/if} {/if}
{/if}
</DropdownMenu.Content> </DropdownMenu.Content>
</div> </div>
</Dropdown> </Dropdown>

View File

@ -17,16 +17,19 @@
import ChevronUp from '$lib/components/icons/ChevronUp.svelte'; import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
import ChevronUpDown from '$lib/components/icons/ChevronUpDown.svelte'; import ChevronUpDown from '$lib/components/icons/ChevronUpDown.svelte';
import CommandLine from '$lib/components/icons/CommandLine.svelte'; import CommandLine from '$lib/components/icons/CommandLine.svelte';
import Cube from '$lib/components/icons/Cube.svelte';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
export let id = ''; export let id = '';
export let onSave = (e) => {}; export let onSave = (e) => {};
export let onCode = (e) => {}; export let onUpdate = (e) => {};
export let onPreview = (e) => {};
export let save = false; export let save = false;
export let run = true; export let run = true;
export let preview = false;
export let collapsed = false; export let collapsed = false;
export let token; export let token;
@ -88,6 +91,10 @@
}, 1000); }, 1000);
}; };
const previewCode = () => {
onPreview(code);
};
const checkPythonCode = (str) => { const checkPythonCode = (str) => {
// Check if the string contains typical Python syntax characters // Check if the string contains typical Python syntax characters
const pythonSyntax = [ const pythonSyntax = [
@ -333,6 +340,8 @@
await drawMermaidDiagram(); await drawMermaidDiagram();
})(); })();
} }
onUpdate(token);
}; };
$: if (token) { $: if (token) {
@ -345,8 +354,6 @@
render(); render();
} }
$: onCode({ lang, code });
$: if (attributes) { $: if (attributes) {
onAttributesUpdate(); onAttributesUpdate();
} }
@ -379,10 +386,10 @@
onMount(async () => { onMount(async () => {
console.log('codeblock', lang, code); console.log('codeblock', lang, code);
if (token) {
if (lang) { onUpdate(token);
onCode({ lang, code });
} }
if (document.documentElement.classList.contains('dark')) { if (document.documentElement.classList.contains('dark')) {
mermaid.initialize({ mermaid.initialize({
startOnLoad: true, startOnLoad: true,
@ -430,7 +437,7 @@
class="flex gap-1 items-center bg-none border-none bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-md px-1.5 py-0.5" class="flex gap-1 items-center bg-none border-none bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-md px-1.5 py-0.5"
on:click={collapseCodeBlock} on:click={collapseCodeBlock}
> >
<div> <div class=" -translate-y-[0.5px]">
<ChevronUpDown className="size-3" /> <ChevronUpDown className="size-3" />
</div> </div>
@ -439,6 +446,21 @@
</div> </div>
</button> </button>
{#if preview && ['html', 'svg'].includes(lang)}
<button
class="flex gap-1 items-center run-code-button bg-none border-none bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-md px-1.5 py-0.5"
on:click={previewCode}
>
<div class=" -translate-y-[0.5px]">
<Cube className="size-3" />
</div>
<div>
{$i18n.t('Preview')}
</div>
</button>
{/if}
{#if ($config?.features?.enable_code_execution ?? true) && (lang.toLowerCase() === 'python' || lang.toLowerCase() === 'py' || (lang === '' && checkPythonCode(code)))} {#if ($config?.features?.enable_code_execution ?? true) && (lang.toLowerCase() === 'python' || lang.toLowerCase() === 'py' || (lang === '' && checkPythonCode(code)))}
{#if executing} {#if executing}
<div class="run-code-button bg-none border-none p-1 cursor-not-allowed"> <div class="run-code-button bg-none border-none p-1 cursor-not-allowed">
@ -453,7 +475,7 @@
executePython(code); executePython(code);
}} }}
> >
<div> <div class=" -translate-y-[0.5px]">
<CommandLine className="size-3" /> <CommandLine className="size-3" />
</div> </div>

View File

@ -1,10 +1,17 @@
<script> <script>
import { onDestroy, onMount, tick, getContext, createEventDispatcher } from 'svelte'; import { onDestroy, onMount, tick, getContext } from 'svelte';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
const dispatch = createEventDispatcher();
import Markdown from './Markdown.svelte'; import Markdown from './Markdown.svelte';
import { chatId, mobile, settings, showArtifacts, showControls, showOverview } from '$lib/stores'; import {
artifactCode,
chatId,
mobile,
settings,
showArtifacts,
showControls,
showOverview
} from '$lib/stores';
import FloatingButtons from '../ContentRenderer/FloatingButtons.svelte'; import FloatingButtons from '../ContentRenderer/FloatingButtons.svelte';
import { createMessagesList } from '$lib/utils'; import { createMessagesList } from '$lib/utils';
@ -15,8 +22,10 @@
export let sources = null; export let sources = null;
export let save = false; export let save = false;
export let preview = false;
export let floatingButtons = true; export let floatingButtons = true;
export let onSave = () => {};
export let onSourceClick = () => {}; export let onSourceClick = () => {};
export let onTaskClick = () => {}; export let onTaskClick = () => {};
@ -122,6 +131,7 @@
{content} {content}
{model} {model}
{save} {save}
{preview}
sourceIds={(sources ?? []).reduce((acc, s) => { sourceIds={(sources ?? []).reduce((acc, s) => {
let ids = []; let ids = [];
s.document.forEach((document, index) => { s.document.forEach((document, index) => {
@ -154,11 +164,9 @@
}, [])} }, [])}
{onSourceClick} {onSourceClick}
{onTaskClick} {onTaskClick}
onUpdate={(value) => { {onSave}
dispatch('update', value); onUpdate={(token) => {
}} const { lang, text: code } = token;
onCode={(value) => {
const { lang, code } = value;
if ( if (
($settings?.detectArtifacts ?? true) && ($settings?.detectArtifacts ?? true) &&
@ -170,6 +178,13 @@
showControls.set(true); showControls.set(true);
} }
}} }}
onPreview={async (value) => {
console.log('Preview', value);
await artifactCode.set(value);
await showControls.set(true);
await showArtifacts.set(true);
await showOverview.set(false);
}}
/> />
</div> </div>

View File

@ -12,11 +12,14 @@
export let content; export let content;
export let model = null; export let model = null;
export let save = false; export let save = false;
export let preview = false;
export let sourceIds = []; export let sourceIds = [];
export let onSave = () => {};
export let onUpdate = () => {}; export let onUpdate = () => {};
export let onCode = () => {};
export let onPreview = () => {};
export let onSourceClick = () => {}; export let onSourceClick = () => {};
export let onTaskClick = () => {}; export let onTaskClick = () => {};
@ -40,5 +43,15 @@
</script> </script>
{#key id} {#key id}
<MarkdownTokens {tokens} {id} {save} {onTaskClick} {onSourceClick} {onUpdate} {onCode} /> <MarkdownTokens
{tokens}
{id}
{save}
{preview}
{onTaskClick}
{onSourceClick}
{onSave}
{onUpdate}
{onPreview}
/>
{/key} {/key}

View File

@ -29,9 +29,11 @@
export let attributes = {}; export let attributes = {};
export let save = false; export let save = false;
export let preview = false;
export let onSave: Function = () => {};
export let onUpdate: Function = () => {}; export let onUpdate: Function = () => {};
export let onCode: Function = () => {}; export let onPreview: Function = () => {};
export let onTaskClick: Function = () => {}; export let onTaskClick: Function = () => {};
export let onSourceClick: Function = () => {}; export let onSourceClick: Function = () => {};
@ -95,14 +97,16 @@
code={token?.text ?? ''} code={token?.text ?? ''}
{attributes} {attributes}
{save} {save}
{onCode} {preview}
onSave={(value) => { onSave={(value) => {
onUpdate({ onSave({
raw: token.raw, raw: token.raw,
oldContent: token.text, oldContent: token.text,
newContent: value newContent: value
}); });
}} }}
{onUpdate}
{onPreview}
/> />
{:else} {:else}
{token.text} {token.text}

View File

@ -157,6 +157,10 @@
const copyToClipboard = async (text) => { const copyToClipboard = async (text) => {
text = removeAllDetails(text); text = removeAllDetails(text);
if (($config?.ui?.response_watermark ?? '').trim() !== '') {
text = `${text}\n\n${$config?.ui?.response_watermark}`;
}
const res = await _copyToClipboard(text, $settings?.copyFormatted ?? false); const res = await _copyToClipboard(text, $settings?.copyFormatted ?? false);
if (res) { if (res) {
toast.success($i18n.t('Copying to clipboard was successful!')); toast.success($i18n.t('Copying to clipboard was successful!'));
@ -560,17 +564,30 @@
await tick(); await tick();
if (buttonsContainerElement) { if (buttonsContainerElement) {
console.log(buttonsContainerElement); console.log(buttonsContainerElement);
buttonsContainerElement.addEventListener('wheel', function (event) {
// console.log(event.deltaY);
buttonsContainerElement.addEventListener('wheel', function (event) {
if (buttonsContainerElement.scrollWidth <= buttonsContainerElement.clientWidth) {
// If the container is not scrollable, horizontal scroll
return;
} else {
event.preventDefault(); event.preventDefault();
if (event.deltaY !== 0) { if (event.deltaY !== 0) {
// Adjust horizontal scroll position based on vertical scroll // Adjust horizontal scroll position based on vertical scroll
buttonsContainerElement.scrollLeft += event.deltaY; buttonsContainerElement.scrollLeft += event.deltaY;
} }
}
}); });
} }
}); });
let screenReaderDiv: HTMLDivElement;
$: if (message.done) {
if (screenReaderDiv) {
screenReaderDiv.textContent = message.content;
}
}
</script> </script>
<DeleteConfirmDialog <DeleteConfirmDialog
@ -581,6 +598,10 @@
}} }}
/> />
<div bind:this={screenReaderDiv} aria-live="polite" class="sr-only">
{message.done ? message.content : ''}
</div>
{#key message.id} {#key message.id}
<div <div
class=" flex w-full message-{message.id}" class=" flex w-full message-{message.id}"
@ -785,6 +806,7 @@
sources={message.sources} sources={message.sources}
floatingButtons={message?.done && !readOnly} floatingButtons={message?.done && !readOnly}
save={!readOnly} save={!readOnly}
preview={!readOnly}
{model} {model}
onTaskClick={async (e) => { onTaskClick={async (e) => {
console.log(e); console.log(e);
@ -819,28 +841,13 @@
onAddMessages={({ modelId, parentId, messages }) => { onAddMessages={({ modelId, parentId, messages }) => {
addMessages({ modelId, parentId, messages }); addMessages({ modelId, parentId, messages });
}} }}
on:update={(e) => { onSave={({ raw, oldContent, newContent }) => {
const { raw, oldContent, newContent } = e.detail;
history.messages[message.id].content = history.messages[ history.messages[message.id].content = history.messages[
message.id message.id
].content.replace(raw, raw.replace(oldContent, newContent)); ].content.replace(raw, raw.replace(oldContent, newContent));
updateChat(); updateChat();
}} }}
on:select={(e) => {
const { type, content } = e.detail;
if (type === 'explain') {
submitMessage(
message.id,
`Explain this section to me in more detail\n\n\`\`\`\n${content}\n\`\`\``
);
} else if (type === 'ask') {
const input = e.detail?.input ?? '';
submitMessage(message.id, `\`\`\`\n${content}\n\`\`\`\n${input}`);
}
}}
/> />
{/if} {/if}
@ -1394,11 +1401,11 @@
actionMessage(action.id, message); actionMessage(action.id, message);
}} }}
> >
{#if action.icon_url} {#if action?.icon}
<div class="size-4"> <div class="size-4">
<img <img
src={action.icon_url} src={action.icon}
class="w-4 h-4 {action.icon_url.includes('svg') class="w-4 h-4 {action.icon.includes('svg')
? 'dark:invert-[80%]' ? 'dark:invert-[80%]'
: ''}" : ''}"
style="fill: currentColor;" style="fill: currentColor;"

View File

@ -114,7 +114,9 @@
</div> </div>
{#if showSetDefault} {#if showSetDefault}
<div class=" absolute text-left mt-[1px] ml-1 text-[0.7rem] text-gray-500 font-primary"> <div
class="absolute text-left mt-[1px] ml-1 text-[0.7rem] text-gray-600 dark:text-gray-400 font-primary"
>
<button on:click={saveDefaultModel}> {$i18n.t('Set as default')}</button> <button on:click={saveDefaultModel}> {$i18n.t('Set as default')}</button>
</div> </div>
{/if} {/if}

View File

@ -100,10 +100,10 @@
.filter((item) => { .filter((item) => {
if (selectedConnectionType === '') { if (selectedConnectionType === '') {
return true; return true;
} else if (selectedConnectionType === 'ollama') { } else if (selectedConnectionType === 'local') {
return item.model?.owned_by === 'ollama'; return item.model?.connection_type === 'local';
} else if (selectedConnectionType === 'openai') { } else if (selectedConnectionType === 'external') {
return item.model?.owned_by === 'openai'; return item.model?.connection_type === 'external';
} else if (selectedConnectionType === 'direct') { } else if (selectedConnectionType === 'direct') {
return item.model?.direct; return item.model?.direct;
} }
@ -118,10 +118,10 @@
.filter((item) => { .filter((item) => {
if (selectedConnectionType === '') { if (selectedConnectionType === '') {
return true; return true;
} else if (selectedConnectionType === 'ollama') { } else if (selectedConnectionType === 'local') {
return item.model?.owned_by === 'ollama'; return item.model?.connection_type === 'local';
} else if (selectedConnectionType === 'openai') { } else if (selectedConnectionType === 'external') {
return item.model?.owned_by === 'openai'; return item.model?.connection_type === 'external';
} else if (selectedConnectionType === 'direct') { } else if (selectedConnectionType === 'direct') {
return item.model?.direct; return item.model?.direct;
} }
@ -393,7 +393,7 @@
class="flex gap-1 w-fit text-center text-sm font-medium rounded-full bg-transparent px-1.5 pb-0.5" class="flex gap-1 w-fit text-center text-sm font-medium rounded-full bg-transparent px-1.5 pb-0.5"
bind:this={tagsContainerElement} bind:this={tagsContainerElement}
> >
{#if (items.find((item) => item.model?.owned_by === 'ollama') && items.find((item) => item.model?.owned_by === 'openai')) || items.find((item) => item.model?.direct) || tags.length > 0} {#if (items.find((item) => item.model?.connection_type === 'local') && items.find((item) => item.model?.connection_type === 'external')) || items.find((item) => item.model?.direct) || tags.length > 0}
<button <button
class="min-w-fit outline-none p-1.5 {selectedTag === '' && class="min-w-fit outline-none p-1.5 {selectedTag === '' &&
selectedConnectionType === '' selectedConnectionType === ''
@ -408,25 +408,25 @@
</button> </button>
{/if} {/if}
{#if items.find((item) => item.model?.owned_by === 'ollama') && items.find((item) => item.model?.owned_by === 'openai')} {#if items.find((item) => item.model?.connection_type === 'local') && items.find((item) => item.model?.connection_type === 'external')}
<button <button
class="min-w-fit outline-none p-1.5 {selectedConnectionType === 'ollama' class="min-w-fit outline-none p-1.5 {selectedConnectionType === 'local'
? '' ? ''
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition capitalize" : 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition capitalize"
on:click={() => { on:click={() => {
selectedTag = ''; selectedTag = '';
selectedConnectionType = 'ollama'; selectedConnectionType = 'local';
}} }}
> >
{$i18n.t('Local')} {$i18n.t('Local')}
</button> </button>
<button <button
class="min-w-fit outline-none p-1.5 {selectedConnectionType === 'openai' class="min-w-fit outline-none p-1.5 {selectedConnectionType === 'external'
? '' ? ''
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition capitalize" : 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition capitalize"
on:click={() => { on:click={() => {
selectedTag = ''; selectedTag = '';
selectedConnectionType = 'openai'; selectedConnectionType = 'external';
}} }}
> >
{$i18n.t('External')} {$i18n.t('External')}

View File

@ -34,6 +34,8 @@
export let files = []; export let files = [];
export let selectedToolIds = []; export let selectedToolIds = [];
export let selectedFilterIds = [];
export let imageGenerationEnabled = false; export let imageGenerationEnabled = false;
export let codeInterpreterEnabled = false; export let codeInterpreterEnabled = false;
export let webSearchEnabled = false; export let webSearchEnabled = false;
@ -192,6 +194,7 @@
bind:prompt bind:prompt
bind:autoScroll bind:autoScroll
bind:selectedToolIds bind:selectedToolIds
bind:selectedFilterIds
bind:imageGenerationEnabled bind:imageGenerationEnabled
bind:codeInterpreterEnabled bind:codeInterpreterEnabled
bind:webSearchEnabled bind:webSearchEnabled

View File

@ -32,6 +32,8 @@
let notificationSound = true; let notificationSound = true;
let notificationSoundAlways = false; let notificationSoundAlways = false;
let highContrastMode = false;
let detectArtifacts = true; let detectArtifacts = true;
let richTextInput = true; let richTextInput = true;
@ -85,6 +87,11 @@
saveSettings({ splitLargeChunks: splitLargeChunks }); saveSettings({ splitLargeChunks: splitLargeChunks });
}; };
const toggleHighContrastMode = async () => {
highContrastMode = !highContrastMode;
saveSettings({ highContrastMode: highContrastMode });
};
const togglePromptAutocomplete = async () => { const togglePromptAutocomplete = async () => {
promptAutocomplete = !promptAutocomplete; promptAutocomplete = !promptAutocomplete;
saveSettings({ promptAutocomplete: promptAutocomplete }); saveSettings({ promptAutocomplete: promptAutocomplete });
@ -281,6 +288,8 @@
titleAutoGenerate = $settings?.title?.auto ?? true; titleAutoGenerate = $settings?.title?.auto ?? true;
autoTags = $settings.autoTags ?? true; autoTags = $settings.autoTags ?? true;
highContrastMode = $settings.highContrastMode ?? false;
detectArtifacts = $settings.detectArtifacts ?? true; detectArtifacts = $settings.detectArtifacts ?? true;
responseAutoCopy = $settings.responseAutoCopy ?? false; responseAutoCopy = $settings.responseAutoCopy ?? false;
@ -370,6 +379,28 @@
<div> <div>
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('UI')}</div> <div class=" mb-1.5 text-sm font-medium">{$i18n.t('UI')}</div>
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs">
{$i18n.t('High Contrast Mode')} ({$i18n.t('Beta')})
</div>
<button
class="p-1 px-3 text-xs flex rounded-sm transition"
on:click={() => {
toggleHighContrastMode();
}}
type="button"
>
{#if highContrastMode === 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>
<div> <div>
<div class=" py-0.5 flex w-full justify-between"> <div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs">{$i18n.t('Landing Page Mode')}</div> <div class=" self-center text-xs">{$i18n.t('Landing Page Mode')}</div>

View File

@ -64,7 +64,7 @@
} }
</script> </script>
<div class="mb-1 flex gap-1 text-xs font-medium items-center text-gray-400 dark:text-gray-600"> <div class="mb-1 flex gap-1 text-xs font-medium items-center text-gray-600 dark:text-gray-400">
{#if filteredPrompts.length > 0} {#if filteredPrompts.length > 0}
<Bolt /> <Bolt />
{$i18n.t('Suggested')} {$i18n.t('Suggested')}
@ -74,7 +74,7 @@
<div <div
class="flex w-full {$settings?.landingPageMode === 'chat' class="flex w-full {$settings?.landingPageMode === 'chat'
? ' -mt-1' ? ' -mt-1'
: 'text-center items-center justify-center'} self-start text-gray-400 dark:text-gray-600" : 'text-center items-center justify-center'} self-start text-gray-600 dark:text-gray-400"
> >
{$WEBUI_NAME} ‧ v{WEBUI_VERSION} {$WEBUI_NAME} ‧ v{WEBUI_VERSION}
</div> </div>
@ -98,7 +98,7 @@
> >
{prompt.title[0]} {prompt.title[0]}
</div> </div>
<div class="text-xs text-gray-500 font-normal line-clamp-1"> <div class="text-xs text-gray-600 dark:text-gray-400 font-normal line-clamp-1">
{prompt.title[1]} {prompt.title[1]}
</div> </div>
{:else} {:else}
@ -107,7 +107,9 @@
> >
{prompt.content} {prompt.content}
</div> </div>
<div class="text-xs text-gray-500 font-normal line-clamp-1">{$i18n.t('Prompt')}</div> <div class="text-xs text-gray-600 dark:text-gray-400 font-normal line-clamp-1">
{$i18n.t('Prompt')}
</div>
{/if} {/if}
</div> </div>
</button> </button>

View File

@ -7,7 +7,7 @@
export let minSize = null; export let minSize = null;
export let required = false; export let required = false;
export let className = 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'; 'w-full rounded-lg px-3.5 py-2 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden h-full';
let textareaElement; let textareaElement;

View File

@ -0,0 +1,19 @@
<script lang="ts">
export let className = 'w-4 h-4';
export let strokeWidth = '1.5';
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width={strokeWidth}
stroke="currentColor"
class={className}
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 18v-5.25m0 0a6.01 6.01 0 0 0 1.5-.189m-1.5.189a6.01 6.01 0 0 1-1.5-.189m3.75 7.478a12.06 12.06 0 0 1-4.5 0m3.75 2.383a14.406 14.406 0 0 1-3 0M14.25 18v-.192c0-.983.658-1.823 1.508-2.316a7.5 7.5 0 1 0-7.517 0c.85.493 1.509 1.333 1.509 2.316V18"
/>
</svg>

View File

@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { getAdminDetails } from '$lib/apis/auths'; import { getAdminDetails } from '$lib/apis/auths';
import { onMount, tick, getContext } from 'svelte'; import { onMount, tick, getContext } from 'svelte';
import { config } from '$lib/stores';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
@ -20,16 +21,29 @@
> >
<div class="m-auto pb-10 flex flex-col justify-center"> <div class="m-auto pb-10 flex flex-col justify-center">
<div class="max-w-md"> <div class="max-w-md">
<div class="text-center dark:text-white text-2xl font-medium z-50"> <div
class="text-center dark:text-white text-2xl font-medium z-50"
style="white-space: pre-wrap;"
>
{#if ($config?.ui?.pending_user_overlay_title ?? '').trim() !== ''}
{$config.ui.pending_user_overlay_title}
{:else}
{$i18n.t('Account Activation Pending')}<br /> {$i18n.t('Account Activation Pending')}<br />
{$i18n.t('Contact Admin for WebUI Access')} {$i18n.t('Contact Admin for WebUI Access')}
{/if}
</div> </div>
<div class=" mt-4 text-center text-sm dark:text-gray-200 w-full"> <div
{$i18n.t('Your account status is currently pending activation.')}<br /> class=" mt-4 text-center text-sm dark:text-gray-200 w-full"
{$i18n.t( style="white-space: pre-wrap;"
>
{#if ($config?.ui?.pending_user_overlay_content ?? '').trim() !== ''}
{$config.ui.pending_user_overlay_content}
{:else}
{$i18n.t('Your account status is currently pending activation.')}{'\n'}{$i18n.t(
'To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.' 'To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.'
)} )}
{/if}
</div> </div>
{#if adminDetails} {#if adminDetails}

View File

@ -156,12 +156,11 @@
<button <button
class="flex rounded-md py-2 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition" class="flex rounded-md py-2 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition"
on:click={async () => { on:click={async () => {
await userSignOut(); const res = await userSignOut();
user.set(null); user.set(null);
localStorage.removeItem('token'); localStorage.removeItem('token');
location.href = '/auth';
location.href = res?.redirect_url ?? '/auth';
show = false; show = false;
}} }}
> >

View File

@ -14,7 +14,7 @@
</script> </script>
<div <div
class="flex items-start bg-[#F1F8FE] dark:bg-[#020C1D] border border-[3371D5] dark:border-[#03113B] text-[#3371D5] dark:text-[#6795EC] rounded-lg px-3.5 py-3 text-xs max-w-80 pr-2 w-full shadow-lg" class="flex items-start bg-[#F1F8FE] dark:bg-[#020C1D] border border-[3371D5] dark:border-[#03113B] text-[#2B6CD4] dark:text-[#6795EC] rounded-lg px-3.5 py-3 text-xs max-w-80 pr-2 w-full shadow-lg"
> >
<div class="flex-1 font-medium"> <div class="flex-1 font-medium">
{$i18n.t(`A new version (v{{LATEST_VERSION}}) is now available.`, { {$i18n.t(`A new version (v{{LATEST_VERSION}}) is now available.`, {

View File

@ -243,7 +243,7 @@
</div> </div>
<div class=" my-2 mb-5 gap-2 grid lg:grid-cols-2 xl:grid-cols-3" id="model-list"> <div class=" my-2 mb-5 gap-2 grid lg:grid-cols-2 xl:grid-cols-3" id="model-list">
{#each filteredModels as model} {#each filteredModels as model (model.id)}
<div <div
class=" flex flex-col cursor-pointer w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl transition" class=" flex flex-col cursor-pointer w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl transition"
id="model-item-{model.id}" id="model-item-{model.id}"

View File

@ -6,16 +6,45 @@
const i18n = getContext('i18n'); const i18n = getContext('i18n');
const helpText = { const capabilityLabels = {
vision: $i18n.t('Model accepts image inputs'), vision: {
usage: $i18n.t( label: $i18n.t('Vision'),
description: $i18n.t('Model accepts image inputs')
},
file_upload: {
label: $i18n.t('File Upload'),
description: $i18n.t('Model accepts file inputs')
},
web_search: {
label: $i18n.t('Web Search'),
description: $i18n.t('Model can search the web for information')
},
image_generation: {
label: $i18n.t('Image Generation'),
description: $i18n.t('Model can generate images based on text prompts')
},
code_interpreter: {
label: $i18n.t('Code Interpreter'),
description: $i18n.t('Model can execute code and perform calculations')
},
usage: {
label: $i18n.t('Usage'),
description: $i18n.t(
'Sends `stream_options: { include_usage: true }` in the request.\nSupported providers will return token usage information in the response when set.' 'Sends `stream_options: { include_usage: true }` in the request.\nSupported providers will return token usage information in the response when set.'
), )
citations: $i18n.t('Displays citations in the response') },
citations: {
label: $i18n.t('Citations'),
description: $i18n.t('Displays citations in the response')
}
}; };
export let capabilities: { export let capabilities: {
vision?: boolean; vision?: boolean;
file_upload?: boolean;
web_search?: boolean;
image_generation?: boolean;
code_interpreter?: boolean;
usage?: boolean; usage?: boolean;
citations?: boolean; citations?: boolean;
} = {}; } = {};
@ -26,7 +55,7 @@
<div class=" self-center text-sm font-semibold">{$i18n.t('Capabilities')}</div> <div class=" self-center text-sm font-semibold">{$i18n.t('Capabilities')}</div>
</div> </div>
<div class="flex"> <div class="flex">
{#each Object.keys(capabilities) as capability} {#each Object.keys(capabilityLabels) as capability}
<div class=" flex items-center gap-2 mr-3"> <div class=" flex items-center gap-2 mr-3">
<Checkbox <Checkbox
state={capabilities[capability] ? 'checked' : 'unchecked'} state={capabilities[capability] ? 'checked' : 'unchecked'}
@ -36,8 +65,8 @@
/> />
<div class=" py-0.5 text-sm capitalize"> <div class=" py-0.5 text-sm capitalize">
<Tooltip content={marked.parse(helpText[capability])}> <Tooltip content={marked.parse(capabilityLabels[capability].description)}>
{$i18n.t(capability)} {$i18n.t(capabilityLabels[capability].label)}
</Tooltip> </Tooltip>
</div> </div>
</div> </div>

View File

@ -77,8 +77,12 @@
}; };
let capabilities = { let capabilities = {
vision: true, vision: true,
usage: undefined, file_upload: true,
citations: true web_search: true,
image_generation: true,
code_interpreter: true,
citations: true,
usage: undefined
}; };
let knowledge = []; let knowledge = [];

View File

@ -74,6 +74,8 @@
"Allow User Location": "", "Allow User Location": "",
"Allow Voice Interruption in Call": "", "Allow Voice Interruption in Call": "",
"Allowed Endpoints": "", "Allowed Endpoints": "",
"Allowed File Extensions": "",
"Allowed file extensions for upload. Separate multiple extensions with commas. Leave empty for all file types.": "",
"Already have an account?": "هل تملك حساب ؟", "Already have an account?": "هل تملك حساب ؟",
"Alternative to the top_p, and aims to ensure a balance of quality and variety. The parameter p represents the minimum probability for a token to be considered, relative to the probability of the most likely token. For example, with p=0.05 and the most likely token having a probability of 0.9, logits with a value less than 0.045 are filtered out.": "", "Alternative to the top_p, and aims to ensure a balance of quality and variety. The parameter p represents the minimum probability for a token to be considered, relative to the probability of the most likely token. For example, with p=0.05 and the most likely token having a probability of 0.9, logits with a value less than 0.045 are filtered out.": "",
"Always": "", "Always": "",
@ -184,6 +186,7 @@
"Chunk Size": "Chunk حجم", "Chunk Size": "Chunk حجم",
"Ciphers": "", "Ciphers": "",
"Citation": "اقتباس", "Citation": "اقتباس",
"Citations": "",
"Clear memory": "", "Clear memory": "",
"Clear Memory": "", "Clear Memory": "",
"click here": "", "click here": "",
@ -316,6 +319,7 @@
"Deleted {{deleteModelTag}}": "{{deleteModelTag}} حذف", "Deleted {{deleteModelTag}}": "{{deleteModelTag}} حذف",
"Deleted {{name}}": "حذف {{name}}", "Deleted {{name}}": "حذف {{name}}",
"Deleted User": "", "Deleted User": "",
"Describe Pictures in Documents": "",
"Describe your knowledge base and objectives": "", "Describe your knowledge base and objectives": "",
"Description": "وصف", "Description": "وصف",
"Detect Artifacts Automatically": "", "Detect Artifacts Automatically": "",
@ -373,6 +377,7 @@
"e.g. My Tools": "", "e.g. My Tools": "",
"e.g. my_filter": "", "e.g. my_filter": "",
"e.g. my_tools": "", "e.g. my_tools": "",
"e.g. pdf, docx, txt": "",
"e.g. Tools for performing various operations": "", "e.g. Tools for performing various operations": "",
"e.g., 3, 4, 5 (leave blank for default)": "", "e.g., 3, 4, 5 (leave blank for default)": "",
"e.g., en-US,ja-JP (leave blank for auto-detect)": "", "e.g., en-US,ja-JP (leave blank for auto-detect)": "",
@ -410,6 +415,8 @@
"Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "تأكد من أن ملف CSV الخاص بك يتضمن 4 أعمدة بهذا الترتيب: Name, Email, Password, Role.", "Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "تأكد من أن ملف CSV الخاص بك يتضمن 4 أعمدة بهذا الترتيب: Name, Email, Password, Role.",
"Enter {{role}} message here": "أدخل رسالة {{role}} هنا", "Enter {{role}} message here": "أدخل رسالة {{role}} هنا",
"Enter a detail about yourself for your LLMs to recall": "ادخل معلومات عنك تريد أن يتذكرها الموديل", "Enter a detail about yourself for your LLMs to recall": "ادخل معلومات عنك تريد أن يتذكرها الموديل",
"Enter a title for the pending user info overlay. Leave empty for default.": "",
"Enter a watermark for the response. Leave empty for none.": "",
"Enter api auth string (e.g. username:password)": "", "Enter api auth string (e.g. username:password)": "",
"Enter Application DN": "", "Enter Application DN": "",
"Enter Application DN Password": "", "Enter Application DN Password": "",
@ -422,6 +429,7 @@
"Enter Chunk Overlap": "أدخل الChunk Overlap", "Enter Chunk Overlap": "أدخل الChunk Overlap",
"Enter Chunk Size": "أدخل Chunk الحجم", "Enter Chunk Size": "أدخل Chunk الحجم",
"Enter comma-separated \"token:bias_value\" pairs (example: 5432:100, 413:-100)": "", "Enter comma-separated \"token:bias_value\" pairs (example: 5432:100, 413:-100)": "",
"Enter content for the pending user info overlay. Leave empty for default.": "",
"Enter description": "", "Enter description": "",
"Enter Docling OCR Engine": "", "Enter Docling OCR Engine": "",
"Enter Docling OCR Language(s)": "", "Enter Docling OCR Language(s)": "",
@ -430,6 +438,8 @@
"Enter Document Intelligence Key": "", "Enter Document Intelligence Key": "",
"Enter domains separated by commas (e.g., example.com,site.org)": "", "Enter domains separated by commas (e.g., example.com,site.org)": "",
"Enter Exa API Key": "", "Enter Exa API Key": "",
"Enter External Document Loader API Key": "",
"Enter External Document Loader URL": "",
"Enter External Web Loader API Key": "", "Enter External Web Loader API Key": "",
"Enter External Web Loader URL": "", "Enter External Web Loader URL": "",
"Enter External Web Search API Key": "", "Enter External Web Search API Key": "",
@ -537,6 +547,7 @@
"Export to CSV": "", "Export to CSV": "",
"Export Tools": "", "Export Tools": "",
"External": "", "External": "",
"External Document Loader URL required.": "",
"External Models": "", "External Models": "",
"External Web Loader API Key": "", "External Web Loader API Key": "",
"External Web Loader URL": "", "External Web Loader URL": "",
@ -566,6 +577,7 @@
"File not found.": "لم يتم العثور على الملف.", "File not found.": "لم يتم العثور على الملف.",
"File removed successfully.": "", "File removed successfully.": "",
"File size should not exceed {{maxSize}} MB.": "", "File size should not exceed {{maxSize}} MB.": "",
"File Upload": "",
"File uploaded successfully": "", "File uploaded successfully": "",
"Files": "", "Files": "",
"Filter is now globally disabled": "", "Filter is now globally disabled": "",
@ -631,6 +643,7 @@
"Hex Color - Leave empty for default color": "", "Hex Color - Leave empty for default color": "",
"Hide": "أخفاء", "Hide": "أخفاء",
"Hide Model": "", "Hide Model": "",
"High Contrast Mode": "",
"Home": "", "Home": "",
"Host": "", "Host": "",
"How can I help you today?": "كيف استطيع مساعدتك اليوم؟", "How can I help you today?": "كيف استطيع مساعدتك اليوم؟",
@ -774,7 +787,11 @@
"Model {{name}} is now {{status}}": "نموذج {{name}} هو الآن {{status}}", "Model {{name}} is now {{status}}": "نموذج {{name}} هو الآن {{status}}",
"Model {{name}} is now hidden": "", "Model {{name}} is now hidden": "",
"Model {{name}} is now visible": "", "Model {{name}} is now visible": "",
"Model accepts file inputs": "",
"Model accepts image inputs": "", "Model accepts image inputs": "",
"Model can execute code and perform calculations": "",
"Model can generate images based on text prompts": "",
"Model can search the web for information": "",
"Model created successfully!": "", "Model created successfully!": "",
"Model filesystem path detected. Model shortname is required for update, cannot continue.": "تم اكتشاف مسار نظام الملفات النموذجي. الاسم المختصر للنموذج مطلوب للتحديث، ولا يمكن الاستمرار.", "Model filesystem path detected. Model shortname is required for update, cannot continue.": "تم اكتشاف مسار نظام الملفات النموذجي. الاسم المختصر للنموذج مطلوب للتحديث، ولا يمكن الاستمرار.",
"Model Filtering": "", "Model Filtering": "",
@ -785,6 +802,7 @@
"Model Params": "معلمات النموذج", "Model Params": "معلمات النموذج",
"Model Permissions": "", "Model Permissions": "",
"Model updated successfully": "", "Model updated successfully": "",
"Model(s) do not support file upload": "",
"Modelfile Content": "محتوى الملف النموذجي", "Modelfile Content": "محتوى الملف النموذجي",
"Models": "الموديلات", "Models": "الموديلات",
"Models Access": "", "Models Access": "",
@ -881,6 +899,8 @@
"PDF document (.pdf)": "PDF ملف (.pdf)", "PDF document (.pdf)": "PDF ملف (.pdf)",
"PDF Extract Images (OCR)": "PDF أستخرج الصور (OCR)", "PDF Extract Images (OCR)": "PDF أستخرج الصور (OCR)",
"pending": "قيد الانتظار", "pending": "قيد الانتظار",
"Pending User Overlay Content": "",
"Pending User Overlay Title": "",
"Permission denied when accessing media devices": "", "Permission denied when accessing media devices": "",
"Permission denied when accessing microphone": "", "Permission denied when accessing microphone": "",
"Permission denied when accessing microphone: {{error}}": "{{error}} تم رفض الإذن عند الوصول إلى الميكروفون ", "Permission denied when accessing microphone: {{error}}": "{{error}} تم رفض الإذن عند الوصول إلى الميكروفون ",
@ -914,6 +934,7 @@
"Prefix ID": "", "Prefix ID": "",
"Prefix ID is used to avoid conflicts with other connections by adding a prefix to the model IDs - leave empty to disable": "", "Prefix ID is used to avoid conflicts with other connections by adding a prefix to the model IDs - leave empty to disable": "",
"Presence Penalty": "", "Presence Penalty": "",
"Preview": "",
"Previous 30 days": "أخر 30 يوم", "Previous 30 days": "أخر 30 يوم",
"Previous 7 days": "أخر 7 أيام", "Previous 7 days": "أخر 7 أيام",
"Private": "", "Private": "",
@ -968,6 +989,7 @@
"Reset view": "", "Reset view": "",
"Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "", "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "",
"Response splitting": "", "Response splitting": "",
"Response Watermark": "",
"Result": "", "Result": "",
"Retrieval": "", "Retrieval": "",
"Retrieval Query Generation": "", "Retrieval Query Generation": "",
@ -1233,6 +1255,7 @@
"Upload Progress": "جاري التحميل", "Upload Progress": "جاري التحميل",
"URL": "", "URL": "",
"URL Mode": "رابط الموديل", "URL Mode": "رابط الموديل",
"Usage": "",
"Use '#' in the prompt input to load and include your knowledge.": "", "Use '#' in the prompt input to load and include your knowledge.": "",
"Use Gravatar": "Gravatar أستخدم", "Use Gravatar": "Gravatar أستخدم",
"Use groups to group your users and assign permissions.": "", "Use groups to group your users and assign permissions.": "",
@ -1262,6 +1285,7 @@
"View Replies": "", "View Replies": "",
"View Result from **{{NAME}}**": "", "View Result from **{{NAME}}**": "",
"Visibility": "", "Visibility": "",
"Vision": "",
"Voice": "", "Voice": "",
"Voice Input": "", "Voice Input": "",
"Warning": "تحذير", "Warning": "تحذير",

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