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
# to store git revision in build
RUN apk add --no-cache git
COPY package.json package-lock.json ./
RUN npm ci

View File

@ -989,6 +989,26 @@ DEFAULT_USER_ROLE = PersistentConfig(
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 = (
os.environ.get("USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS", "False").lower()
== "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_PREFER_GRPC = os.environ.get("QDRANT_PREFER_GRPC", "False").lower() == "true"
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_URI = os.environ.get("OPENSEARCH_URI", "https://localhost:9200")
@ -1825,6 +1848,18 @@ CONTENT_EXTRACTION_ENGINE = PersistentConfig(
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",
"rag.tika_server_url",
@ -1849,6 +1884,12 @@ DOCLING_OCR_LANG = PersistentConfig(
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",
"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",
"rag.embedding_engine",
@ -2839,6 +2890,12 @@ LDAP_CA_CERT_FILE = PersistentConfig(
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", "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_BATCH_SIZE,
RAG_RELEVANCE_THRESHOLD,
RAG_ALLOWED_FILE_EXTENSIONS,
RAG_FILE_MAX_COUNT,
RAG_FILE_MAX_SIZE,
RAG_OPENAI_API_BASE_URL,
@ -206,10 +207,13 @@ from open_webui.config import (
CHUNK_OVERLAP,
CHUNK_SIZE,
CONTENT_EXTRACTION_ENGINE,
EXTERNAL_DOCUMENT_LOADER_URL,
EXTERNAL_DOCUMENT_LOADER_API_KEY,
TIKA_SERVER_URL,
DOCLING_SERVER_URL,
DOCLING_OCR_ENGINE,
DOCLING_OCR_LANG,
DOCLING_DO_PICTURE_DESCRIPTION,
DOCUMENT_INTELLIGENCE_ENDPOINT,
DOCUMENT_INTELLIGENCE_KEY,
MISTRAL_OCR_API_KEY,
@ -291,6 +295,8 @@ from open_webui.config import (
ENABLE_EVALUATION_ARENA_MODELS,
USER_PERMISSIONS,
DEFAULT_USER_ROLE,
PENDING_USER_OVERLAY_CONTENT,
PENDING_USER_OVERLAY_TITLE,
DEFAULT_PROMPT_SUGGESTIONS,
DEFAULT_MODELS,
DEFAULT_ARENA_MODEL,
@ -317,6 +323,7 @@ from open_webui.config import (
LDAP_APP_PASSWORD,
LDAP_USE_TLS,
LDAP_CA_CERT_FILE,
LDAP_VALIDATE_CERT,
LDAP_CIPHERS,
# Misc
ENV,
@ -327,6 +334,7 @@ from open_webui.config import (
DEFAULT_LOCALE,
OAUTH_PROVIDERS,
WEBUI_URL,
RESPONSE_WATERMARK,
# Admin
ENABLE_ADMIN_CHAT_ACCESS,
ENABLE_ADMIN_EXPORT,
@ -373,6 +381,7 @@ from open_webui.env import (
OFFLINE_MODE,
ENABLE_OTEL,
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_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.WEBHOOK_URL = WEBHOOK_URL
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_USE_TLS = LDAP_USE_TLS
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
@ -631,6 +646,7 @@ app.state.FUNCTIONS = {}
app.state.config.TOP_K = RAG_TOP_K
app.state.config.TOP_K_RERANKER = RAG_TOP_K_RERANKER
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_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.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.DOCLING_SERVER_URL = DOCLING_SERVER_URL
app.state.config.DOCLING_OCR_ENGINE = DOCLING_OCR_ENGINE
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_KEY = DOCUMENT_INTELLIGENCE_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),
"message_id": form_data.pop("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_servers": form_data.pop("tool_servers", None),
"files": form_data.get("files", None),
"features": form_data.get("features", None),
"variables": form_data.get("variables", None),
"features": form_data.get("features", {}),
"variables": form_data.get("variables", {}),
"model": model,
"direct": model_item.get("direct", False),
**(
@ -1395,6 +1415,11 @@ async def get_app_config(request: Request):
"sharepoint_url": ONEDRIVE_SHAREPOINT_URL.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,
**(
{
@ -1446,7 +1471,8 @@ async def get_app_latest_release_version(user=Depends(get_verified_user)):
timeout = aiohttp.ClientTimeout(total=1)
async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
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:
response.raise_for_status()
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"])
class ExternalLoader(BaseLoader):
class ExternalWebLoader(BaseLoader):
def __init__(
self,
web_paths: Union[str, List[str]],
@ -32,7 +32,7 @@ class ExternalLoader(BaseLoader):
response = requests.post(
self.external_url,
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}",
},
json={

View File

@ -21,6 +21,8 @@ from langchain_community.document_loaders import (
)
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.env import SRC_LOG_LEVELS, GLOBAL_LOG_LEVEL
@ -126,14 +128,12 @@ class TikaLoader:
class DoclingLoader:
def __init__(
self, url, file_path=None, mime_type=None, ocr_engine=None, ocr_lang=None
):
def __init__(self, url, file_path=None, mime_type=None, params=None):
self.url = url.rstrip("/")
self.file_path = file_path
self.mime_type = mime_type
self.ocr_engine = ocr_engine
self.ocr_lang = ocr_lang
self.params = params or {}
def load(self) -> list[Document]:
with open(self.file_path, "rb") as f:
@ -150,11 +150,19 @@ class DoclingLoader:
"table_mode": "accurate",
}
if self.ocr_engine and self.ocr_lang:
params["ocr_engine"] = self.ocr_engine
params["ocr_lang"] = [
lang.strip() for lang in self.ocr_lang.split(",") if lang.strip()
]
if self.params:
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"] = [
lang.strip()
for lang in self.params.get("ocr_lang").split(",")
if lang.strip()
]
endpoint = f"{self.url}/v1alpha/convert/file"
r = requests.post(endpoint, files=files, data=params)
@ -207,6 +215,17 @@ class Loader:
def _get_loader(self, filename: str, file_content_type: str, file_path: str):
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._is_text_file(file_ext, file_content_type):
loader = TextLoader(file_path, autodetect_encoding=True)
@ -225,8 +244,13 @@ class Loader:
url=self.kwargs.get("DOCLING_SERVER_URL"),
file_path=file_path,
mime_type=file_content_type,
ocr_engine=self.kwargs.get("DOCLING_OCR_ENGINE"),
ocr_lang=self.kwargs.get("DOCLING_OCR_LANG"),
params={
"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 (
self.engine == "document_intelligence"
@ -258,6 +282,15 @@ class Loader:
loader = MistralLoader(
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:
if file_ext == "pdf":
loader = PyPDFLoader(

View File

@ -12,7 +12,7 @@ from langchain_community.retrievers import BM25Retriever
from langchain_core.documents import Document
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.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_core.documents import Document
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.config import (
ENABLE_RAG_LOCAL_WEB_FETCH,
@ -39,7 +39,7 @@ from open_webui.config import (
EXTERNAL_WEB_LOADER_URL,
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.setLevel(SRC_LOG_LEVELS["RAG"])
@ -515,7 +515,9 @@ class SafeWebBaseLoader(WebBaseLoader):
kwargs["ssl"] = False
async with session.get(
url, **(self.requests_kwargs | kwargs)
url,
**(self.requests_kwargs | kwargs),
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as response:
if self.raise_for_status:
response.raise_for_status()
@ -628,7 +630,7 @@ def get_web_loader(
web_loader_args["extract_depth"] = TAVILY_EXTRACT_DEPTH.value
if WEB_LOADER_ENGINE.value == "external":
WebLoaderClass = ExternalLoader
WebLoaderClass = ExternalWebLoader
web_loader_args["external_url"] = EXTERNAL_WEB_LOADER_URL.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 pydub import AudioSegment
from pydub.silence import split_on_silence
from concurrent.futures import ThreadPoolExecutor
import aiohttp
import aiofiles
@ -38,6 +39,7 @@ from open_webui.config import (
from open_webui.constants import ERROR_MESSAGES
from open_webui.env import (
AIOHTTP_CLIENT_SESSION_SSL,
AIOHTTP_CLIENT_TIMEOUT,
ENV,
SRC_LOG_LEVELS,
@ -49,7 +51,7 @@ from open_webui.env import (
router = APIRouter()
# Constants
MAX_FILE_SIZE_MB = 25
MAX_FILE_SIZE_MB = 20
MAX_FILE_SIZE = MAX_FILE_SIZE_MB * 1024 * 1024 # Convert MB to bytes
AZURE_MAX_FILE_SIZE_MB = 200
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"
):
return "mp4"
elif info.get("format_name") == "ogg":
return "ogg"
except Exception as e:
log.error(f"Error getting audio format: {e}")
return False
@ -326,6 +326,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
else {}
),
},
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as r:
r.raise_for_status()
@ -381,6 +382,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
"Content-Type": "application/json",
"xi-api-key": request.app.state.config.TTS_API_KEY,
},
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as r:
r.raise_for_status()
@ -439,6 +441,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
"X-Microsoft-OutputFormat": output_format,
},
data=data,
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as r:
r.raise_for_status()
@ -507,8 +510,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
return FileResponse(file_path)
def transcribe(request: Request, file_path):
log.info(f"transcribe: {file_path}")
def transcription_handler(request, file_path):
filename = os.path.basename(file_path)
file_dir = os.path.dirname(file_path)
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):
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)
audio = AudioSegment.from_file(file_path)
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 (
os.path.getsize(compressed_path) > MAX_FILE_SIZE
): # Still larger than MAX_FILE_SIZE after compression
raise Exception(ERROR_MESSAGES.FILE_TOO_LARGE(size=f"{MAX_FILE_SIZE_MB}MB"))
compressed_path = os.path.join(file_dir, f"{id}_compressed.mp3")
audio.export(compressed_path, format="mp3", bitrate="32k")
# log.debug(f"Compressed audio to {compressed_path}") # Uncomment if log is defined
return compressed_path
else:
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")
def transcription(
request: Request,
@ -803,6 +900,7 @@ def transcription(
"audio/ogg",
"audio/x-m4a",
"audio/webm",
"video/webm",
)
if not file.content_type.startswith(supported_filetypes):
@ -826,19 +924,13 @@ def transcription(
f.write(contents)
try:
try:
file_path = compress_audio(file_path)
except Exception as e:
log.exception(e)
result = transcribe(request, file_path)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
)
return {
**result,
"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:
log.exception(e)

View File

@ -31,7 +31,7 @@ from open_webui.env import (
SRC_LOG_LEVELS,
)
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 pydantic import BaseModel
@ -51,7 +51,7 @@ from open_webui.utils.access_control import get_permissions
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:
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_USE_TLS = request.app.state.config.LDAP_USE_TLS
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 = (
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:
tls = Tls(
validate=CERT_REQUIRED,
validate=LDAP_VALIDATE_CERT,
version=PROTOCOL_TLS,
ca_certs_file=LDAP_CA_CERT_FILE,
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
)
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.
if len(form_data.password.encode("utf-8")) > 72:
raise HTTPException(
@ -541,6 +540,10 @@ async def signup(request: Request, response: Response, form_data: SignupForm):
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 {
"token": token,
"token_type": "Bearer",
@ -574,9 +577,14 @@ async def signout(request: Request, response: Response):
logout_url = openid_data.get("end_session_endpoint")
if logout_url:
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,
url=f"{logout_url}?id_token_hint={oauth_id_token}",
)
else:
raise HTTPException(
@ -591,12 +599,18 @@ async def signout(request: Request, response: Response):
)
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,
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_NOTES": request.app.state.config.ENABLE_NOTES,
"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_NOTES: 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")
@ -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.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 {
"SHOW_ADMIN_DETAILS": request.app.state.config.SHOW_ADMIN_DETAILS,
"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_NOTES": request.app.state.config.ENABLE_NOTES,
"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 = ""
use_tls: bool = True
certificate_path: Optional[str] = None
validate_cert: bool = True
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,
"use_tls": request.app.state.config.LDAP_USE_TLS,
"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,
}
@ -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_USE_TLS = form_data.use_tls
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
return {
@ -845,6 +880,7 @@ async def update_ldap_server(
"search_filters": request.app.state.config.LDAP_SEARCH_FILTERS,
"use_tls": request.app.state.config.LDAP_USE_TLS,
"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,
}

View File

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

View File

@ -95,6 +95,20 @@ def upload_file(
unsanitized_filename = file.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
id = str(uuid.uuid4())
name = filename
@ -125,33 +139,38 @@ def upload_file(
)
if process:
try:
if file.content_type:
if file.content_type.startswith(
(
"audio/mpeg",
"audio/wav",
"audio/ogg",
"audio/x-m4a",
"audio/webm",
"video/webm",
)
):
file_path = Storage.get_file(file_path)
result = transcribe(request, file_path)
if file.content_type.startswith(
(
"audio/mpeg",
"audio/wav",
"audio/ogg",
"audio/x-m4a",
"audio/webm",
"video/webm",
process_file(
request,
ProcessFileForm(file_id=id, content=result.get("text", "")),
user=user,
)
elif file.content_type not in [
"image/png",
"image/jpeg",
"image/gif",
"video/mp4",
"video/ogg",
"video/quicktime",
]:
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"
)
):
file_path = Storage.get_file(file_path)
result = transcribe(request, file_path)
process_file(
request,
ProcessFileForm(file_id=id, content=result.get("text", "")),
user=user,
)
elif file.content_type not in [
"image/png",
"image/jpeg",
"image/gif",
"video/mp4",
"video/ogg",
"video/quicktime",
]:
process_file(request, ProcessFileForm(file_id=id), user=user)
file_item = Files.get_file_by_id(id=id)

View File

@ -10,7 +10,7 @@ from open_webui.models.knowledge import (
KnowledgeUserResponse,
)
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 (
process_file,
ProcessFileForm,

View File

@ -4,7 +4,7 @@ import logging
from typing import Optional
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.env import SRC_LOG_LEVELS

View File

@ -340,6 +340,8 @@ async def get_all_models(request: Request, user: UserModel = None):
), # Legacy support
)
connection_type = api_config.get("connection_type", "local")
prefix_id = api_config.get("prefix_id", None)
tags = api_config.get("tags", [])
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']}"
if tags:
for model in response.get("models", []):
if tags:
model["tags"] = tags
if connection_type:
model["connection_type"] = connection_type
def merge_models_lists(model_lists):
merged_models = {}
@ -1585,7 +1589,9 @@ async def upload_model(
if url_idx is None:
url_idx = 0
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)
# --- P1: save file locally ---
@ -1630,13 +1636,13 @@ async def upload_model(
os.remove(file_path)
# 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
create_payload = {
"model": model_name,
# 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
@ -1653,7 +1659,7 @@ async def upload_model(
done_msg = {
"done": True,
"blob": f"sha256:{file_hash}",
"name": file.filename,
"name": filename,
"model_created": model_name,
}
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
)
connection_type = api_config.get("connection_type", "external")
prefix_id = api_config.get("prefix_id", None)
tags = api_config.get("tags", [])
if prefix_id:
for model in (
response if isinstance(response, list) else response.get("data", [])
):
for model in (
response if isinstance(response, list) else response.get("data", [])
):
if prefix_id:
model["id"] = f"{prefix_id}.{model['id']}"
if tags:
for model in (
response if isinstance(response, list) else response.get("data", [])
):
if tags:
model["tags"] = tags
if connection_type:
model["connection_type"] = connection_type
log.debug(f"get_all_models:responses() {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"]),
"owned_by": "openai",
"openai": model,
"connection_type": model.get("connection_type", "external"),
"urlIdx": idx,
}
for model in models

View File

@ -18,7 +18,7 @@ from pydantic import BaseModel
from starlette.responses import FileResponse
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.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:
for filter in sorted_filters:
urlIdx = filter.get("urlIdx")
if urlIdx is None:
try:
urlIdx = int(urlIdx)
except:
continue
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",
headers=headers,
json=request_data,
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as response:
payload = await response.json()
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:
for filter in sorted_filters:
urlIdx = filter.get("urlIdx")
if urlIdx is None:
try:
urlIdx = int(urlIdx)
except:
continue
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",
headers=headers,
json=request_data,
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as response:
payload = await response.json()
response.raise_for_status()
@ -197,8 +205,10 @@ async def upload_pipeline(
user=Depends(get_admin_user),
):
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
if not (file.filename and file.filename.endswith(".py")):
if not (filename and filename.endswith(".py")):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Only Python (.py) files are allowed.",
@ -206,7 +216,7 @@ async def upload_pipeline(
upload_folder = f"{CACHE_DIR}/pipelines"
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
try:

View File

@ -36,7 +36,7 @@ from open_webui.models.knowledge import Knowledges
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
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_ENGINE": request.app.state.config.CONTENT_EXTRACTION_ENGINE,
"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,
"DOCLING_SERVER_URL": request.app.state.config.DOCLING_SERVER_URL,
"DOCLING_OCR_ENGINE": request.app.state.config.DOCLING_OCR_ENGINE,
"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_KEY": request.app.state.config.DOCUMENT_INTELLIGENCE_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_MAX_SIZE": request.app.state.config.FILE_MAX_SIZE,
"FILE_MAX_COUNT": request.app.state.config.FILE_MAX_COUNT,
"ALLOWED_FILE_EXTENSIONS": request.app.state.config.ALLOWED_FILE_EXTENSIONS,
# Integration settings
"ENABLE_GOOGLE_DRIVE_INTEGRATION": request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION,
"ENABLE_ONEDRIVE_INTEGRATION": request.app.state.config.ENABLE_ONEDRIVE_INTEGRATION,
@ -492,10 +496,14 @@ class ConfigForm(BaseModel):
# Content extraction settings
CONTENT_EXTRACTION_ENGINE: Optional[str] = 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
DOCLING_SERVER_URL: Optional[str] = None
DOCLING_OCR_ENGINE: Optional[str] = None
DOCLING_OCR_LANG: Optional[str] = None
DOCLING_DO_PICTURE_DESCRIPTION: Optional[bool] = None
DOCUMENT_INTELLIGENCE_ENDPOINT: Optional[str] = None
DOCUMENT_INTELLIGENCE_KEY: Optional[str] = None
MISTRAL_OCR_API_KEY: Optional[str] = None
@ -514,6 +522,7 @@ class ConfigForm(BaseModel):
# File upload settings
FILE_MAX_SIZE: Optional[int] = None
FILE_MAX_COUNT: Optional[int] = None
ALLOWED_FILE_EXTENSIONS: Optional[List[str]] = None
# Integration settings
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
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 = (
form_data.TIKA_SERVER_URL
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
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 = (
form_data.DOCUMENT_INTELLIGENCE_ENDPOINT
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
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
request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION = (
@ -809,10 +840,13 @@ async def update_rag_config(
# Content extraction settings
"CONTENT_EXTRACTION_ENGINE": request.app.state.config.CONTENT_EXTRACTION_ENGINE,
"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,
"DOCLING_SERVER_URL": request.app.state.config.DOCLING_SERVER_URL,
"DOCLING_OCR_ENGINE": request.app.state.config.DOCLING_OCR_ENGINE,
"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_KEY": request.app.state.config.DOCUMENT_INTELLIGENCE_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_MAX_SIZE": request.app.state.config.FILE_MAX_SIZE,
"FILE_MAX_COUNT": request.app.state.config.FILE_MAX_COUNT,
"ALLOWED_FILE_EXTENSIONS": request.app.state.config.ALLOWED_FILE_EXTENSIONS,
# Integration settings
"ENABLE_GOOGLE_DRIVE_INTEGRATION": request.app.state.config.ENABLE_GOOGLE_DRIVE_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)
loader = Loader(
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,
DOCLING_SERVER_URL=request.app.state.config.DOCLING_SERVER_URL,
DOCLING_OCR_ENGINE=request.app.state.config.DOCLING_OCR_ENGINE,
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,
DOCUMENT_INTELLIGENCE_ENDPOINT=request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT,
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.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.config import (

View File

@ -13,6 +13,8 @@ import pytz
from pytz import UTC
from typing import Optional, Union, List, Dict
from opentelemetry import trace
from open_webui.models.users import Users
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
)
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
try:
@ -213,6 +225,14 @@ def get_current_user(
detail=ERROR_MESSAGES.INVALID_TOKEN,
)
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
# to prevent blocking the request
if background_tasks:
@ -234,6 +254,14 @@ def get_current_user_by_api_key(api_key: str):
detail=ERROR_MESSAGES.INVALID_TOKEN,
)
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)
return user

View File

@ -309,6 +309,7 @@ async def chat_completed(request: Request, form_data: dict, user: Any):
metadata = {
"chat_id": data["chat_id"],
"message_id": data["id"],
"filter_ids": data.get("filter_ids", []),
"session_id": data["session_id"],
"user_id": user.id,
}
@ -330,7 +331,9 @@ async def chat_completed(request: Request, form_data: dict, user: Any):
try:
filter_functions = [
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(

View File

@ -9,7 +9,20 @@ log = logging.getLogger(__name__)
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):
function = Functions.get_function_by_id(function_id)
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"]:
filter_ids.extend(model["info"]["meta"].get("filterIds", []))
filter_ids = list(set(filter_ids))
enabled_filter_ids = [
active_filter_ids = [
function.id
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)
return filter_ids
@ -43,12 +65,7 @@ async def process_filter_functions(
if not filter:
continue
if filter_id in request.app.state.FUNCTIONS:
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
function_module = get_function_module(request, filter_id)
# Prepare handler function
handler = getattr(function_module, filter_type, None)
if not handler:

View File

@ -340,6 +340,11 @@ async def chat_web_search_handler(
log.exception(e)
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:
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"])
)
except Exception as e:
print(f"Error parsing logit_bias: {e}")
log.exception(f"Error parsing logit_bias: {e}")
return form_data
@ -749,9 +754,12 @@ async def process_chat_payload(request, form_data, user, metadata, model):
raise e
try:
filter_functions = [
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(
@ -942,21 +950,35 @@ async def process_chat_response(
message = message_map.get(metadata["message_id"]) if message_map else None
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
# the original messages outside of this handler
for message in messages:
message["content"] = re.sub(
r"<details\s+type=\"reasoning\"[^>]*>.*?<\/details>",
"",
message["content"],
flags=re.S,
).strip()
if message.get("files"):
message["files"] = []
messages = []
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>",
"",
content,
flags=re.S | re.I,
).strip()
messages.append(
{
"role": message["role"],
"content": content,
}
)
if tasks and messages:
if TASKS.TITLE_GENERATION in tasks:
@ -1169,7 +1191,9 @@ async def process_chat_response(
}
filter_functions = [
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

View File

@ -49,6 +49,7 @@ async def get_all_base_models(request: Request, user: UserModel = None):
"created": int(time.time()),
"owned_by": "ollama",
"ollama": model,
"connection_type": model.get("connection_type", "local"),
"tags": model.get("tags", []),
}
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)
]
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()
for custom_model in custom_models:
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["info"] = custom_model.model_dump()
# Set action_ids and filter_ids
action_ids = []
filter_ids = []
if "info" in model and "meta" in model["info"]:
action_ids.extend(
model["info"]["meta"].get("actionIds", [])
)
filter_ids.extend(
model["info"]["meta"].get("filterIds", [])
)
model["action_ids"] = action_ids
model["filter_ids"] = filter_ids
else:
models.remove(model)
@ -140,7 +156,9 @@ async def get_all_models(request, user: UserModel = None):
):
owned_by = "openai"
pipe = None
action_ids = []
filter_ids = []
for model in models:
if (
@ -154,9 +172,13 @@ async def get_all_models(request, user: UserModel = None):
if custom_model.meta:
meta = custom_model.meta.model_dump()
if "actionIds" in meta:
action_ids.extend(meta["actionIds"])
if "filterIds" in meta:
filter_ids.extend(meta["filterIds"])
models.append(
{
"id": f"{custom_model.id}",
@ -168,6 +190,7 @@ async def get_all_models(request, user: UserModel = None):
"preset": True,
**({"pipe": pipe} if pipe is not None else {}),
"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']}",
"name": action.get("name", f"{function.name} ({action['id']})"),
"description": function.meta.description,
"icon_url": action.get(
"icon_url", function.meta.manifest.get("icon_url", None)
"icon": action.get(
"icon_url",
function.meta.manifest.get("icon_url", None)
or getattr(module, "icon_url", None)
or getattr(module, "icon", None),
),
}
for action in actions
@ -193,10 +219,25 @@ async def get_all_models(request, user: UserModel = None):
"id": function.id,
"name": function.name,
"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),
}
]
def get_function_module_by_id(function_id):
if function_id in request.app.state.FUNCTIONS:
function_module = request.app.state.FUNCTIONS[function_id]
@ -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))
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"] = []
for action_id in action_ids:
@ -222,6 +268,20 @@ async def get_all_models(request, user: UserModel = None):
model["actions"].extend(
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")
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.env import (
AIOHTTP_CLIENT_SESSION_SSL,
WEBUI_NAME,
WEBUI_AUTH_COOKIE_SAME_SITE,
WEBUI_AUTH_COOKIE_SECURE,
@ -305,8 +306,10 @@ class OAuthManager:
get_kwargs["headers"] = {
"Authorization": f"Bearer {access_token}",
}
async with aiohttp.ClientSession() as session:
async with session.get(picture_url, **get_kwargs) as resp:
async with aiohttp.ClientSession(trust_env=True) as session:
async with session.get(
picture_url, **get_kwargs, ssl=AIOHTTP_CLIENT_SESSION_SSL
) as resp:
if resp.ok:
picture = await resp.read()
base64_encoded_picture = base64.b64encode(picture).decode(
@ -371,7 +374,9 @@ class OAuthManager:
headers = {"Authorization": f"Bearer {access_token}"}
async with aiohttp.ClientSession(trust_env=True) as session:
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:
if resp.ok:
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.utils.plugin import load_tool_module_by_id
from open_webui.env import (
SRC_LOG_LEVELS,
AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA,
AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL,
)
@ -44,6 +45,7 @@ from open_webui.env import (
import copy
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
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),
}
print("Fetched data:", data)
log.info("Fetched data:", data)
return data
@ -510,7 +512,7 @@ async def get_tool_servers_data(
results = []
for (idx, server, url, _), response in zip(server_entries, responses):
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
results.append(
@ -620,5 +622,5 @@ async def execute_tool_server(
except Exception as err:
error = str(err)
print("API Request Error:", error)
log.exception("API Request Error:", error)
return {"error": error}

View File

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

4
package-lock.json generated
View File

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

View File

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

View File

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

View File

@ -314,12 +314,20 @@ input[type='number'] {
.ProseMirror p.is-editor-empty:first-child::before {
content: attr(data-placeholder);
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;
@apply line-clamp-1 absolute;
}
@media (prefers-color-scheme: dark) {
.ProseMirror p.is-editor-empty:first-child::before {
color: #757575;
}
}
.ai-autocompletion::after {
color: #a0a0a0;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -30,6 +30,9 @@
let url = '';
let key = '';
let connectionType = 'external';
let azure = false;
let prefixId = '';
let enable = true;
let tags = [];
@ -95,7 +98,9 @@
enable: enable,
tags: tags,
prefix_id: prefixId,
model_ids: modelIds
model_ids: modelIds,
connection_type: connectionType,
...(!ollama && azure ? { azure: true } : {})
}
};
@ -120,6 +125,13 @@
tags = connection.config?.tags ?? [];
prefixId = connection.config?.prefix_id ?? '';
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>
<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">
{#if edit}
{$i18n.t('Edit Connection')}
@ -172,6 +184,28 @@
>
<div class="px-1">
<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=" mb-0.5 text-xs text-gray-500">{$i18n.t('URL')}</div>

View File

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

View File

@ -91,7 +91,7 @@
return;
}
console.log('Update embedding model attempt:', embeddingModel);
console.debug('Update embedding model attempt:', embeddingModel);
updateEmbeddingModelLoading = true;
const res = await updateEmbeddingConfig(localStorage.token, {
@ -114,7 +114,7 @@
updateEmbeddingModelLoading = false;
if (res) {
console.log('embeddingModelUpdateHandler:', res);
console.debug('embeddingModelUpdateHandler:', res);
if (res.status === true) {
toast.success($i18n.t('Embedding model set to "{{embedding_model}}"', res), {
duration: 1000 * 10
@ -124,6 +124,13 @@
};
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 === '') {
toast.error($i18n.t('Tika Server URL required.'));
return;
@ -163,6 +170,10 @@
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);
dispatch('save');
};
@ -185,7 +196,10 @@
onMount(async () => {
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>
@ -246,7 +260,7 @@
<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="flex w-full justify-between">
<div class="flex w-full justify-between mb-1">
<div class="self-center text-xs font-medium">
{$i18n.t('Content Extraction Engine')}
</div>
@ -256,6 +270,7 @@
bind:value={RAGConfig.CONTENT_EXTRACTION_ENGINE}
>
<option value="">{$i18n.t('Default')}</option>
<option value="external">{$i18n.t('External')}</option>
<option value="tika">{$i18n.t('Tika')}</option>
<option value="docling">{$i18n.t('Docling')}</option>
<option value="document_intelligence">{$i18n.t('Document Intelligence')}</option>
@ -275,11 +290,24 @@
</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'}
<div class="flex w-full mt-1">
<div class="flex-1 mr-2">
<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')}
bind:value={RAGConfig.TIKA_SERVER_URL}
/>
@ -288,27 +316,38 @@
{:else if RAGConfig.CONTENT_EXTRACTION_ENGINE === 'docling'}
<div class="flex w-full mt-1">
<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')}
bind:value={RAGConfig.DOCLING_SERVER_URL}
/>
</div>
<div class="flex w-full mt-2">
<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')}
bind:value={RAGConfig.DOCLING_OCR_ENGINE}
/>
<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)')}
bind:value={RAGConfig.DOCLING_OCR_LANG}
/>
</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'}
<div class="my-0.5 flex gap-2 pr-2">
<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')}
bind:value={RAGConfig.DOCUMENT_INTELLIGENCE_ENDPOINT}
/>
@ -437,7 +476,7 @@
{#if embeddingEngine === 'openai'}
<div class="my-0.5 flex gap-2 pr-2">
<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')}
bind:value={OpenAIUrl}
required
@ -448,7 +487,7 @@
{:else if embeddingEngine === 'ollama'}
<div class="my-0.5 flex gap-2 pr-2">
<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')}
bind:value={OllamaUrl}
required
@ -471,7 +510,7 @@
<div class="flex w-full">
<div class="flex-1 mr-2">
<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}
placeholder={$i18n.t('Set embedding model')}
required
@ -482,7 +521,7 @@
<div class="flex w-full">
<div class="flex-1 mr-2">
<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}})', {
model: embeddingModel.slice(-40)
})}
@ -639,7 +678,7 @@
{#if RAGConfig.RAG_RERANKING_ENGINE === 'external'}
<div class="my-0.5 flex gap-2 pr-2">
<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')}
bind:value={RAGConfig.RAG_EXTERNAL_RERANKER_URL}
required
@ -661,7 +700,7 @@
<div class="flex w-full">
<div class="flex-1 mr-2">
<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}})', {
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="flex items-center relative">
<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"
placeholder={$i18n.t('Enter 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="flex items-center relative">
<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"
placeholder={$i18n.t('Enter Top K Reranker')}
bind:value={RAGConfig.TOP_K_RERANKER}
@ -711,7 +750,7 @@
</div>
<div class="flex items-center relative">
<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"
step="0.01"
placeholder={$i18n.t('Enter Score')}
@ -760,6 +799,26 @@
<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=" self-center text-xs font-medium">{$i18n.t('Max Upload Size')}</div>
<div class="flex items-center relative">
@ -770,7 +829,7 @@
placement="top-start"
>
<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"
placeholder={$i18n.t('Leave empty for unlimited')}
bind:value={RAGConfig.FILE_MAX_SIZE}
@ -791,7 +850,7 @@
placement="top-start"
>
<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"
placeholder={$i18n.t('Leave empty for unlimited')}
bind:value={RAGConfig.FILE_MAX_COUNT}

View File

@ -18,6 +18,7 @@
import { compareVersion } from '$lib/utils';
import { onMount, getContext } from 'svelte';
import { toast } from 'svelte-sonner';
import Textarea from '$lib/components/common/Textarea.svelte';
const i18n = getContext('i18n');
@ -58,10 +59,10 @@
};
});
console.log(version);
console.info(version);
updateAvailable = compareVersion(version.latest, version.current);
console.log(updateAvailable);
console.info(updateAvailable);
};
const updateLdapServerHandler = async () => {
@ -305,6 +306,31 @@
<Switch bind:state={adminConfig.SHOW_ADMIN_DETAILS} />
</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=" self-center text-xs font-medium">{$i18n.t('Enable API Key')}</div>
@ -559,6 +585,13 @@
/>
</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="w-full">
<div class=" self-center text-xs font-medium min-w-fit mb-1">
@ -625,6 +658,16 @@
<Switch bind:state={adminConfig.ENABLE_USER_WEBHOOKS} />
</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="flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('WebUI URL')}</div>

View File

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

View File

@ -90,7 +90,7 @@
}
});
console.log('models', models);
console.debug('models', models);
};
</script>
@ -108,8 +108,8 @@
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
<div class=" mb-1 font-medium flex items-center">
<div class=" text-xs mr-1">{$i18n.t('Set Task Model')}</div>
<div class=" mb-2 font-medium flex items-center">
<div class=" text-xs mr-1">{$i18n.t('Task Model')}</div>
<Tooltip
content={$i18n.t(
'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="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
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}
@ -159,7 +159,7 @@
}}
>
<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">
{model.name}
</option>
@ -168,7 +168,7 @@
</div>
<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
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}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,13 +4,15 @@
const i18n = getContext('i18n');
const dispatch = createEventDispatcher();
import { chatId, settings, showArtifacts, showControls } from '$lib/stores';
import XMark from '../icons/XMark.svelte';
import { artifactCode, chatId, settings, showArtifacts, showControls } from '$lib/stores';
import { copyToClipboard, createMessagesList } from '$lib/utils';
import XMark from '../icons/XMark.svelte';
import ArrowsPointingOut from '../icons/ArrowsPointingOut.svelte';
import Tooltip from '../common/Tooltip.svelte';
import SvgPanZoom from '../common/SVGPanZoom.svelte';
import ArrowLeft from '../icons/ArrowLeft.svelte';
import ArrowDownTray from '../icons/ArrowDownTray.svelte';
export let overlay = false;
export let history;
@ -154,7 +156,7 @@
url.pathname + url.search + url.hash
);
} 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>
<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" />
</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 gap-0.5 self-center min-w-fit" dir="ltr">
<button
@ -252,7 +273,7 @@
</div>
</div>
<div class="flex items-center gap-1">
<div class="flex items-center gap-1.5">
<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"
on:click={() => {
@ -265,6 +286,15 @@
}}>{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'}
<Tooltip content={$i18n.t('Open in full screen')}>
<button

View File

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

View File

@ -38,6 +38,7 @@
import VoiceRecording from './MessageInput/VoiceRecording.svelte';
import FilesOverlay from './MessageInput/FilesOverlay.svelte';
import Commands from './MessageInput/Commands.svelte';
import ToolServersModal from './ToolServersModal.svelte';
import RichTextInput from '../common/RichTextInput.svelte';
import Tooltip from '../common/Tooltip.svelte';
@ -47,12 +48,12 @@
import XMark from '../icons/XMark.svelte';
import Headphone from '../icons/Headphone.svelte';
import GlobeAlt from '../icons/GlobeAlt.svelte';
import PhotoSolid from '../icons/PhotoSolid.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 CommandLine from '../icons/CommandLine.svelte';
import Sparkles from '../icons/Sparkles.svelte';
import { KokoroWorker } from '$lib/workers/KokoroWorker';
const i18n = getContext('i18n');
@ -79,6 +80,7 @@
export let toolServers = [];
export let selectedToolIds = [];
export let selectedFilterIds = [];
export let imageGenerationEnabled = false;
export let webSearchEnabled = false;
@ -88,6 +90,7 @@
prompt,
files: files.filter((file) => file.type !== 'image'),
selectedToolIds,
selectedFilterIds,
imageGenerationEnabled,
webSearchEnabled,
codeInterpreterEnabled
@ -113,10 +116,41 @@
export let placeholder = '';
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
);
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 element = document.getElementById('messages-container');
element.scrollTo({
@ -355,7 +389,6 @@
</script>
<FilesOverlay show={dragged} />
<ToolServersModal bind:show={showTools} {selectedToolIds} />
{#if loaded}
@ -581,11 +614,15 @@
dismissible={true}
edit={true}
on:dismiss={async () => {
if (file.type !== 'collection' && !file?.collection) {
if (file.id) {
// This will handle both file deletion and Chroma cleanup
await deleteFileById(localStorage.token, file.id);
try {
if (file.type !== 'collection' && !file?.collection) {
if (file.id) {
// This will handle both file deletion and Chroma cleanup
await deleteFileById(localStorage.token, file.id);
}
}
} catch (error) {
console.error('Error deleting file:', error);
}
// Remove from UI state
@ -771,8 +808,11 @@
console.log('Escape');
atSelectedModel = undefined;
selectedToolIds = [];
selectedFilterIds = [];
webSearchEnabled = false;
imageGenerationEnabled = false;
codeInterpreterEnabled = false;
}
}}
on:paste={async (e) => {
@ -823,7 +863,7 @@
id="chat-input"
dir="auto"
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')}
bind:value={prompt}
on:compositionstart={() => (isComposing = true)}
@ -869,7 +909,7 @@
console.log(userMessageElement);
userMessageElement.scrollIntoView({ block: 'center' });
userMessageElement?.scrollIntoView({ block: 'center' });
editButton?.click();
}
@ -878,20 +918,38 @@
e.preventDefault();
commandsElement.selectUp();
const container = document.getElementById('command-options-container');
const commandOptionButton = [
...document.getElementsByClassName('selected-command-option-button')
]?.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') {
e.preventDefault();
commandsElement.selectDown();
const container = document.getElementById('command-options-container');
const commandOptionButton = [
...document.getElementsByClassName('selected-command-option-button')
]?.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') {
@ -939,8 +997,6 @@
? (e.key === 'Enter' || e.keyCode === 13) && isCtrlPressed
: (e.key === 'Enter' || e.keyCode === 13) && !e.shiftKey;
console.log('Enter pressed:', enterPressed);
if (enterPressed) {
e.preventDefault();
}
@ -978,8 +1034,10 @@
console.log('Escape');
atSelectedModel = undefined;
selectedToolIds = [];
selectedFilterIds = [];
webSearchEnabled = false;
imageGenerationEnabled = false;
codeInterpreterEnabled = false;
}
}}
rows="1"
@ -1037,6 +1095,8 @@
<div class="ml-1 self-end flex items-center flex-1 max-w-[80%] gap-0.5">
<InputMenu
bind:selectedToolIds
selectedModels={atSelectedModel ? [atSelectedModel.id] : selectedModels}
{fileUploadCapableModels}
{screenCaptureHandler}
{inputFilesHandler}
uploadFilesHandler={() => {
@ -1127,7 +1187,48 @@
{/if}
{#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">
<button
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 ||
($settings?.webSearch ?? false) === 'always'
? '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" />
<span
@ -1146,7 +1247,7 @@
</Tooltip>
{/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">
<button
on:click|preventDefault={() =>
@ -1154,7 +1255,7 @@
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
? '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" />
<span
@ -1165,7 +1266,7 @@
</Tooltip>
{/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">
<button
on:click|preventDefault={() =>
@ -1173,7 +1274,7 @@
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
? '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" />
<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"
>
<div class="flex w-full rounded-xl border border-gray-100 dark:border-gray-850">
<div
class="max-h-60 flex flex-col w-full rounded-xl bg-white dark:bg-gray-900 dark:text-gray-100"
>
<div class="m-1 overflow-y-auto p-1 rounded-r-xl space-y-0.5 scrollbar-hidden">
<div class="flex flex-col w-full rounded-xl bg-white dark:bg-gray-900 dark:text-gray-100">
<div
class="m-1 overflow-y-auto p-1 rounded-r-xl space-y-0.5 scrollbar-hidden max-h-60"
id="command-options-container"
>
{#each filteredItems as item, idx}
<button
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"
>
<div class="flex w-full rounded-xl border border-gray-100 dark:border-gray-850">
<div
class="max-h-60 flex flex-col w-full rounded-xl bg-white dark:bg-gray-900 dark:text-gray-100"
>
<div class="m-1 overflow-y-auto p-1 rounded-r-lg space-y-0.5 scrollbar-hidden">
<div class="flex flex-col w-full rounded-xl bg-white dark:bg-gray-900 dark:text-gray-100">
<div
class="m-1 overflow-y-auto p-1 rounded-r-lg space-y-0.5 scrollbar-hidden max-h-60"
id="command-options-container"
>
{#each filteredItems as model, modelIdx}
<button
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"
>
<div class="flex w-full rounded-xl border border-gray-100 dark:border-gray-850">
<div
class="max-h-60 flex flex-col w-full rounded-xl bg-white dark:bg-gray-900 dark:text-gray-100"
>
<div class="m-1 overflow-y-auto p-1 space-y-0.5 scrollbar-hidden">
<div class="flex flex-col w-full rounded-xl bg-white dark:bg-gray-900 dark:text-gray-100">
<div
class="m-1 overflow-y-auto p-1 space-y-0.5 scrollbar-hidden max-h-60"
id="command-options-container"
>
{#each filteredPrompts as prompt, promptIdx}
<button
class=" px-3 py-1.5 rounded-xl w-full text-left {promptIdx === selectedPromptIdx

View File

@ -20,6 +20,11 @@
const i18n = getContext('i18n');
export let selectedToolIds: string[] = [];
export let selectedModels: string[] = [];
export let fileUploadCapableModels: string[] = [];
export let screenCaptureHandler: Function;
export let uploadFilesHandler: Function;
export let inputFilesHandler: Function;
@ -27,8 +32,6 @@
export let uploadGoogleDriveHandler: Function;
export let uploadOneDriveHandler: Function;
export let selectedToolIds: string[] = [];
export let onClose: Function;
let tools = {};
@ -40,7 +43,9 @@
}
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 () => {
if ($_tools === null) {
@ -169,7 +174,11 @@
{/if}
<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"
>
<DropdownMenu.Item
@ -196,7 +205,11 @@
</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"
>
<DropdownMenu.Item
@ -214,157 +227,164 @@
</DropdownMenu.Item>
</Tooltip>
{#if $config?.features?.enable_google_drive_integration}
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
on:click={() => {
uploadGoogleDriveHandler();
}}
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 87.3 78" class="w-5 h-5">
<path
d="m6.6 66.85 3.85 6.65c.8 1.4 1.95 2.5 3.3 3.3l13.75-23.8h-27.5c0 1.55.4 3.1 1.2 4.5z"
fill="#0066da"
/>
<path
d="m43.65 25-13.75-23.8c-1.35.8-2.5 1.9-3.3 3.3l-25.4 44a9.06 9.06 0 0 0 -1.2 4.5h27.5z"
fill="#00ac47"
/>
<path
d="m73.55 76.8c1.35-.8 2.5-1.9 3.3-3.3l1.6-2.75 7.65-13.25c.8-1.4 1.2-2.95 1.2-4.5h-27.502l5.852 11.5z"
fill="#ea4335"
/>
<path
d="m43.65 25 13.75-23.8c-1.35-.8-2.9-1.2-4.5-1.2h-18.5c-1.6 0-3.15.45-4.5 1.2z"
fill="#00832d"
/>
<path
d="m59.8 53h-32.3l-13.75 23.8c1.35.8 2.9 1.2 4.5 1.2h50.8c1.6 0 3.15-.45 4.5-1.2z"
fill="#2684fc"
/>
<path
d="m73.4 26.5-12.7-22c-.8-1.4-1.95-2.5-3.3-3.3l-13.75 23.8 16.15 28h27.45c0-1.55-.4-3.1-1.2-4.5z"
fill="#ffba00"
/>
</svg>
<div class="line-clamp-1">{$i18n.t('Google Drive')}</div>
</DropdownMenu.Item>
{/if}
{#if $config?.features?.enable_onedrive_integration}
<DropdownMenu.Sub>
<DropdownMenu.SubTrigger
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl w-full"
{#if fileUploadEnabled}
{#if $config?.features?.enable_google_drive_integration}
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
on:click={() => {
uploadGoogleDriveHandler();
}}
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" class="w-5 h-5" fill="none">
<mask
id="mask0_87_7796"
style="mask-type:alpha"
maskUnits="userSpaceOnUse"
x="0"
y="6"
width="32"
height="20"
>
<path
d="M7.82979 26C3.50549 26 0 22.5675 0 18.3333C0 14.1921 3.35322 10.8179 7.54613 10.6716C9.27535 7.87166 12.4144 6 16 6C20.6308 6 24.5169 9.12183 25.5829 13.3335C29.1316 13.3603 32 16.1855 32 19.6667C32 23.0527 29 26 25.8723 25.9914L7.82979 26Z"
fill="#C4C4C4"
/>
</mask>
<g mask="url(#mask0_87_7796)">
<path
d="M7.83017 26.0001C5.37824 26.0001 3.18957 24.8966 1.75391 23.1691L18.0429 16.3335L30.7089 23.4647C29.5926 24.9211 27.9066 26.0001 26.0004 25.9915C23.1254 26.0001 12.0629 26.0001 7.83017 26.0001Z"
fill="url(#paint0_linear_87_7796)"
/>
<path
d="M25.5785 13.3149L18.043 16.3334L30.709 23.4647C31.5199 22.4065 32.0004 21.0916 32.0004 19.6669C32.0004 16.1857 29.1321 13.3605 25.5833 13.3337C25.5817 13.3274 25.5801 13.3212 25.5785 13.3149Z"
fill="url(#paint1_linear_87_7796)"
/>
<path
d="M7.06445 10.7028L18.0423 16.3333L25.5779 13.3148C24.5051 9.11261 20.6237 6 15.9997 6C12.4141 6 9.27508 7.87166 7.54586 10.6716C7.3841 10.6773 7.22358 10.6877 7.06445 10.7028Z"
fill="url(#paint2_linear_87_7796)"
/>
<path
d="M1.7535 23.1687L18.0425 16.3331L7.06471 10.7026C3.09947 11.0792 0 14.3517 0 18.3331C0 20.1665 0.657197 21.8495 1.7535 23.1687Z"
fill="url(#paint3_linear_87_7796)"
/>
</g>
<defs>
<linearGradient
id="paint0_linear_87_7796"
x1="4.42591"
y1="24.6668"
x2="27.2309"
y2="23.2764"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#2086B8" />
<stop offset="1" stop-color="#46D3F6" />
</linearGradient>
<linearGradient
id="paint1_linear_87_7796"
x1="23.8302"
y1="19.6668"
x2="30.2108"
y2="15.2082"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#1694DB" />
<stop offset="1" stop-color="#62C3FE" />
</linearGradient>
<linearGradient
id="paint2_linear_87_7796"
x1="8.51037"
y1="7.33333"
x2="23.3335"
y2="15.9348"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#0D3D78" />
<stop offset="1" stop-color="#063B83" />
</linearGradient>
<linearGradient
id="paint3_linear_87_7796"
x1="-0.340429"
y1="19.9998"
x2="14.5634"
y2="14.4649"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#16589B" />
<stop offset="1" stop-color="#1464B7" />
</linearGradient>
</defs>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 87.3 78" class="w-5 h-5">
<path
d="m6.6 66.85 3.85 6.65c.8 1.4 1.95 2.5 3.3 3.3l13.75-23.8h-27.5c0 1.55.4 3.1 1.2 4.5z"
fill="#0066da"
/>
<path
d="m43.65 25-13.75-23.8c-1.35.8-2.5 1.9-3.3 3.3l-25.4 44a9.06 9.06 0 0 0 -1.2 4.5h27.5z"
fill="#00ac47"
/>
<path
d="m73.55 76.8c1.35-.8 2.5-1.9 3.3-3.3l1.6-2.75 7.65-13.25c.8-1.4 1.2-2.95 1.2-4.5h-27.502l5.852 11.5z"
fill="#ea4335"
/>
<path
d="m43.65 25 13.75-23.8c-1.35-.8-2.9-1.2-4.5-1.2h-18.5c-1.6 0-3.15.45-4.5 1.2z"
fill="#00832d"
/>
<path
d="m59.8 53h-32.3l-13.75 23.8c1.35.8 2.9 1.2 4.5 1.2h50.8c1.6 0 3.15-.45 4.5-1.2z"
fill="#2684fc"
/>
<path
d="m73.4 26.5-12.7-22c-.8-1.4-1.95-2.5-3.3-3.3l-13.75 23.8 16.15 28h27.45c0-1.55-.4-3.1-1.2-4.5z"
fill="#ffba00"
/>
</svg>
<div class="line-clamp-1">{$i18n.t('Microsoft OneDrive')}</div>
</DropdownMenu.SubTrigger>
<DropdownMenu.SubContent
class="w-[calc(100vw-2rem)] max-w-[280px] rounded-xl px-1 py-1 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-sm"
side={$mobile ? 'bottom' : 'right'}
sideOffset={$mobile ? 5 : 0}
alignOffset={$mobile ? 0 : -8}
>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
on:click={() => {
uploadOneDriveHandler('personal');
}}
<div class="line-clamp-1">{$i18n.t('Google Drive')}</div>
</DropdownMenu.Item>
{/if}
{#if $config?.features?.enable_onedrive_integration}
<DropdownMenu.Sub>
<DropdownMenu.SubTrigger
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl w-full"
>
<div class="line-clamp-1">{$i18n.t('Microsoft OneDrive (personal)')}</div>
</DropdownMenu.Item>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
on:click={() => {
uploadOneDriveHandler('organizations');
}}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 32 32"
class="w-5 h-5"
fill="none"
>
<mask
id="mask0_87_7796"
style="mask-type:alpha"
maskUnits="userSpaceOnUse"
x="0"
y="6"
width="32"
height="20"
>
<path
d="M7.82979 26C3.50549 26 0 22.5675 0 18.3333C0 14.1921 3.35322 10.8179 7.54613 10.6716C9.27535 7.87166 12.4144 6 16 6C20.6308 6 24.5169 9.12183 25.5829 13.3335C29.1316 13.3603 32 16.1855 32 19.6667C32 23.0527 29 26 25.8723 25.9914L7.82979 26Z"
fill="#C4C4C4"
/>
</mask>
<g mask="url(#mask0_87_7796)">
<path
d="M7.83017 26.0001C5.37824 26.0001 3.18957 24.8966 1.75391 23.1691L18.0429 16.3335L30.7089 23.4647C29.5926 24.9211 27.9066 26.0001 26.0004 25.9915C23.1254 26.0001 12.0629 26.0001 7.83017 26.0001Z"
fill="url(#paint0_linear_87_7796)"
/>
<path
d="M25.5785 13.3149L18.043 16.3334L30.709 23.4647C31.5199 22.4065 32.0004 21.0916 32.0004 19.6669C32.0004 16.1857 29.1321 13.3605 25.5833 13.3337C25.5817 13.3274 25.5801 13.3212 25.5785 13.3149Z"
fill="url(#paint1_linear_87_7796)"
/>
<path
d="M7.06445 10.7028L18.0423 16.3333L25.5779 13.3148C24.5051 9.11261 20.6237 6 15.9997 6C12.4141 6 9.27508 7.87166 7.54586 10.6716C7.3841 10.6773 7.22358 10.6877 7.06445 10.7028Z"
fill="url(#paint2_linear_87_7796)"
/>
<path
d="M1.7535 23.1687L18.0425 16.3331L7.06471 10.7026C3.09947 11.0792 0 14.3517 0 18.3331C0 20.1665 0.657197 21.8495 1.7535 23.1687Z"
fill="url(#paint3_linear_87_7796)"
/>
</g>
<defs>
<linearGradient
id="paint0_linear_87_7796"
x1="4.42591"
y1="24.6668"
x2="27.2309"
y2="23.2764"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#2086B8" />
<stop offset="1" stop-color="#46D3F6" />
</linearGradient>
<linearGradient
id="paint1_linear_87_7796"
x1="23.8302"
y1="19.6668"
x2="30.2108"
y2="15.2082"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#1694DB" />
<stop offset="1" stop-color="#62C3FE" />
</linearGradient>
<linearGradient
id="paint2_linear_87_7796"
x1="8.51037"
y1="7.33333"
x2="23.3335"
y2="15.9348"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#0D3D78" />
<stop offset="1" stop-color="#063B83" />
</linearGradient>
<linearGradient
id="paint3_linear_87_7796"
x1="-0.340429"
y1="19.9998"
x2="14.5634"
y2="14.4649"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#16589B" />
<stop offset="1" stop-color="#1464B7" />
</linearGradient>
</defs>
</svg>
<div class="line-clamp-1">{$i18n.t('Microsoft OneDrive')}</div>
</DropdownMenu.SubTrigger>
<DropdownMenu.SubContent
class="w-[calc(100vw-2rem)] max-w-[280px] rounded-xl px-1 py-1 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-sm"
side={$mobile ? 'bottom' : 'right'}
sideOffset={$mobile ? 5 : 0}
alignOffset={$mobile ? 0 : -8}
>
<div class="flex flex-col">
<div class="line-clamp-1">{$i18n.t('Microsoft OneDrive (work/school)')}</div>
<div class="text-xs text-gray-500">Includes SharePoint</div>
</div>
</DropdownMenu.Item>
</DropdownMenu.SubContent>
</DropdownMenu.Sub>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
on:click={() => {
uploadOneDriveHandler('personal');
}}
>
<div class="line-clamp-1">{$i18n.t('Microsoft OneDrive (personal)')}</div>
</DropdownMenu.Item>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
on:click={() => {
uploadOneDriveHandler('organizations');
}}
>
<div class="flex flex-col">
<div class="line-clamp-1">{$i18n.t('Microsoft OneDrive (work/school)')}</div>
<div class="text-xs text-gray-500">Includes SharePoint</div>
</div>
</DropdownMenu.Item>
</DropdownMenu.SubContent>
</DropdownMenu.Sub>
{/if}
{/if}
</DropdownMenu.Content>
</div>

View File

@ -17,16 +17,19 @@
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
import ChevronUpDown from '$lib/components/icons/ChevronUpDown.svelte';
import CommandLine from '$lib/components/icons/CommandLine.svelte';
import Cube from '$lib/components/icons/Cube.svelte';
const i18n = getContext('i18n');
export let id = '';
export let onSave = (e) => {};
export let onCode = (e) => {};
export let onUpdate = (e) => {};
export let onPreview = (e) => {};
export let save = false;
export let run = true;
export let preview = false;
export let collapsed = false;
export let token;
@ -88,6 +91,10 @@
}, 1000);
};
const previewCode = () => {
onPreview(code);
};
const checkPythonCode = (str) => {
// Check if the string contains typical Python syntax characters
const pythonSyntax = [
@ -333,6 +340,8 @@
await drawMermaidDiagram();
})();
}
onUpdate(token);
};
$: if (token) {
@ -345,8 +354,6 @@
render();
}
$: onCode({ lang, code });
$: if (attributes) {
onAttributesUpdate();
}
@ -379,10 +386,10 @@
onMount(async () => {
console.log('codeblock', lang, code);
if (lang) {
onCode({ lang, code });
if (token) {
onUpdate(token);
}
if (document.documentElement.classList.contains('dark')) {
mermaid.initialize({
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"
on:click={collapseCodeBlock}
>
<div>
<div class=" -translate-y-[0.5px]">
<ChevronUpDown className="size-3" />
</div>
@ -439,6 +446,21 @@
</div>
</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 executing}
<div class="run-code-button bg-none border-none p-1 cursor-not-allowed">
@ -453,7 +475,7 @@
executePython(code);
}}
>
<div>
<div class=" -translate-y-[0.5px]">
<CommandLine className="size-3" />
</div>

View File

@ -1,10 +1,17 @@
<script>
import { onDestroy, onMount, tick, getContext, createEventDispatcher } from 'svelte';
import { onDestroy, onMount, tick, getContext } from 'svelte';
const i18n = getContext('i18n');
const dispatch = createEventDispatcher();
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 { createMessagesList } from '$lib/utils';
@ -15,8 +22,10 @@
export let sources = null;
export let save = false;
export let preview = false;
export let floatingButtons = true;
export let onSave = () => {};
export let onSourceClick = () => {};
export let onTaskClick = () => {};
@ -122,6 +131,7 @@
{content}
{model}
{save}
{preview}
sourceIds={(sources ?? []).reduce((acc, s) => {
let ids = [];
s.document.forEach((document, index) => {
@ -154,11 +164,9 @@
}, [])}
{onSourceClick}
{onTaskClick}
onUpdate={(value) => {
dispatch('update', value);
}}
onCode={(value) => {
const { lang, code } = value;
{onSave}
onUpdate={(token) => {
const { lang, text: code } = token;
if (
($settings?.detectArtifacts ?? true) &&
@ -170,6 +178,13 @@
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>

View File

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

View File

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

View File

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

View File

@ -114,7 +114,9 @@
</div>
{#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>
</div>
{/if}

View File

@ -100,10 +100,10 @@
.filter((item) => {
if (selectedConnectionType === '') {
return true;
} else if (selectedConnectionType === 'ollama') {
return item.model?.owned_by === 'ollama';
} else if (selectedConnectionType === 'openai') {
return item.model?.owned_by === 'openai';
} else if (selectedConnectionType === 'local') {
return item.model?.connection_type === 'local';
} else if (selectedConnectionType === 'external') {
return item.model?.connection_type === 'external';
} else if (selectedConnectionType === 'direct') {
return item.model?.direct;
}
@ -118,10 +118,10 @@
.filter((item) => {
if (selectedConnectionType === '') {
return true;
} else if (selectedConnectionType === 'ollama') {
return item.model?.owned_by === 'ollama';
} else if (selectedConnectionType === 'openai') {
return item.model?.owned_by === 'openai';
} else if (selectedConnectionType === 'local') {
return item.model?.connection_type === 'local';
} else if (selectedConnectionType === 'external') {
return item.model?.connection_type === 'external';
} else if (selectedConnectionType === '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"
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
class="min-w-fit outline-none p-1.5 {selectedTag === '' &&
selectedConnectionType === ''
@ -408,25 +408,25 @@
</button>
{/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
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"
on:click={() => {
selectedTag = '';
selectedConnectionType = 'ollama';
selectedConnectionType = 'local';
}}
>
{$i18n.t('Local')}
</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"
on:click={() => {
selectedTag = '';
selectedConnectionType = 'openai';
selectedConnectionType = 'external';
}}
>
{$i18n.t('External')}

View File

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

View File

@ -32,6 +32,8 @@
let notificationSound = true;
let notificationSoundAlways = false;
let highContrastMode = false;
let detectArtifacts = true;
let richTextInput = true;
@ -85,6 +87,11 @@
saveSettings({ splitLargeChunks: splitLargeChunks });
};
const toggleHighContrastMode = async () => {
highContrastMode = !highContrastMode;
saveSettings({ highContrastMode: highContrastMode });
};
const togglePromptAutocomplete = async () => {
promptAutocomplete = !promptAutocomplete;
saveSettings({ promptAutocomplete: promptAutocomplete });
@ -281,6 +288,8 @@
titleAutoGenerate = $settings?.title?.auto ?? true;
autoTags = $settings.autoTags ?? true;
highContrastMode = $settings.highContrastMode ?? false;
detectArtifacts = $settings.detectArtifacts ?? true;
responseAutoCopy = $settings.responseAutoCopy ?? false;
@ -370,6 +379,28 @@
<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 class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs">{$i18n.t('Landing Page Mode')}</div>

View File

@ -64,7 +64,7 @@
}
</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}
<Bolt />
{$i18n.t('Suggested')}
@ -74,7 +74,7 @@
<div
class="flex w-full {$settings?.landingPageMode === 'chat'
? ' -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}
</div>
@ -98,7 +98,7 @@
>
{prompt.title[0]}
</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]}
</div>
{:else}
@ -107,7 +107,9 @@
>
{prompt.content}
</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}
</div>
</button>

View File

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

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

View File

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

View File

@ -14,7 +14,7 @@
</script>
<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">
{$i18n.t(`A new version (v{{LATEST_VERSION}}) is now available.`, {

View File

@ -243,7 +243,7 @@
</div>
<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
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}"

View File

@ -6,16 +6,45 @@
const i18n = getContext('i18n');
const helpText = {
vision: $i18n.t('Model accepts image inputs'),
usage: $i18n.t(
'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')
const capabilityLabels = {
vision: {
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.'
)
},
citations: {
label: $i18n.t('Citations'),
description: $i18n.t('Displays citations in the response')
}
};
export let capabilities: {
vision?: boolean;
file_upload?: boolean;
web_search?: boolean;
image_generation?: boolean;
code_interpreter?: boolean;
usage?: boolean;
citations?: boolean;
} = {};
@ -26,7 +55,7 @@
<div class=" self-center text-sm font-semibold">{$i18n.t('Capabilities')}</div>
</div>
<div class="flex">
{#each Object.keys(capabilities) as capability}
{#each Object.keys(capabilityLabels) as capability}
<div class=" flex items-center gap-2 mr-3">
<Checkbox
state={capabilities[capability] ? 'checked' : 'unchecked'}
@ -36,8 +65,8 @@
/>
<div class=" py-0.5 text-sm capitalize">
<Tooltip content={marked.parse(helpText[capability])}>
{$i18n.t(capability)}
<Tooltip content={marked.parse(capabilityLabels[capability].description)}>
{$i18n.t(capabilityLabels[capability].label)}
</Tooltip>
</div>
</div>

View File

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

View File

@ -74,6 +74,8 @@
"Allow User Location": "",
"Allow Voice Interruption in Call": "",
"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?": "هل تملك حساب ؟",
"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": "",
@ -184,6 +186,7 @@
"Chunk Size": "Chunk حجم",
"Ciphers": "",
"Citation": "اقتباس",
"Citations": "",
"Clear memory": "",
"Clear Memory": "",
"click here": "",
@ -316,6 +319,7 @@
"Deleted {{deleteModelTag}}": "{{deleteModelTag}} حذف",
"Deleted {{name}}": "حذف {{name}}",
"Deleted User": "",
"Describe Pictures in Documents": "",
"Describe your knowledge base and objectives": "",
"Description": "وصف",
"Detect Artifacts Automatically": "",
@ -373,6 +377,7 @@
"e.g. My Tools": "",
"e.g. my_filter": "",
"e.g. my_tools": "",
"e.g. pdf, docx, txt": "",
"e.g. Tools for performing various operations": "",
"e.g., 3, 4, 5 (leave blank for default)": "",
"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.",
"Enter {{role}} message here": "أدخل رسالة {{role}} هنا",
"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 Application DN": "",
"Enter Application DN Password": "",
@ -422,6 +429,7 @@
"Enter Chunk Overlap": "أدخل الChunk Overlap",
"Enter Chunk Size": "أدخل Chunk الحجم",
"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 Docling OCR Engine": "",
"Enter Docling OCR Language(s)": "",
@ -430,6 +438,8 @@
"Enter Document Intelligence Key": "",
"Enter domains separated by commas (e.g., example.com,site.org)": "",
"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 URL": "",
"Enter External Web Search API Key": "",
@ -537,6 +547,7 @@
"Export to CSV": "",
"Export Tools": "",
"External": "",
"External Document Loader URL required.": "",
"External Models": "",
"External Web Loader API Key": "",
"External Web Loader URL": "",
@ -566,6 +577,7 @@
"File not found.": "لم يتم العثور على الملف.",
"File removed successfully.": "",
"File size should not exceed {{maxSize}} MB.": "",
"File Upload": "",
"File uploaded successfully": "",
"Files": "",
"Filter is now globally disabled": "",
@ -631,6 +643,7 @@
"Hex Color - Leave empty for default color": "",
"Hide": "أخفاء",
"Hide Model": "",
"High Contrast Mode": "",
"Home": "",
"Host": "",
"How can I help you today?": "كيف استطيع مساعدتك اليوم؟",
@ -774,7 +787,11 @@
"Model {{name}} is now {{status}}": "نموذج {{name}} هو الآن {{status}}",
"Model {{name}} is now hidden": "",
"Model {{name}} is now visible": "",
"Model accepts file 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 filesystem path detected. Model shortname is required for update, cannot continue.": "تم اكتشاف مسار نظام الملفات النموذجي. الاسم المختصر للنموذج مطلوب للتحديث، ولا يمكن الاستمرار.",
"Model Filtering": "",
@ -785,6 +802,7 @@
"Model Params": "معلمات النموذج",
"Model Permissions": "",
"Model updated successfully": "",
"Model(s) do not support file upload": "",
"Modelfile Content": "محتوى الملف النموذجي",
"Models": "الموديلات",
"Models Access": "",
@ -881,6 +899,8 @@
"PDF document (.pdf)": "PDF ملف (.pdf)",
"PDF Extract Images (OCR)": "PDF أستخرج الصور (OCR)",
"pending": "قيد الانتظار",
"Pending User Overlay Content": "",
"Pending User Overlay Title": "",
"Permission denied when accessing media devices": "",
"Permission denied when accessing microphone": "",
"Permission denied when accessing microphone: {{error}}": "{{error}} تم رفض الإذن عند الوصول إلى الميكروفون ",
@ -914,6 +934,7 @@
"Prefix ID": "",
"Prefix ID is used to avoid conflicts with other connections by adding a prefix to the model IDs - leave empty to disable": "",
"Presence Penalty": "",
"Preview": "",
"Previous 30 days": "أخر 30 يوم",
"Previous 7 days": "أخر 7 أيام",
"Private": "",
@ -968,6 +989,7 @@
"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 splitting": "",
"Response Watermark": "",
"Result": "",
"Retrieval": "",
"Retrieval Query Generation": "",
@ -1233,6 +1255,7 @@
"Upload Progress": "جاري التحميل",
"URL": "",
"URL Mode": "رابط الموديل",
"Usage": "",
"Use '#' in the prompt input to load and include your knowledge.": "",
"Use Gravatar": "Gravatar أستخدم",
"Use groups to group your users and assign permissions.": "",
@ -1262,6 +1285,7 @@
"View Replies": "",
"View Result from **{{NAME}}**": "",
"Visibility": "",
"Vision": "",
"Voice": "",
"Voice Input": "",
"Warning": "تحذير",

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