Merge pull request #13707 from open-webui/dev

0.6.8
This commit is contained in:
Tim Jaeryang Baek 2025-05-10 19:31:52 +04:00 committed by GitHub
commit ef301aa16b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
96 changed files with 3002 additions and 2264 deletions

View file

@ -5,6 +5,40 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.6.8] - 2025-05-10
### Added
- 🏆 **External Reranker Support for Knowledge Base Search**: Supercharge your Retrieval-Augmented Generation (RAG) workflows with the new External Reranker integration; easily plug in advanced reranking services via the UI to deliver sharper and more relevant search results, accelerating research and insight discovery.
- 📤 **Unstylized PDF Export Option (Reduced File Size)**: When exporting chat transcripts or documents, you can now choose an unstylized PDF export for snappier downloads, minimal file size, and clean data archiving—perfect for large-scale storage or sharing.
- 📝 **Vazirmatn Font for Persian & Arabic**: Arabic and Persian users will now see their text beautifully rendered with the specialized Vazirmatn font for an improved localized reading experience.
- 🏷️ **SharePoint Tenant ID Support for OneDrive**: You can now specify a SharePoint tenant ID in OneDrive settings for seamless authentication and granular enterprise integration.
- 👤 **Refresh OAuth Profile Picture**: Your OAuth profile picture now updates in real-time, ensuring your presence and avatar always match your latest identity across integrated platforms.
- 🔧 **Milvus Configuration Improvements**: Configure index and metric types for Milvus directly within settings; take full control of your vector database for more accurate and robust AI search experiences.
- 🛡️ **S3 Tagging Toggle for Compatibility**: Optional S3 tagging via an environment toggle grants full compatibility with all storage backends—including those that dont support tagging like Cloudflare R2—ensuring error-free attachment and document management.
- 👨‍🦯 **Icon Button Accessibility Improvements**: Key interactive icon-buttons now include aria-labels and ARIA descriptions, so screen readers provide precise guidance about what action each button performs for improved accessibility.
- ♿ **Enhanced Accessibility with Modal Focus Trap**: Modal dialogs and pop-ups now feature a focus trap and improved ARIA roles, ensuring seamless navigation and screen reader support—making the interface friendlier for everyone, including keyboard and assistive tech users.
- 🏃 **Improved Admin User List Loading Indicator**: The user list loading experience is now clearer and more responsive in the admin panel.
- 🧑‍🤝‍🧑 **Larger Admin User List Page Size**: Admins can now manage up to 30 users per page in the admin interface, drastically reducing pagination and making large user teams easier and faster to manage.
- 🌠 **Default Code Interpreter Prompt Clarified**: The built-in code interpreter prompt is now more explicit, preventing AI from wrapping code in Markdown blocks when not needed—ensuring properly formatted code runs as intended every time.
- 🧾 **Improved Default Title Generation Prompt Template**: Title generation now uses a robust template for reliable JSON output, improving chat organization and searchability.
- 🔗 **Support Jupyter Notebooks with Non-Root Base URLs**: Notebook-based code execution now supports non-root deployed Jupyter servers, granting full flexibility for hybrid or multi-user setups.
- 📰 **UI Scrollbar Always Visible for Overflow Tools**: When available tools overflow the display, the scrollbar is now always visible and theres a handy "show all" toggle, making navigation of large toolsets snappier and more intuitive.
- 🛠️ **General Backend Refactoring for Stability**: Multiple under-the-hood improvements have been made across backend components, ensuring smoother performance, fewer errors, and a more reliable overall experience for all users.
- 🚀 **Optimized Web Search for Faster Results**: Web search speed and performance have been significantly enhanced, delivering answers and sources in record time to accelerate your research-heavy workflows.
- 💡 **More Supported Languages**: Expanded language support ensures an even wider range of users can enjoy an intuitive and natural interface in their native tongue.
### Fixed
- 🏃‍♂️ **Exhausting Workers in Nginx Reverse Proxy Due to Websocket Fix**: Websocket sessions are now fully compatible behind Nginx, eliminating worker exhaustion and restoring 24/7 reliability for real-time chats even in complex deployments.
- 🎤 **Audio Transcription Issue with OpenAI Resolved**: OpenAI-based audio transcription now handles WebM and newer formats without error, ensuring seamless voice-to-text workflows every time.
- 👉 **Message Input RTL Issue Fixed**: The chat message input now displays correctly for right-to-left languages, creating a flawless typing and reading experience for Arabic, Hebrew, and more.
- 🀄 **Katex: Proper Rendering of Chinese Characters Next to Math**: Math formulas now render perfectly even when directly adjacent to Chinese (CJK) characters, improving visual clarity for multilingual teams and cross-language documents.
- 🔂 **Duplicate Web Search URLs Eliminated**: Search results now reliably filter out URL duplicates, so your knowledge and search citations are always clean, trimmed, and easy to review.
- 📄 **Markdown Rendering Fixed in Knowledge Bases**: Markdown is now displayed correctly within knowledge bases, enabling better formatting and clarity of information-rich files.
- 🗂️ **LDAP Import/Loading Issue Resolved**: LDAP user imports process correctly, ensuring smooth onboarding and access without interruption.
- 🌎 **Pinecone Batch Operations and Async Safety**: All Pinecone operations (batch insert, upsert, delete) now run efficiently and safely in an async environment, boosting performance and preventing slowdowns in large-scale RAG jobs.
## [0.6.7] - 2025-05-07
### Added

View file

@ -1,64 +0,0 @@
# Run with
# caddy run --envfile ./example.env --config ./Caddyfile.localhost
#
# This is configured for
# - Automatic HTTPS (even for localhost)
# - Reverse Proxying to Ollama API Base URL (http://localhost:11434/api)
# - CORS
# - HTTP Basic Auth API Tokens (uncomment basicauth section)
# CORS Preflight (OPTIONS) + Request (GET, POST, PATCH, PUT, DELETE)
(cors-api) {
@match-cors-api-preflight method OPTIONS
handle @match-cors-api-preflight {
header {
Access-Control-Allow-Origin "{http.request.header.origin}"
Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS"
Access-Control-Allow-Headers "Origin, Accept, Authorization, Content-Type, X-Requested-With"
Access-Control-Allow-Credentials "true"
Access-Control-Max-Age "3600"
defer
}
respond "" 204
}
@match-cors-api-request {
not {
header Origin "{http.request.scheme}://{http.request.host}"
}
header Origin "{http.request.header.origin}"
}
handle @match-cors-api-request {
header {
Access-Control-Allow-Origin "{http.request.header.origin}"
Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS"
Access-Control-Allow-Headers "Origin, Accept, Authorization, Content-Type, X-Requested-With"
Access-Control-Allow-Credentials "true"
Access-Control-Max-Age "3600"
defer
}
}
}
# replace localhost with example.com or whatever
localhost {
## HTTP Basic Auth
## (uncomment to enable)
# basicauth {
# # see .example.env for how to generate tokens
# {env.OLLAMA_API_ID} {env.OLLAMA_API_TOKEN_DIGEST}
# }
handle /api/* {
# Comment to disable CORS
import cors-api
reverse_proxy localhost:11434
}
# Same-Origin Static Web Server
file_server {
root ./build/
}
}

View file

@ -109,54 +109,7 @@ if os.path.exists(f"{DATA_DIR}/config.json"):
DEFAULT_CONFIG = {
"version": 0,
"ui": {
"default_locale": "",
"prompt_suggestions": [
{
"title": [
"Help me study",
"vocabulary for a college entrance exam",
],
"content": "Help me study vocabulary: write a sentence for me to fill in the blank, and I'll try to pick the correct option.",
},
{
"title": [
"Give me ideas",
"for what to do with my kids' art",
],
"content": "What are 5 creative things I could do with my kids' art? I don't want to throw them away, but it's also so much clutter.",
},
{
"title": ["Tell me a fun fact", "about the Roman Empire"],
"content": "Tell me a random fun fact about the Roman Empire",
},
{
"title": [
"Show me a code snippet",
"of a website's sticky header",
],
"content": "Show me a code snippet of a website's sticky header in CSS and JavaScript.",
},
{
"title": [
"Explain options trading",
"if I'm familiar with buying and selling stocks",
],
"content": "Explain options trading in simple terms if I'm familiar with buying and selling stocks.",
},
{
"title": ["Overcome procrastination", "give me tips"],
"content": "Could you start by asking me about instances when I procrastinate the most and then give me some suggestions to overcome it?",
},
{
"title": [
"Grammar check",
"rewrite it for better readability ",
],
"content": 'Check the following sentence for grammar and clarity: "[sentence]". Rewrite it for better readability while maintaining its original meaning.',
},
],
},
"ui": {},
}
@ -552,6 +505,12 @@ OAUTH_ALLOWED_DOMAINS = PersistentConfig(
],
)
OAUTH_UPDATE_PICTURE_ON_LOGIN = PersistentConfig(
"OAUTH_UPDATE_PICTURE_ON_LOGIN",
"oauth.update_picture_on_login",
os.environ.get("OAUTH_UPDATE_PICTURE_ON_LOGIN", "False").lower() == "true",
)
def load_oauth_providers():
OAUTH_PROVIDERS.clear()
@ -761,9 +720,10 @@ S3_BUCKET_NAME = os.environ.get("S3_BUCKET_NAME", None)
S3_KEY_PREFIX = os.environ.get("S3_KEY_PREFIX", None)
S3_ENDPOINT_URL = os.environ.get("S3_ENDPOINT_URL", None)
S3_USE_ACCELERATE_ENDPOINT = (
os.environ.get("S3_USE_ACCELERATE_ENDPOINT", "False").lower() == "true"
os.environ.get("S3_USE_ACCELERATE_ENDPOINT", "false").lower() == "true"
)
S3_ADDRESSING_STYLE = os.environ.get("S3_ADDRESSING_STYLE", None)
S3_ENABLE_TAGGING = os.getenv("S3_ENABLE_TAGGING", "false").lower() == "true"
GCS_BUCKET_NAME = os.environ.get("GCS_BUCKET_NAME", None)
GOOGLE_APPLICATION_CREDENTIALS_JSON = os.environ.get(
@ -1255,7 +1215,16 @@ ENABLE_USER_WEBHOOKS = PersistentConfig(
)
# FastAPI / AnyIO settings
THREAD_POOL_SIZE = int(os.getenv("THREAD_POOL_SIZE", "0"))
THREAD_POOL_SIZE = os.getenv("THREAD_POOL_SIZE", None)
if THREAD_POOL_SIZE is not None and isinstance(THREAD_POOL_SIZE, str):
try:
THREAD_POOL_SIZE = int(THREAD_POOL_SIZE)
except ValueError:
log.warning(
f"THREAD_POOL_SIZE is not a valid integer: {THREAD_POOL_SIZE}. Defaulting to None."
)
THREAD_POOL_SIZE = None
def validate_cors_origins(origins):
@ -1357,6 +1326,9 @@ Generate a concise, 3-5 word title with an emoji summarizing the chat history.
- Use emojis that enhance understanding of the topic, but avoid quotation marks or special formatting.
- Write the title in the chat's primary language; default to English if multilingual.
- Prioritize accuracy over excessive creativity; keep it clear and simple.
- Your entire response must consist solely of the JSON object, without any introductory or concluding text.
- The output must be a single, raw JSON object, without any markdown code fences or other encapsulating text.
- Ensure no conversational text, affirmations, or explanations precede or follow the raw JSON output, as this will cause direct parsing failure.
### Output:
JSON format: { "title": "your concise title here" }
### Examples:
@ -1699,7 +1671,8 @@ DEFAULT_CODE_INTERPRETER_PROMPT = """
1. **Code Interpreter**: `<code_interpreter type="code" lang="python"></code_interpreter>`
- You have access to a Python shell that runs directly in the user's browser, enabling fast execution of code for analysis, calculations, or problem-solving. Use it in this response.
- The Python code you write can incorporate a wide array of libraries, handle data manipulation or visualization, perform API calls for web-related tasks, or tackle virtually any computational challenge. Use this flexibility to **think outside the box, craft elegant solutions, and harness Python's full potential**.
- To use it, **you must enclose your code within `<code_interpreter type="code" lang="python">` XML tags** and stop right away. If you don't, the code won't execute. Do NOT use triple backticks.
- To use it, **you must enclose your code within `<code_interpreter type="code" lang="python">` XML tags** and stop right away. If you don't, the code won't execute.
- When writing code in the code_interpreter XML tag, Do NOT use the triple backticks code block for markdown formatting, example: ```py # python code ``` will cause an error because it is markdown formatting, it is not python code.
- When coding, **always aim to print meaningful outputs** (e.g., results, tables, summaries, or visuals) to better interpret and verify the findings. Avoid relying on implicit outputs; prioritize explicit and clear print statements so the results are effectively communicated to the user.
- After obtaining the printed output, **always provide a concise analysis, interpretation, or next steps to help the user understand the findings or refine the outcome further.**
- If the results are unclear, unexpected, or require validation, refine the code and execute it again as needed. Always aim to deliver meaningful insights from the results, iterating if necessary.
@ -1746,6 +1719,12 @@ MILVUS_URI = os.environ.get("MILVUS_URI", f"{DATA_DIR}/vector_db/milvus.db")
MILVUS_DB = os.environ.get("MILVUS_DB", "default")
MILVUS_TOKEN = os.environ.get("MILVUS_TOKEN", None)
MILVUS_INDEX_TYPE = os.environ.get("MILVUS_INDEX_TYPE", "HNSW")
MILVUS_METRIC_TYPE = os.environ.get("MILVUS_METRIC_TYPE", "COSINE")
MILVUS_HNSW_M = int(os.environ.get("MILVUS_HNSW_M", "16"))
MILVUS_HNSW_EFCONSTRUCTION = int(os.environ.get("MILVUS_HNSW_EFCONSTRUCTION", "100"))
MILVUS_IVF_FLAT_NLIST = int(os.environ.get("MILVUS_IVF_FLAT_NLIST", "128"))
# Qdrant
QDRANT_URI = os.environ.get("QDRANT_URI", None)
QDRANT_API_KEY = os.environ.get("QDRANT_API_KEY", None)
@ -1833,6 +1812,11 @@ ONEDRIVE_SHAREPOINT_URL = PersistentConfig(
os.environ.get("ONEDRIVE_SHAREPOINT_URL", ""),
)
ONEDRIVE_SHAREPOINT_TENANT_ID = PersistentConfig(
"ONEDRIVE_SHAREPOINT_TENANT_ID",
"onedrive.sharepoint_tenant_id",
os.environ.get("ONEDRIVE_SHAREPOINT_TENANT_ID", ""),
)
# RAG Content Extraction
CONTENT_EXTRACTION_ENGINE = PersistentConfig(
@ -1981,6 +1965,12 @@ RAG_EMBEDDING_PREFIX_FIELD_NAME = os.environ.get(
"RAG_EMBEDDING_PREFIX_FIELD_NAME", None
)
RAG_RERANKING_ENGINE = PersistentConfig(
"RAG_RERANKING_ENGINE",
"rag.reranking_engine",
os.environ.get("RAG_RERANKING_ENGINE", ""),
)
RAG_RERANKING_MODEL = PersistentConfig(
"RAG_RERANKING_MODEL",
"rag.reranking_model",
@ -1989,6 +1979,7 @@ RAG_RERANKING_MODEL = PersistentConfig(
if RAG_RERANKING_MODEL.value != "":
log.info(f"Reranking model set: {RAG_RERANKING_MODEL.value}")
RAG_RERANKING_MODEL_AUTO_UPDATE = (
not OFFLINE_MODE
and os.environ.get("RAG_RERANKING_MODEL_AUTO_UPDATE", "True").lower() == "true"
@ -1998,6 +1989,18 @@ RAG_RERANKING_MODEL_TRUST_REMOTE_CODE = (
os.environ.get("RAG_RERANKING_MODEL_TRUST_REMOTE_CODE", "True").lower() == "true"
)
RAG_EXTERNAL_RERANKER_URL = PersistentConfig(
"RAG_EXTERNAL_RERANKER_URL",
"rag.external_reranker_url",
os.environ.get("RAG_EXTERNAL_RERANKER_URL", ""),
)
RAG_EXTERNAL_RERANKER_API_KEY = PersistentConfig(
"RAG_EXTERNAL_RERANKER_API_KEY",
"rag.external_reranker_api_key",
os.environ.get("RAG_EXTERNAL_RERANKER_API_KEY", ""),
)
RAG_TEXT_SPLITTER = PersistentConfig(
"RAG_TEXT_SPLITTER",

View file

@ -103,6 +103,7 @@ from open_webui.config import (
ENABLE_OPENAI_API,
ONEDRIVE_CLIENT_ID,
ONEDRIVE_SHAREPOINT_URL,
ONEDRIVE_SHAREPOINT_TENANT_ID,
OPENAI_API_BASE_URLS,
OPENAI_API_KEYS,
OPENAI_API_CONFIGS,
@ -187,7 +188,10 @@ from open_webui.config import (
RAG_EMBEDDING_MODEL,
RAG_EMBEDDING_MODEL_AUTO_UPDATE,
RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE,
RAG_RERANKING_ENGINE,
RAG_RERANKING_MODEL,
RAG_EXTERNAL_RERANKER_URL,
RAG_EXTERNAL_RERANKER_API_KEY,
RAG_RERANKING_MODEL_AUTO_UPDATE,
RAG_RERANKING_MODEL_TRUST_REMOTE_CODE,
RAG_EMBEDDING_ENGINE,
@ -255,6 +259,7 @@ from open_webui.config import (
GOOGLE_DRIVE_API_KEY,
ONEDRIVE_CLIENT_ID,
ONEDRIVE_SHAREPOINT_URL,
ONEDRIVE_SHAREPOINT_TENANT_ID,
ENABLE_RAG_HYBRID_SEARCH,
ENABLE_RAG_LOCAL_WEB_FETCH,
ENABLE_WEB_LOADER_SSL_VERIFICATION,
@ -459,10 +464,9 @@ async def lifespan(app: FastAPI):
log.info("Installing external dependencies of functions and tools...")
install_tool_and_function_dependencies()
pool_size = THREAD_POOL_SIZE
if pool_size and pool_size > 0:
if THREAD_POOL_SIZE and THREAD_POOL_SIZE > 0:
limiter = anyio.to_thread.current_default_thread_limiter()
limiter.total_tokens = pool_size
limiter.total_tokens = THREAD_POOL_SIZE
asyncio.create_task(periodic_usage_pool_cleanup())
@ -654,7 +658,12 @@ app.state.config.CHUNK_OVERLAP = CHUNK_OVERLAP
app.state.config.RAG_EMBEDDING_ENGINE = RAG_EMBEDDING_ENGINE
app.state.config.RAG_EMBEDDING_MODEL = RAG_EMBEDDING_MODEL
app.state.config.RAG_EMBEDDING_BATCH_SIZE = RAG_EMBEDDING_BATCH_SIZE
app.state.config.RAG_RERANKING_ENGINE = RAG_RERANKING_ENGINE
app.state.config.RAG_RERANKING_MODEL = RAG_RERANKING_MODEL
app.state.config.RAG_EXTERNAL_RERANKER_URL = RAG_EXTERNAL_RERANKER_URL
app.state.config.RAG_EXTERNAL_RERANKER_API_KEY = RAG_EXTERNAL_RERANKER_API_KEY
app.state.config.RAG_TEMPLATE = RAG_TEMPLATE
app.state.config.RAG_OPENAI_API_BASE_URL = RAG_OPENAI_API_BASE_URL
@ -735,7 +744,10 @@ try:
)
app.state.rf = get_rf(
app.state.config.RAG_RERANKING_ENGINE,
app.state.config.RAG_RERANKING_MODEL,
app.state.config.RAG_EXTERNAL_RERANKER_URL,
app.state.config.RAG_EXTERNAL_RERANKER_API_KEY,
RAG_RERANKING_MODEL_AUTO_UPDATE,
)
except Exception as e:
@ -1381,6 +1393,7 @@ async def get_app_config(request: Request):
"onedrive": {
"client_id": ONEDRIVE_CLIENT_ID.value,
"sharepoint_url": ONEDRIVE_SHAREPOINT_URL.value,
"sharepoint_tenant_id": ONEDRIVE_SHAREPOINT_TENANT_ID.value,
},
"license_metadata": app.state.LICENSE_METADATA,
**(

View file

@ -0,0 +1,58 @@
import logging
import requests
from typing import Optional, List, Tuple
from open_webui.env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
class ExternalReranker:
def __init__(
self,
api_key: str,
url: str = "http://localhost:8080/v1/rerank",
model: str = "reranker",
):
self.api_key = api_key
self.url = url
self.model = model
def predict(self, sentences: List[Tuple[str, str]]) -> Optional[List[float]]:
query = sentences[0][0]
docs = [i[1] for i in sentences]
payload = {
"model": self.model,
"query": query,
"documents": docs,
"top_n": len(docs),
}
try:
log.info(f"ExternalReranker:predict:model {self.model}")
log.info(f"ExternalReranker:predict:query {query}")
r = requests.post(
f"{self.url}",
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {self.api_key}",
},
json=payload,
)
r.raise_for_status()
data = r.json()
if "results" in data:
sorted_results = sorted(data["results"], key=lambda x: x["index"])
return [result["relevance_score"] for result in sorted_results]
else:
log.error("No results found in external reranking response")
return None
except Exception as e:
log.exception(f"Error in external reranking: {e}")
return None

View file

@ -3,7 +3,6 @@ from pymilvus import FieldSchema, DataType
import json
import logging
from typing import Optional
from open_webui.retrieval.vector.main import (
VectorDBBase,
VectorItem,
@ -14,6 +13,11 @@ from open_webui.config import (
MILVUS_URI,
MILVUS_DB,
MILVUS_TOKEN,
MILVUS_INDEX_TYPE,
MILVUS_METRIC_TYPE,
MILVUS_HNSW_M,
MILVUS_HNSW_EFCONSTRUCTION,
MILVUS_IVF_FLAT_NLIST,
)
from open_webui.env import SRC_LOG_LEVELS
@ -33,7 +37,6 @@ class MilvusClient(VectorDBBase):
ids = []
documents = []
metadatas = []
for match in result:
_ids = []
_documents = []
@ -42,11 +45,9 @@ class MilvusClient(VectorDBBase):
_ids.append(item.get("id"))
_documents.append(item.get("data", {}).get("text"))
_metadatas.append(item.get("metadata"))
ids.append(_ids)
documents.append(_documents)
metadatas.append(_metadatas)
return GetResult(
**{
"ids": ids,
@ -60,13 +61,11 @@ class MilvusClient(VectorDBBase):
distances = []
documents = []
metadatas = []
for match in result:
_ids = []
_distances = []
_documents = []
_metadatas = []
for item in match:
_ids.append(item.get("id"))
# normalize milvus score from [-1, 1] to [0, 1] range
@ -75,12 +74,10 @@ class MilvusClient(VectorDBBase):
_distances.append(_dist)
_documents.append(item.get("entity", {}).get("data", {}).get("text"))
_metadatas.append(item.get("entity", {}).get("metadata"))
ids.append(_ids)
distances.append(_distances)
documents.append(_documents)
metadatas.append(_metadatas)
return SearchResult(
**{
"ids": ids,
@ -113,11 +110,39 @@ class MilvusClient(VectorDBBase):
)
index_params = self.client.prepare_index_params()
# Use configurations from config.py
index_type = MILVUS_INDEX_TYPE.upper()
metric_type = MILVUS_METRIC_TYPE.upper()
log.info(f"Using Milvus index type: {index_type}, metric type: {metric_type}")
index_creation_params = {}
if index_type == "HNSW":
index_creation_params = {
"M": MILVUS_HNSW_M,
"efConstruction": MILVUS_HNSW_EFCONSTRUCTION,
}
log.info(f"HNSW params: {index_creation_params}")
elif index_type == "IVF_FLAT":
index_creation_params = {"nlist": MILVUS_IVF_FLAT_NLIST}
log.info(f"IVF_FLAT params: {index_creation_params}")
elif index_type in ["FLAT", "AUTOINDEX"]:
log.info(f"Using {index_type} index with no specific build-time params.")
else:
log.warning(
f"Unsupported MILVUS_INDEX_TYPE: '{index_type}'. "
f"Supported types: HNSW, IVF_FLAT, FLAT, AUTOINDEX. "
f"Milvus will use its default for the collection if this type is not directly supported for index creation."
)
# For unsupported types, pass the type directly to Milvus; it might handle it or use a default.
# If Milvus errors out, the user needs to correct the MILVUS_INDEX_TYPE env var.
index_params.add_index(
field_name="vector",
index_type="HNSW",
metric_type="COSINE",
params={"M": 16, "efConstruction": 100},
index_type=index_type,
metric_type=metric_type,
params=index_creation_params,
)
self.client.create_collection(
@ -125,6 +150,9 @@ class MilvusClient(VectorDBBase):
schema=schema,
index_params=index_params,
)
log.info(
f"Successfully created collection '{self.collection_prefix}_{collection_name}' with index type '{index_type}' and metric '{metric_type}'."
)
def has_collection(self, collection_name: str) -> bool:
# Check if the collection exists based on the collection name.
@ -145,84 +173,113 @@ class MilvusClient(VectorDBBase):
) -> Optional[SearchResult]:
# Search for the nearest neighbor items based on the vectors and return 'limit' number of results.
collection_name = collection_name.replace("-", "_")
# For some index types like IVF_FLAT, search params like nprobe can be set.
# Example: search_params = {"nprobe": 10} if using IVF_FLAT
# For simplicity, not adding configurable search_params here, but could be extended.
result = self.client.search(
collection_name=f"{self.collection_prefix}_{collection_name}",
data=vectors,
limit=limit,
output_fields=["data", "metadata"],
# search_params=search_params # Potentially add later if needed
)
return self._result_to_search_result(result)
def query(self, collection_name: str, filter: dict, limit: Optional[int] = None):
# Construct the filter string for querying
collection_name = collection_name.replace("-", "_")
if not self.has_collection(collection_name):
log.warning(
f"Query attempted on non-existent collection: {self.collection_prefix}_{collection_name}"
)
return None
filter_string = " && ".join(
[
f'metadata["{key}"] == {json.dumps(value)}'
for key, value in filter.items()
]
)
max_limit = 16383 # The maximum number of records per request
all_results = []
if limit is None:
limit = float("inf") # Use infinity as a placeholder for no limit
# Milvus default limit for query if not specified is 16384, but docs mention iteration.
# Let's set a practical high number if "all" is intended, or handle true pagination.
# For now, if limit is None, we'll fetch in batches up to a very large number.
# This part could be refined based on expected use cases for "get all".
# For this function signature, None implies "as many as possible" up to Milvus limits.
limit = (
16384 * 10
) # A large number to signify fetching many, will be capped by actual data or max_limit per call.
log.info(
f"Limit not specified for query, fetching up to {limit} results in batches."
)
# Initialize offset and remaining to handle pagination
offset = 0
remaining = limit
try:
log.info(
f"Querying collection {self.collection_prefix}_{collection_name} with filter: '{filter_string}', limit: {limit}"
)
# Loop until there are no more items to fetch or the desired limit is reached
while remaining > 0:
log.info(f"remaining: {remaining}")
current_fetch = min(
max_limit, remaining
) # Determine how many items to fetch in this iteration
max_limit, remaining if isinstance(remaining, int) else max_limit
)
log.debug(
f"Querying with offset: {offset}, current_fetch: {current_fetch}"
)
results = self.client.query(
collection_name=f"{self.collection_prefix}_{collection_name}",
filter=filter_string,
output_fields=["*"],
output_fields=[
"id",
"data",
"metadata",
], # Explicitly list needed fields. Vector not usually needed in query.
limit=current_fetch,
offset=offset,
)
if not results:
log.debug("No more results from query.")
break
all_results.extend(results)
results_count = len(results)
remaining -= (
results_count # Decrease remaining by the number of items fetched
)
log.debug(f"Fetched {results_count} results in this batch.")
if isinstance(remaining, int):
remaining -= results_count
offset += results_count
# Break the loop if the results returned are less than the requested fetch count
# Break the loop if the results returned are less than the requested fetch count (means end of data)
if results_count < current_fetch:
log.debug(
"Fetched less than requested, assuming end of results for this query."
)
break
log.debug(all_results)
log.info(f"Total results from query: {len(all_results)}")
return self._result_to_get_result([all_results])
except Exception as e:
log.exception(
f"Error querying collection {collection_name} with limit {limit}: {e}"
f"Error querying collection {self.collection_prefix}_{collection_name} with filter '{filter_string}' and limit {limit}: {e}"
)
return None
def get(self, collection_name: str) -> Optional[GetResult]:
# Get all the items in the collection.
# Get all the items in the collection. This can be very resource-intensive for large collections.
collection_name = collection_name.replace("-", "_")
result = self.client.query(
collection_name=f"{self.collection_prefix}_{collection_name}",
filter='id != ""',
log.warning(
f"Fetching ALL items from collection '{self.collection_prefix}_{collection_name}'. This might be slow for large collections."
)
return self._result_to_get_result([result])
# Using query with a trivial filter to get all items.
# This will use the paginated query logic.
return self.query(collection_name=collection_name, filter={}, limit=None)
def insert(self, collection_name: str, items: list[VectorItem]):
# Insert the items into the collection, if the collection does not exist, it will be created.
@ -230,10 +287,23 @@ class MilvusClient(VectorDBBase):
if not self.client.has_collection(
collection_name=f"{self.collection_prefix}_{collection_name}"
):
log.info(
f"Collection {self.collection_prefix}_{collection_name} does not exist. Creating now."
)
if not items:
log.error(
f"Cannot create collection {self.collection_prefix}_{collection_name} without items to determine dimension."
)
raise ValueError(
"Cannot create Milvus collection without items to determine vector dimension."
)
self._create_collection(
collection_name=collection_name, dimension=len(items[0]["vector"])
)
log.info(
f"Inserting {len(items)} items into collection {self.collection_prefix}_{collection_name}."
)
return self.client.insert(
collection_name=f"{self.collection_prefix}_{collection_name}",
data=[
@ -253,10 +323,23 @@ class MilvusClient(VectorDBBase):
if not self.client.has_collection(
collection_name=f"{self.collection_prefix}_{collection_name}"
):
log.info(
f"Collection {self.collection_prefix}_{collection_name} does not exist for upsert. Creating now."
)
if not items:
log.error(
f"Cannot create collection {self.collection_prefix}_{collection_name} for upsert without items to determine dimension."
)
raise ValueError(
"Cannot create Milvus collection for upsert without items to determine vector dimension."
)
self._create_collection(
collection_name=collection_name, dimension=len(items[0]["vector"])
)
log.info(
f"Upserting {len(items)} items into collection {self.collection_prefix}_{collection_name}."
)
return self.client.upsert(
collection_name=f"{self.collection_prefix}_{collection_name}",
data=[
@ -276,30 +359,55 @@ class MilvusClient(VectorDBBase):
ids: Optional[list[str]] = None,
filter: Optional[dict] = None,
):
# Delete the items from the collection based on the ids.
# Delete the items from the collection based on the ids or filter.
collection_name = collection_name.replace("-", "_")
if not self.has_collection(collection_name):
log.warning(
f"Delete attempted on non-existent collection: {self.collection_prefix}_{collection_name}"
)
return None
if ids:
log.info(
f"Deleting items by IDs from {self.collection_prefix}_{collection_name}. IDs: {ids}"
)
return self.client.delete(
collection_name=f"{self.collection_prefix}_{collection_name}",
ids=ids,
)
elif filter:
# Convert the filter dictionary to a string using JSON_CONTAINS.
filter_string = " && ".join(
[
f'metadata["{key}"] == {json.dumps(value)}'
for key, value in filter.items()
]
)
log.info(
f"Deleting items by filter from {self.collection_prefix}_{collection_name}. Filter: {filter_string}"
)
return self.client.delete(
collection_name=f"{self.collection_prefix}_{collection_name}",
filter=filter_string,
)
else:
log.warning(
f"Delete operation on {self.collection_prefix}_{collection_name} called without IDs or filter. No action taken."
)
return None
def reset(self):
# Resets the database. This will delete all collections and item entries.
# Resets the database. This will delete all collections and item entries that match the prefix.
log.warning(
f"Resetting Milvus: Deleting all collections with prefix '{self.collection_prefix}'."
)
collection_names = self.client.list_collections()
for collection_name in collection_names:
if collection_name.startswith(self.collection_prefix):
self.client.drop_collection(collection_name=collection_name)
deleted_collections = []
for collection_name_full in collection_names:
if collection_name_full.startswith(self.collection_prefix):
try:
self.client.drop_collection(collection_name=collection_name_full)
deleted_collections.append(collection_name_full)
log.info(f"Deleted collection: {collection_name_full}")
except Exception as e:
log.error(f"Error deleting collection {collection_name_full}: {e}")
log.info(f"Milvus reset complete. Deleted collections: {deleted_collections}")

View file

@ -1,6 +1,13 @@
from typing import Optional, List, Dict, Any, Union
import logging
from pinecone import Pinecone, ServerlessSpec
import time # for measuring elapsed time
from pinecone import ServerlessSpec
import asyncio # for async upserts
import functools # for partial binding in async tasks
import concurrent.futures # for parallel batch upserts
from pinecone.grpc import PineconeGRPC # use gRPC client for faster upserts
from open_webui.retrieval.vector.main import (
VectorDBBase,
@ -40,8 +47,13 @@ class PineconeClient(VectorDBBase):
self.metric = PINECONE_METRIC
self.cloud = PINECONE_CLOUD
# Initialize Pinecone client
self.client = Pinecone(api_key=self.api_key)
# Initialize Pinecone gRPC client for improved performance
self.client = PineconeGRPC(
api_key=self.api_key, environment=self.environment, cloud=self.cloud
)
# Persistent executor for batch operations
self._executor = concurrent.futures.ThreadPoolExecutor(max_workers=5)
# Create index if it doesn't exist
self._initialize_index()
@ -191,27 +203,29 @@ class PineconeClient(VectorDBBase):
log.warning("No items to insert")
return
start_time = time.time()
collection_name_with_prefix = self._get_collection_name_with_prefix(
collection_name
)
points = self._create_points(items, collection_name_with_prefix)
# Insert in batches for better performance and reliability
# Parallelize batch inserts for performance
executor = self._executor
futures = []
for i in range(0, len(points), BATCH_SIZE):
batch = points[i : i + BATCH_SIZE]
futures.append(executor.submit(self.index.upsert, vectors=batch))
for future in concurrent.futures.as_completed(futures):
try:
self.index.upsert(vectors=batch)
log.debug(
f"Inserted batch of {len(batch)} vectors into '{collection_name_with_prefix}'"
)
future.result()
except Exception as e:
log.error(
f"Error inserting batch into '{collection_name_with_prefix}': {e}"
)
log.error(f"Error inserting batch: {e}")
raise
elapsed = time.time() - start_time
log.debug(f"Insert of {len(points)} vectors took {elapsed:.2f} seconds")
log.info(
f"Successfully inserted {len(items)} vectors into '{collection_name_with_prefix}'"
f"Successfully inserted {len(points)} vectors in parallel batches into '{collection_name_with_prefix}'"
)
def upsert(self, collection_name: str, items: List[VectorItem]) -> None:
@ -220,29 +234,119 @@ class PineconeClient(VectorDBBase):
log.warning("No items to upsert")
return
start_time = time.time()
collection_name_with_prefix = self._get_collection_name_with_prefix(
collection_name
)
points = self._create_points(items, collection_name_with_prefix)
# Parallelize batch upserts for performance
executor = self._executor
futures = []
for i in range(0, len(points), BATCH_SIZE):
batch = points[i : i + BATCH_SIZE]
futures.append(executor.submit(self.index.upsert, vectors=batch))
for future in concurrent.futures.as_completed(futures):
try:
future.result()
except Exception as e:
log.error(f"Error upserting batch: {e}")
raise
elapsed = time.time() - start_time
log.debug(f"Upsert of {len(points)} vectors took {elapsed:.2f} seconds")
log.info(
f"Successfully upserted {len(points)} vectors in parallel batches into '{collection_name_with_prefix}'"
)
async def insert_async(self, collection_name: str, items: List[VectorItem]) -> None:
"""Async version of insert using asyncio and run_in_executor for improved performance."""
if not items:
log.warning("No items to insert")
return
collection_name_with_prefix = self._get_collection_name_with_prefix(
collection_name
)
points = self._create_points(items, collection_name_with_prefix)
# Create batches
batches = [
points[i : i + BATCH_SIZE] for i in range(0, len(points), BATCH_SIZE)
]
loop = asyncio.get_event_loop()
tasks = [
loop.run_in_executor(
None, functools.partial(self.index.upsert, vectors=batch)
)
for batch in batches
]
results = await asyncio.gather(*tasks, return_exceptions=True)
for result in results:
if isinstance(result, Exception):
log.error(f"Error in async insert batch: {result}")
raise result
log.info(
f"Successfully async inserted {len(points)} vectors in batches into '{collection_name_with_prefix}'"
)
async def upsert_async(self, collection_name: str, items: List[VectorItem]) -> None:
"""Async version of upsert using asyncio and run_in_executor for improved performance."""
if not items:
log.warning("No items to upsert")
return
collection_name_with_prefix = self._get_collection_name_with_prefix(
collection_name
)
points = self._create_points(items, collection_name_with_prefix)
# Upsert in batches
for i in range(0, len(points), BATCH_SIZE):
batch = points[i : i + BATCH_SIZE]
try:
self.index.upsert(vectors=batch)
log.debug(
f"Upserted batch of {len(batch)} vectors into '{collection_name_with_prefix}'"
)
except Exception as e:
log.error(
f"Error upserting batch into '{collection_name_with_prefix}': {e}"
)
raise
# Create batches
batches = [
points[i : i + BATCH_SIZE] for i in range(0, len(points), BATCH_SIZE)
]
loop = asyncio.get_event_loop()
tasks = [
loop.run_in_executor(
None, functools.partial(self.index.upsert, vectors=batch)
)
for batch in batches
]
results = await asyncio.gather(*tasks, return_exceptions=True)
for result in results:
if isinstance(result, Exception):
log.error(f"Error in async upsert batch: {result}")
raise result
log.info(
f"Successfully upserted {len(items)} vectors into '{collection_name_with_prefix}'"
f"Successfully async upserted {len(points)} vectors in batches into '{collection_name_with_prefix}'"
)
def streaming_upsert(self, collection_name: str, items: List[VectorItem]) -> None:
"""Perform a streaming upsert over gRPC for performance testing."""
if not items:
log.warning("No items to upsert via streaming")
return
collection_name_with_prefix = self._get_collection_name_with_prefix(
collection_name
)
points = self._create_points(items, collection_name_with_prefix)
# Open a streaming upsert channel
stream = self.index.streaming_upsert()
try:
for point in points:
# send each point over the stream
stream.send(point)
# close the stream to finalize
stream.close()
log.info(
f"Successfully streamed upsert of {len(points)} vectors into '{collection_name_with_prefix}'"
)
except Exception as e:
log.error(f"Error during streaming upsert: {e}")
raise
def search(
self, collection_name: str, vectors: List[List[Union[float, int]]], limit: int
) -> Optional[SearchResult]:
@ -410,3 +514,20 @@ class PineconeClient(VectorDBBase):
except Exception as e:
log.error(f"Failed to reset Pinecone index: {e}")
raise
def close(self):
"""Shut down the gRPC channel and thread pool."""
try:
self.client.close()
log.info("Pinecone gRPC channel closed.")
except Exception as e:
log.warning(f"Failed to close Pinecone gRPC channel: {e}")
self._executor.shutdown(wait=True)
def __enter__(self):
"""Enter context manager."""
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""Exit context manager, ensuring resources are cleaned up."""
self.close()

View file

@ -71,23 +71,27 @@ from pydub import AudioSegment
from pydub.utils import mediainfo
def get_audio_format(file_path):
def get_audio_convert_format(file_path):
"""Check if the given file needs to be converted to a different format."""
if not os.path.isfile(file_path):
log.error(f"File not found: {file_path}")
return False
info = mediainfo(file_path)
if (
info.get("codec_name") == "aac"
and info.get("codec_type") == "audio"
and info.get("codec_tag_string") == "mp4a"
):
return "mp4"
elif info.get("format_name") == "ogg":
return "ogg"
elif info.get("format_name") == "matroska,webm":
return "webm"
try:
info = mediainfo(file_path)
if (
info.get("codec_name") == "aac"
and info.get("codec_type") == "audio"
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
return None
@ -538,14 +542,17 @@ def transcribe(request: Request, file_path):
log.debug(data)
return data
elif request.app.state.config.STT_ENGINE == "openai":
audio_format = get_audio_format(file_path)
if audio_format:
os.rename(file_path, file_path.replace(".wav", f".{audio_format}"))
convert_format = get_audio_convert_format(file_path)
if convert_format:
ext = convert_format.split(".")[-1]
os.rename(file_path, file_path.replace(".{ext}", f".{convert_format}"))
# Convert unsupported audio file to WAV format
convert_audio_to_wav(
file_path.replace(".wav", f".{audio_format}"),
file_path.replace(".{ext}", f".{convert_format}"),
file_path,
audio_format,
convert_format,
)
r = None

View file

@ -234,7 +234,7 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
],
)
if not search_success:
if not search_success or not connection_app.entries:
raise HTTPException(400, detail="User not found in the LDAP server")
entry = connection_app.entries[0]

View file

@ -133,6 +133,7 @@ def upload_file(
"audio/ogg",
"audio/x-m4a",
"audio/webm",
"video/webm",
)
):
file_path = Storage.get_file(file_path)
@ -150,7 +151,6 @@ def upload_file(
"video/mp4",
"video/ogg",
"video/quicktime",
"video/webm",
]:
process_file(request, ProcessFileForm(file_id=id), user=user)

View file

@ -3,6 +3,8 @@ import logging
import mimetypes
import os
import shutil
import asyncio
import uuid
from datetime import datetime
@ -135,7 +137,10 @@ def get_ef(
def get_rf(
engine: str = "",
reranking_model: Optional[str] = None,
external_reranker_url: str = "",
external_reranker_api_key: str = "",
auto_update: bool = False,
):
rf = None
@ -153,19 +158,33 @@ def get_rf(
log.error(f"ColBERT: {e}")
raise Exception(ERROR_MESSAGES.DEFAULT(e))
else:
import sentence_transformers
if engine == "external":
try:
from open_webui.retrieval.models.external import ExternalReranker
rf = ExternalReranker(
url=external_reranker_url,
api_key=external_reranker_api_key,
model=reranking_model,
)
except Exception as e:
log.error(f"ExternalReranking: {e}")
raise Exception(ERROR_MESSAGES.DEFAULT(e))
else:
import sentence_transformers
try:
rf = sentence_transformers.CrossEncoder(
get_model_path(reranking_model, auto_update),
device=DEVICE_TYPE,
trust_remote_code=RAG_RERANKING_MODEL_TRUST_REMOTE_CODE,
backend=SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND,
model_kwargs=SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS,
)
except Exception as e:
log.error(f"CrossEncoder: {e}")
raise Exception(ERROR_MESSAGES.DEFAULT("CrossEncoder error"))
try:
rf = sentence_transformers.CrossEncoder(
get_model_path(reranking_model, auto_update),
device=DEVICE_TYPE,
trust_remote_code=RAG_RERANKING_MODEL_TRUST_REMOTE_CODE,
backend=SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND,
model_kwargs=SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS,
)
except Exception as e:
log.error(f"CrossEncoder: {e}")
raise Exception(ERROR_MESSAGES.DEFAULT("CrossEncoder error"))
return rf
@ -188,7 +207,7 @@ class ProcessUrlForm(CollectionNameForm):
class SearchForm(BaseModel):
query: str
queries: List[str]
@router.get("/")
@ -223,14 +242,6 @@ async def get_embedding_config(request: Request, user=Depends(get_admin_user)):
}
@router.get("/reranking")
async def get_reraanking_config(request: Request, user=Depends(get_admin_user)):
return {
"status": True,
"reranking_model": request.app.state.config.RAG_RERANKING_MODEL,
}
class OpenAIConfigForm(BaseModel):
url: str
key: str
@ -325,41 +336,6 @@ async def update_embedding_config(
)
class RerankingModelUpdateForm(BaseModel):
reranking_model: str
@router.post("/reranking/update")
async def update_reranking_config(
request: Request, form_data: RerankingModelUpdateForm, user=Depends(get_admin_user)
):
log.info(
f"Updating reranking model: {request.app.state.config.RAG_RERANKING_MODEL} to {form_data.reranking_model}"
)
try:
request.app.state.config.RAG_RERANKING_MODEL = form_data.reranking_model
try:
request.app.state.rf = get_rf(
request.app.state.config.RAG_RERANKING_MODEL,
True,
)
except Exception as e:
log.error(f"Error loading reranking model: {e}")
request.app.state.config.ENABLE_RAG_HYBRID_SEARCH = False
return {
"status": True,
"reranking_model": request.app.state.config.RAG_RERANKING_MODEL,
}
except Exception as e:
log.exception(f"Problem updating reranking model: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=ERROR_MESSAGES.DEFAULT(e),
)
@router.get("/config")
async def get_rag_config(request: Request, user=Depends(get_admin_user)):
return {
@ -383,6 +359,11 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)):
"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,
# Reranking settings
"RAG_RERANKING_MODEL": request.app.state.config.RAG_RERANKING_MODEL,
"RAG_RERANKING_ENGINE": request.app.state.config.RAG_RERANKING_ENGINE,
"RAG_EXTERNAL_RERANKER_URL": request.app.state.config.RAG_EXTERNAL_RERANKER_URL,
"RAG_EXTERNAL_RERANKER_API_KEY": request.app.state.config.RAG_EXTERNAL_RERANKER_API_KEY,
# Chunking settings
"TEXT_SPLITTER": request.app.state.config.TEXT_SPLITTER,
"CHUNK_SIZE": request.app.state.config.CHUNK_SIZE,
@ -519,6 +500,12 @@ class ConfigForm(BaseModel):
DOCUMENT_INTELLIGENCE_KEY: Optional[str] = None
MISTRAL_OCR_API_KEY: Optional[str] = None
# Reranking settings
RAG_RERANKING_MODEL: Optional[str] = None
RAG_RERANKING_ENGINE: Optional[str] = None
RAG_EXTERNAL_RERANKER_URL: Optional[str] = None
RAG_EXTERNAL_RERANKER_API_KEY: Optional[str] = None
# Chunking settings
TEXT_SPLITTER: Optional[str] = None
CHUNK_SIZE: Optional[int] = None
@ -630,6 +617,49 @@ async def update_rag_config(
else request.app.state.config.MISTRAL_OCR_API_KEY
)
# Reranking settings
request.app.state.config.RAG_RERANKING_ENGINE = (
form_data.RAG_RERANKING_ENGINE
if form_data.RAG_RERANKING_ENGINE is not None
else request.app.state.config.RAG_RERANKING_ENGINE
)
request.app.state.config.RAG_EXTERNAL_RERANKER_URL = (
form_data.RAG_EXTERNAL_RERANKER_URL
if form_data.RAG_EXTERNAL_RERANKER_URL is not None
else request.app.state.config.RAG_EXTERNAL_RERANKER_URL
)
request.app.state.config.RAG_EXTERNAL_RERANKER_API_KEY = (
form_data.RAG_EXTERNAL_RERANKER_API_KEY
if form_data.RAG_EXTERNAL_RERANKER_API_KEY is not None
else request.app.state.config.RAG_EXTERNAL_RERANKER_API_KEY
)
log.info(
f"Updating reranking model: {request.app.state.config.RAG_RERANKING_MODEL} to {form_data.RAG_RERANKING_MODEL}"
)
try:
request.app.state.config.RAG_RERANKING_MODEL = form_data.RAG_RERANKING_MODEL
try:
request.app.state.rf = get_rf(
request.app.state.config.RAG_RERANKING_ENGINE,
request.app.state.config.RAG_RERANKING_MODEL,
request.app.state.config.RAG_EXTERNAL_RERANKER_URL,
request.app.state.config.RAG_EXTERNAL_RERANKER_API_KEY,
True,
)
except Exception as e:
log.error(f"Error loading reranking model: {e}")
request.app.state.config.ENABLE_RAG_HYBRID_SEARCH = False
except Exception as e:
log.exception(f"Problem updating reranking model: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=ERROR_MESSAGES.DEFAULT(e),
)
# Chunking settings
request.app.state.config.TEXT_SPLITTER = (
form_data.TEXT_SPLITTER
@ -786,6 +816,11 @@ async def update_rag_config(
"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,
# Reranking settings
"RAG_RERANKING_MODEL": request.app.state.config.RAG_RERANKING_MODEL,
"RAG_RERANKING_ENGINE": request.app.state.config.RAG_RERANKING_ENGINE,
"RAG_EXTERNAL_RERANKER_URL": request.app.state.config.RAG_EXTERNAL_RERANKER_URL,
"RAG_EXTERNAL_RERANKER_API_KEY": request.app.state.config.RAG_EXTERNAL_RERANKER_API_KEY,
# Chunking settings
"TEXT_SPLITTER": request.app.state.config.TEXT_SPLITTER,
"CHUNK_SIZE": request.app.state.config.CHUNK_SIZE,
@ -1568,16 +1603,34 @@ def search_web(request: Request, engine: str, query: str) -> list[SearchResult]:
async def process_web_search(
request: Request, form_data: SearchForm, user=Depends(get_verified_user)
):
urls = []
try:
logging.info(
f"trying to web search with {request.app.state.config.WEB_SEARCH_ENGINE, form_data.query}"
)
web_results = await run_in_threadpool(
search_web,
request,
request.app.state.config.WEB_SEARCH_ENGINE,
form_data.query,
f"trying to web search with {request.app.state.config.WEB_SEARCH_ENGINE, form_data.queries}"
)
search_tasks = [
run_in_threadpool(
search_web,
request,
request.app.state.config.WEB_SEARCH_ENGINE,
query,
)
for query in form_data.queries
]
search_results = await asyncio.gather(*search_tasks)
for result in search_results:
if result:
for item in result:
if item and item.link:
urls.append(item.link)
urls = list(dict.fromkeys(urls))
log.debug(f"urls: {urls}")
except Exception as e:
log.exception(e)
@ -1586,10 +1639,7 @@ async def process_web_search(
detail=ERROR_MESSAGES.WEB_SEARCH_ERROR(e),
)
log.debug(f"web_results: {web_results}")
try:
urls = [result.link for result in web_results]
loader = get_web_loader(
urls,
verify_ssl=request.app.state.config.ENABLE_WEB_LOADER_SSL_VERIFICATION,
@ -1599,7 +1649,7 @@ async def process_web_search(
docs = await loader.aload()
urls = [
doc.metadata.get("source") for doc in docs if doc.metadata.get("source")
] # only keep URLs
] # only keep the urls returned by the loader
if request.app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL:
return {
@ -1616,29 +1666,28 @@ async def process_web_search(
"loaded_count": len(docs),
}
else:
collection_names = []
for doc_idx, doc in enumerate(docs):
if doc and doc.page_content:
try:
collection_name = f"web-search-{calculate_sha256_string(form_data.query + '-' + urls[doc_idx])}"[
:63
]
# Create a single collection for all documents
collection_name = (
f"web-search-{calculate_sha256_string('-'.join(form_data.queries))}"[
:63
]
)
collection_names.append(collection_name)
await run_in_threadpool(
save_docs_to_vector_db,
request,
[doc],
collection_name,
overwrite=True,
user=user,
)
except Exception as e:
log.debug(f"error saving doc {doc_idx}: {e}")
try:
await run_in_threadpool(
save_docs_to_vector_db,
request,
docs,
collection_name,
overwrite=True,
user=user,
)
except Exception as e:
log.debug(f"error saving docs: {e}")
return {
"status": True,
"collection_names": collection_names,
"collection_names": [collection_name],
"filenames": urls,
"loaded_count": len(docs),
}

View file

@ -186,20 +186,9 @@ async def generate_title(
else:
template = DEFAULT_TITLE_GENERATION_PROMPT_TEMPLATE
messages = form_data["messages"]
# Remove reasoning details from the messages
for message in messages:
message["content"] = re.sub(
r"<details\s+type=\"reasoning\"[^>]*>.*?<\/details>",
"",
message["content"],
flags=re.S,
).strip()
content = title_generation_template(
template,
messages,
form_data["messages"],
{
"name": user.name,
"location": user.info.get("location") if user.info else None,

View file

@ -34,7 +34,7 @@ router = APIRouter()
############################
PAGE_ITEM_COUNT = 10
PAGE_ITEM_COUNT = 30
@router.get("/", response_model=UserListResponse)

View file

@ -159,18 +159,19 @@ def get_models_in_use():
@sio.on("usage")
async def usage(sid, data):
model_id = data["model"]
# Record the timestamp for the last update
current_time = int(time.time())
if sid in SESSION_POOL:
model_id = data["model"]
# Record the timestamp for the last update
current_time = int(time.time())
# Store the new usage data and task
USAGE_POOL[model_id] = {
**(USAGE_POOL[model_id] if model_id in USAGE_POOL else {}),
sid: {"updated_at": current_time},
}
# Store the new usage data and task
USAGE_POOL[model_id] = {
**(USAGE_POOL[model_id] if model_id in USAGE_POOL else {}),
sid: {"updated_at": current_time},
}
# Broadcast the usage data to all clients
await sio.emit("usage", {"models": get_models_in_use()})
# Broadcast the usage data to all clients
await sio.emit("usage", {"models": get_models_in_use()})
@sio.event
@ -192,9 +193,6 @@ async def connect(sid, environ, auth):
# print(f"user {user.name}({user.id}) connected with session ID {sid}")
await sio.emit("user-list", {"user_ids": list(USER_POOL.keys())})
await sio.emit("usage", {"models": get_models_in_use()})
return True
return False
@sio.on("user-join")
@ -281,7 +279,8 @@ async def channel_events(sid, data):
@sio.on("user-list")
async def user_list(sid):
await sio.emit("user-list", {"user_ids": list(USER_POOL.keys())})
if sid in SESSION_POOL:
await sio.emit("user-list", {"user_ids": list(USER_POOL.keys())})
@sio.event

View file

@ -17,6 +17,7 @@ from open_webui.config import (
S3_SECRET_ACCESS_KEY,
S3_USE_ACCELERATE_ENDPOINT,
S3_ADDRESSING_STYLE,
S3_ENABLE_TAGGING,
GCS_BUCKET_NAME,
GOOGLE_APPLICATION_CREDENTIALS_JSON,
AZURE_STORAGE_ENDPOINT,
@ -140,18 +141,19 @@ class S3StorageProvider(StorageProvider):
) -> Tuple[bytes, str]:
"""Handles uploading of the file to S3 storage."""
_, file_path = LocalStorageProvider.upload_file(file, filename, tags)
tagging = {"TagSet": [{"Key": k, "Value": v} for k, v in tags.items()]}
s3_key = os.path.join(self.key_prefix, filename)
try:
s3_key = os.path.join(self.key_prefix, filename)
self.s3_client.upload_file(file_path, self.bucket_name, s3_key)
self.s3_client.put_object_tagging(
Bucket=self.bucket_name,
Key=s3_key,
Tagging=tagging,
)
if S3_ENABLE_TAGGING and tags:
tagging = {"TagSet": [{"Key": k, "Value": v} for k, v in tags.items()]}
self.s3_client.put_object_tagging(
Bucket=self.bucket_name,
Key=s3_key,
Tagging=tagging,
)
return (
open(file_path, "rb").read(),
"s3://" + self.bucket_name + "/" + s3_key,
f"s3://{self.bucket_name}/{s3_key}",
)
except ClientError as e:
raise RuntimeError(f"Error uploading file to S3: {e}")

View file

@ -44,12 +44,14 @@ class JupyterCodeExecuter:
:param password: Jupyter password (optional)
:param timeout: WebSocket timeout in seconds (default: 60s)
"""
self.base_url = base_url.rstrip("/")
self.base_url = base_url
self.code = code
self.token = token
self.password = password
self.timeout = timeout
self.kernel_id = ""
if self.base_url[-1] != "/":
self.base_url += "/"
self.session = aiohttp.ClientSession(trust_env=True, base_url=self.base_url)
self.params = {}
self.result = ResultModel()
@ -61,7 +63,7 @@ class JupyterCodeExecuter:
if self.kernel_id:
try:
async with self.session.delete(
f"/api/kernels/{self.kernel_id}", params=self.params
f"api/kernels/{self.kernel_id}", params=self.params
) as response:
response.raise_for_status()
except Exception as err:
@ -81,7 +83,7 @@ class JupyterCodeExecuter:
async def sign_in(self) -> None:
# password authentication
if self.password and not self.token:
async with self.session.get("/login") as response:
async with self.session.get("login") as response:
response.raise_for_status()
xsrf_token = response.cookies["_xsrf"].value
if not xsrf_token:
@ -89,7 +91,7 @@ class JupyterCodeExecuter:
self.session.cookie_jar.update_cookies(response.cookies)
self.session.headers.update({"X-XSRFToken": xsrf_token})
async with self.session.post(
"/login",
"login",
data={"_xsrf": xsrf_token, "password": self.password},
allow_redirects=False,
) as response:
@ -101,17 +103,15 @@ class JupyterCodeExecuter:
self.params.update({"token": self.token})
async def init_kernel(self) -> None:
async with self.session.post(
url="/api/kernels", params=self.params
) as response:
async with self.session.post(url="api/kernels", params=self.params) as response:
response.raise_for_status()
kernel_data = await response.json()
self.kernel_id = kernel_data["id"]
def init_ws(self) -> (str, dict):
ws_base = self.base_url.replace("http", "ws")
ws_base = self.base_url.replace("http", "ws", 1)
ws_params = "?" + "&".join([f"{key}={val}" for key, val in self.params.items()])
websocket_url = f"{ws_base}/api/kernels/{self.kernel_id}/channels{ws_params if len(ws_params) > 1 else ''}"
websocket_url = f"{ws_base}api/kernels/{self.kernel_id}/channels{ws_params if len(ws_params) > 1 else ''}"
ws_headers = {}
if self.password and not self.token:
ws_headers = {

View file

@ -353,8 +353,6 @@ async def chat_web_search_handler(
)
return form_data
all_results = []
await event_emitter(
{
"type": "status",
@ -366,106 +364,75 @@ async def chat_web_search_handler(
}
)
gathered_results = await asyncio.gather(
*(
process_web_search(
request,
SearchForm(**{"query": searchQuery}),
user=user,
)
for searchQuery in queries
),
return_exceptions=True,
)
try:
results = await process_web_search(
request,
SearchForm(queries=queries),
user=user,
)
for searchQuery, results in zip(queries, gathered_results):
try:
if isinstance(results, Exception):
raise Exception(f"Error searching {searchQuery}: {str(results)}")
if results:
files = form_data.get("files", [])
if results:
all_results.append(results)
files = form_data.get("files", [])
if results.get("collection_names"):
for col_idx, collection_name in enumerate(
results.get("collection_names")
):
files.append(
{
"collection_name": collection_name,
"name": ", ".join(queries),
"type": "web_search",
"urls": results["filenames"],
}
)
elif results.get("docs"):
# Invoked when bypass embedding and retrieval is set to True
docs = results["docs"]
files.append(
{
"docs": docs,
"name": ", ".join(queries),
"type": "web_search",
"urls": results["filenames"],
}
)
if results.get("collection_names"):
for col_idx, collection_name in enumerate(
results.get("collection_names")
):
files.append(
{
"collection_name": collection_name,
"name": searchQuery,
"type": "web_search",
"urls": [results["filenames"][col_idx]],
}
)
elif results.get("docs"):
# Invoked when bypass embedding and retrieval is set to True
docs = results["docs"]
form_data["files"] = files
if len(docs) == len(results["filenames"]):
# the number of docs and filenames (urls) should be the same
for doc_idx, doc in enumerate(docs):
files.append(
{
"docs": [doc],
"name": searchQuery,
"type": "web_search",
"urls": [results["filenames"][doc_idx]],
}
)
else:
# edge case when the number of docs and filenames (urls) are not the same
# this should not happen, but if it does, we will just append the docs
files.append(
{
"docs": results.get("docs", []),
"name": searchQuery,
"type": "web_search",
"urls": results["filenames"],
}
)
form_data["files"] = files
except Exception as e:
log.exception(e)
await event_emitter(
{
"type": "status",
"data": {
"action": "web_search",
"description": 'Error searching "{{searchQuery}}"',
"query": searchQuery,
"description": "Searched {{count}} sites",
"urls": results["filenames"],
"done": True,
},
}
)
else:
await event_emitter(
{
"type": "status",
"data": {
"action": "web_search",
"description": "No search results found",
"done": True,
"error": True,
},
}
)
if all_results:
urls = []
for results in all_results:
if "filenames" in results:
urls.extend(results["filenames"])
except Exception as e:
log.exception(e)
await event_emitter(
{
"type": "status",
"data": {
"action": "web_search",
"description": "Searched {{count}} sites",
"urls": urls,
"done": True,
},
}
)
else:
await event_emitter(
{
"type": "status",
"data": {
"action": "web_search",
"description": "No search results found",
"description": "An error occurred while searching the web",
"queries": queries,
"done": True,
"error": True,
},
@ -672,6 +639,9 @@ def apply_params_to_form_data(form_data, model):
if "frequency_penalty" in params and params["frequency_penalty"] is not None:
form_data["frequency_penalty"] = params["frequency_penalty"]
if "presence_penalty" in params and params["presence_penalty"] is not None:
form_data["presence_penalty"] = params["presence_penalty"]
if "reasoning_effort" in params and params["reasoning_effort"] is not None:
form_data["reasoning_effort"] = params["reasoning_effort"]
@ -974,6 +944,20 @@ async def process_chat_response(
if message:
messages = get_message_list(message_map, message.get("id"))
# Remove reasoning details 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"] = []
if tasks and messages:
if TASKS.TITLE_GENERATION in tasks:
if tasks[TASKS.TITLE_GENERATION]:

View file

@ -34,6 +34,7 @@ from open_webui.config import (
OAUTH_ALLOWED_ROLES,
OAUTH_ADMIN_ROLES,
OAUTH_ALLOWED_DOMAINS,
OAUTH_UPDATE_PICTURE_ON_LOGIN,
WEBHOOK_URL,
JWT_EXPIRES_IN,
AppConfig,
@ -72,6 +73,7 @@ auth_manager_config.OAUTH_ADMIN_ROLES = OAUTH_ADMIN_ROLES
auth_manager_config.OAUTH_ALLOWED_DOMAINS = OAUTH_ALLOWED_DOMAINS
auth_manager_config.WEBHOOK_URL = WEBHOOK_URL
auth_manager_config.JWT_EXPIRES_IN = JWT_EXPIRES_IN
auth_manager_config.OAUTH_UPDATE_PICTURE_ON_LOGIN = OAUTH_UPDATE_PICTURE_ON_LOGIN
class OAuthManager:
@ -282,6 +284,49 @@ class OAuthManager:
id=group_model.id, form_data=update_form, overwrite=False
)
async def _process_picture_url(
self, picture_url: str, access_token: str = None
) -> str:
"""Process a picture URL and return a base64 encoded data URL.
Args:
picture_url: The URL of the picture to process
access_token: Optional OAuth access token for authenticated requests
Returns:
A data URL containing the base64 encoded picture, or "/user.png" if processing fails
"""
if not picture_url:
return "/user.png"
try:
get_kwargs = {}
if access_token:
get_kwargs["headers"] = {
"Authorization": f"Bearer {access_token}",
}
async with aiohttp.ClientSession() as session:
async with session.get(picture_url, **get_kwargs) as resp:
if resp.ok:
picture = await resp.read()
base64_encoded_picture = base64.b64encode(picture).decode(
"utf-8"
)
guessed_mime_type = mimetypes.guess_type(picture_url)[0]
if guessed_mime_type is None:
guessed_mime_type = "image/jpeg"
return (
f"data:{guessed_mime_type};base64,{base64_encoded_picture}"
)
else:
log.warning(
f"Failed to fetch profile picture from {picture_url}"
)
return "/user.png"
except Exception as e:
log.error(f"Error processing profile picture '{picture_url}': {e}")
return "/user.png"
async def handle_login(self, request, provider):
if provider not in OAUTH_PROVIDERS:
raise HTTPException(404)
@ -382,6 +427,22 @@ class OAuthManager:
if user.role != determined_role:
Users.update_user_role_by_id(user.id, determined_role)
# Update profile picture if enabled and different from current
if auth_manager_config.OAUTH_UPDATE_PICTURE_ON_LOGIN:
picture_claim = auth_manager_config.OAUTH_PICTURE_CLAIM
if picture_claim:
new_picture_url = user_data.get(
picture_claim, OAUTH_PROVIDERS[provider].get("picture_url", "")
)
processed_picture_url = await self._process_picture_url(
new_picture_url, token.get("access_token")
)
if processed_picture_url != user.profile_image_url:
Users.update_user_profile_image_url_by_id(
user.id, processed_picture_url
)
log.debug(f"Updated profile picture for user {user.email}")
if not user:
user_count = Users.get_num_users()
@ -397,40 +458,9 @@ class OAuthManager:
picture_url = user_data.get(
picture_claim, OAUTH_PROVIDERS[provider].get("picture_url", "")
)
if picture_url:
# Download the profile image into a base64 string
try:
access_token = token.get("access_token")
get_kwargs = {}
if access_token:
get_kwargs["headers"] = {
"Authorization": f"Bearer {access_token}",
}
async with aiohttp.ClientSession(trust_env=True) as session:
async with session.get(
picture_url, **get_kwargs
) as resp:
if resp.ok:
picture = await resp.read()
base64_encoded_picture = base64.b64encode(
picture
).decode("utf-8")
guessed_mime_type = mimetypes.guess_type(
picture_url
)[0]
if guessed_mime_type is None:
# assume JPG, browsers are tolerant enough of image formats
guessed_mime_type = "image/jpeg"
picture_url = f"data:{guessed_mime_type};base64,{base64_encoded_picture}"
else:
picture_url = "/user.png"
except Exception as e:
log.error(
f"Error downloading profile image '{picture_url}': {e}"
)
picture_url = "/user.png"
if not picture_url:
picture_url = "/user.png"
picture_url = await self._process_picture_url(
picture_url, token.get("access_token")
)
else:
picture_url = "/user.png"

View file

@ -59,6 +59,7 @@ def apply_model_params_to_body_openai(params: dict, form_data: dict) -> dict:
"top_p": float,
"max_tokens": int,
"frequency_penalty": float,
"presence_penalty": float,
"reasoning_effort": str,
"seed": lambda x: x,
"stop": lambda x: [bytes(s, "utf-8").decode("unicode_escape") for s in x],

View file

@ -65,4 +65,6 @@ if [ -n "$SPACE_ID" ]; then
export WEBUI_URL=${SPACE_HOST}
fi
WEBUI_SECRET_KEY="$WEBUI_SECRET_KEY" exec uvicorn open_webui.main:app --host "$HOST" --port "$PORT" --forwarded-allow-ips '*' --workers "${UVICORN_WORKERS:-1}"
PYTHON_CMD=$(command -v python3 || command -v python)
WEBUI_SECRET_KEY="$WEBUI_SECRET_KEY" exec "$PYTHON_CMD" -m uvicorn open_webui.main:app --host "$HOST" --port "$PORT" --forwarded-allow-ips '*' --workers "${UVICORN_WORKERS:-1}"

74
contribution_stats.py Normal file
View file

@ -0,0 +1,74 @@
import os
import subprocess
from collections import Counter
CONFIG_FILE_EXTENSIONS = (".json", ".yml", ".yaml", ".ini", ".conf", ".toml")
def is_text_file(filepath):
# Check for binary file by scanning for null bytes.
try:
with open(filepath, "rb") as f:
chunk = f.read(4096)
if b"\0" in chunk:
return False
return True
except Exception:
return False
def should_skip_file(path):
base = os.path.basename(path)
# Skip dotfiles and dotdirs
if base.startswith("."):
return True
# Skip config files by extension
if base.lower().endswith(CONFIG_FILE_EXTENSIONS):
return True
return False
def get_tracked_files():
try:
output = subprocess.check_output(["git", "ls-files"], text=True)
files = output.strip().split("\n")
files = [f for f in files if f and os.path.isfile(f)]
return files
except subprocess.CalledProcessError:
print("Error: Are you in a git repository?")
return []
def main():
files = get_tracked_files()
email_counter = Counter()
total_lines = 0
for file in files:
if should_skip_file(file):
continue
if not is_text_file(file):
continue
try:
blame = subprocess.check_output(
["git", "blame", "-e", file], text=True, errors="replace"
)
for line in blame.splitlines():
# The email always inside <>
if "<" in line and ">" in line:
try:
email = line.split("<")[1].split(">")[0].strip()
except Exception:
continue
email_counter[email] += 1
total_lines += 1
except subprocess.CalledProcessError:
continue
for email, lines in email_counter.most_common():
percent = (lines / total_lines * 100) if total_lines else 0
print(f"{email}: {lines}/{total_lines} {percent:.2f}%")
if __name__ == "__main__":
main()

12
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "open-webui",
"version": "0.6.7",
"version": "0.6.8",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "open-webui",
"version": "0.6.7",
"version": "0.6.8",
"dependencies": {
"@azure/msal-browser": "^4.5.0",
"@codemirror/lang-javascript": "^6.2.2",
@ -36,6 +36,7 @@
"dompurify": "^3.2.5",
"eventsource-parser": "^1.1.2",
"file-saver": "^2.0.5",
"focus-trap": "^7.6.4",
"fuse.js": "^7.0.0",
"highlight.js": "^11.9.0",
"html-entities": "^2.5.3",
@ -6801,9 +6802,10 @@
"dev": true
},
"node_modules/focus-trap": {
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.5.4.tgz",
"integrity": "sha512-N7kHdlgsO/v+iD/dMoJKtsSqs5Dz/dXZVebRgJw23LDk+jMi/974zyiOYDziY2JPp8xivq9BmUGwIJMiuSBi7w==",
"version": "7.6.4",
"resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.4.tgz",
"integrity": "sha512-xx560wGBk7seZ6y933idtjJQc1l+ck+pI3sKvhKozdBV1dRZoKhkW5xoCaFv9tQiX5RH1xfSxjuNu6g+lmN/gw==",
"license": "MIT",
"dependencies": {
"tabbable": "^6.2.0"
}

View file

@ -1,6 +1,6 @@
{
"name": "open-webui",
"version": "0.6.7",
"version": "0.6.8",
"private": true,
"scripts": {
"dev": "npm run pyodide:fetch && vite dev --host",
@ -79,6 +79,7 @@
"dompurify": "^3.2.5",
"eventsource-parser": "^1.1.2",
"file-saver": "^2.0.5",
"focus-trap": "^7.6.4",
"fuse.js": "^7.0.0",
"highlight.js": "^11.9.0",
"html-entities": "^2.5.3",

View file

@ -24,6 +24,12 @@
font-display: swap;
}
@font-face {
font-family: 'Vazirmatn';
src: url('/assets/fonts/Vazirmatn-Variable.ttf');
font-display: swap;
}
html {
word-break: break-word;
}

View file

@ -43,6 +43,7 @@
fill="currentColor"
class="w-5 h-5"
>
<p class="sr-only">{$i18n.t('Close')}</p>
<path
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
/>

View file

@ -87,6 +87,7 @@
<div class="flex justify-center mt-8">
<div class="flex flex-col justify-center items-center">
<button
aria-labelledby="get-started"
class="relative z-20 flex p-1 rounded-full bg-white/5 hover:bg-white/10 transition font-medium text-sm"
on:click={() => {
getStartedHandler();
@ -94,12 +95,12 @@
>
<ArrowRightCircle className="size-6" />
</button>
<div class="mt-1.5 font-primary text-base font-medium">{$i18n.t(`Get started`)}</div>
<div id="get-started" class="mt-1.5 font-primary text-base font-medium">
{$i18n.t(`Get started`)}
</div>
</div>
</div>
</div>
<!-- <div class="absolute bottom-12 left-0 right-0 w-full"></div> -->
</div>
</div>
{/if}

View file

@ -123,35 +123,6 @@
}
};
const rerankingModelUpdateHandler = async () => {
console.log('Update reranking model attempt:', rerankingModel);
updateRerankingModelLoading = true;
const res = await updateRerankingConfig(localStorage.token, {
reranking_model: rerankingModel
}).catch(async (error) => {
toast.error(`${error}`);
await setRerankingConfig();
return null;
});
updateRerankingModelLoading = false;
if (res) {
console.log('rerankingModelUpdateHandler:', res);
if (res.status === true) {
if (rerankingModel === '') {
toast.success($i18n.t('Reranking model disabled', res), {
duration: 1000 * 10
});
} else {
toast.success($i18n.t('Reranking model set to "{{reranking_model}}"', res), {
duration: 1000 * 10
});
}
}
}
};
const submitHandler = async () => {
if (RAGConfig.CONTENT_EXTRACTION_ENGINE === 'tika' && RAGConfig.TIKA_SERVER_URL === '') {
toast.error($i18n.t('Tika Server URL required.'));
@ -190,10 +161,6 @@
if (!RAGConfig.BYPASS_EMBEDDING_AND_RETRIEVAL) {
await embeddingModelUpdateHandler();
if (RAGConfig.ENABLE_RAG_HYBRID_SEARCH) {
await rerankingModelUpdateHandler();
}
}
const res = await updateRAGConfig(localStorage.token, RAGConfig);
@ -215,18 +182,8 @@
OllamaUrl = embeddingConfig.ollama_config.url;
}
};
const setRerankingConfig = async () => {
const rerankingConfig = await getRerankingConfig(localStorage.token);
if (rerankingConfig) {
rerankingModel = rerankingConfig.reranking_model;
}
};
onMount(async () => {
await setEmbeddingConfig();
await setRerankingConfig();
RAGConfig = await getRAGConfig(localStorage.token);
});
@ -655,6 +612,48 @@
</div>
{#if RAGConfig.ENABLE_RAG_HYBRID_SEARCH === true}
<div class=" mb-2.5 flex flex-col w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">
{$i18n.t('Reranking Engine')}
</div>
<div class="flex items-center relative">
<select
class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right"
bind:value={RAGConfig.RAG_RERANKING_ENGINE}
placeholder="Select a reranking model engine"
on:change={(e) => {
if (e.target.value === 'external') {
RAGConfig.RAG_RERANKING_MODEL = '';
} else if (e.target.value === '') {
RAGConfig.RAG_RERANKING_MODEL = 'BAAI/bge-reranker-v2-m3';
}
}}
>
<option value="">{$i18n.t('Default (SentenceTransformers)')}</option>
<option value="external">{$i18n.t('External')}</option>
</select>
</div>
</div>
{#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"
placeholder={$i18n.t('API Base URL')}
bind:value={RAGConfig.RAG_EXTERNAL_RERANKER_URL}
required
/>
<SensitiveInput
placeholder={$i18n.t('API Key')}
bind:value={RAGConfig.RAG_EXTERNAL_RERANKER_API_KEY}
required={false}
/>
</div>
{/if}
</div>
<div class=" mb-2.5 flex flex-col w-full">
<div class=" mb-1 text-xs font-medium">{$i18n.t('Reranking Model')}</div>
@ -666,62 +665,9 @@
placeholder={$i18n.t('Set reranking model (e.g. {{model}})', {
model: 'BAAI/bge-reranker-v2-m3'
})}
bind:value={rerankingModel}
bind:value={RAGConfig.RAG_RERANKING_MODEL}
/>
</div>
<button
class="px-2.5 bg-transparent text-gray-800 dark:bg-transparent dark:text-gray-100 rounded-lg transition"
on:click={() => {
rerankingModelUpdateHandler();
}}
disabled={updateRerankingModelLoading}
>
{#if updateRerankingModelLoading}
<div class="self-center">
<svg
class=" w-4 h-4"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<style>
.spinner_ajPY {
transform-origin: center;
animation: spinner_AtaB 0.75s infinite linear;
}
@keyframes spinner_AtaB {
100% {
transform: rotate(360deg);
}
}
</style>
<path
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
opacity=".25"
/>
<path
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
class="spinner_ajPY"
/>
</svg>
</div>
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
d="M8.75 2.75a.75.75 0 0 0-1.5 0v5.69L5.03 6.22a.75.75 0 0 0-1.06 1.06l3.5 3.5a.75.75 0 0 0 1.06 0l3.5-3.5a.75.75 0 0 0-1.06-1.06L8.75 8.44V2.75Z"
/>
<path
d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
/>
</svg>
{/if}
</button>
</div>
</div>
</div>

View file

@ -32,13 +32,14 @@
import About from '$lib/components/chat/Settings/About.svelte';
import Banner from '$lib/components/common/Banner.svelte';
import Markdown from '$lib/components/chat/Messages/Markdown.svelte';
import Spinner from '$lib/components/common/Spinner.svelte';
const i18n = getContext('i18n');
let page = 1;
let users = [];
let total = 0;
let users = null;
let total = null;
let query = '';
let orderBy = 'created_at'; // default sort key
@ -181,314 +182,293 @@
</div>
{/if}
<div class="mt-0.5 mb-2 gap-1 flex flex-col md:flex-row justify-between">
<div class="flex md:self-center text-lg font-medium px-0.5">
<div class="flex-shrink-0">
{$i18n.t('Users')}
</div>
<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-50 dark:bg-gray-850" />
{#if users === null || total === null}
<div class="my-10">
<Spinner />
</div>
{:else}
<div class="mt-0.5 mb-2 gap-1 flex flex-col md:flex-row justify-between">
<div class="flex md:self-center text-lg font-medium px-0.5">
<div class="flex-shrink-0">
{$i18n.t('Users')}
</div>
<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-50 dark:bg-gray-850" />
{#if ($config?.license_metadata?.seats ?? null) !== null}
{#if total > $config?.license_metadata?.seats}
<span class="text-lg font-medium text-red-500"
>{total} of {$config?.license_metadata?.seats}
<span class="text-sm font-normal">available users</span></span
>
{#if ($config?.license_metadata?.seats ?? null) !== null}
{#if total > $config?.license_metadata?.seats}
<span class="text-lg font-medium text-red-500"
>{total} of {$config?.license_metadata?.seats}
<span class="text-sm font-normal">available users</span></span
>
{:else}
<span class="text-lg font-medium text-gray-500 dark:text-gray-300"
>{total} of {$config?.license_metadata?.seats}
<span class="text-sm font-normal">available users</span></span
>
{/if}
{:else}
<span class="text-lg font-medium text-gray-500 dark:text-gray-300"
>{total} of {$config?.license_metadata?.seats}
<span class="text-sm font-normal">available users</span></span
>
<span class="text-lg font-medium text-gray-500 dark:text-gray-300">{total}</span>
{/if}
{:else}
<span class="text-lg font-medium text-gray-500 dark:text-gray-300">{total}</span>
{/if}
</div>
<div class="flex gap-1">
<div class=" flex w-full space-x-2">
<div class="flex flex-1">
<div class=" self-center ml-1 mr-3">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z"
clip-rule="evenodd"
/>
</svg>
</div>
<input
class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-hidden bg-transparent"
bind:value={query}
placeholder={$i18n.t('Search')}
/>
</div>
<div>
<Tooltip content={$i18n.t('Add User')}>
<button
class=" p-2 rounded-xl hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 transition font-medium text-sm flex items-center space-x-1"
on:click={() => {
showAddUserModal = !showAddUserModal;
}}
>
<Plus className="size-3.5" />
</button>
</Tooltip>
</div>
</div>
</div>
</div>
<div
class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full rounded-sm pt-0.5"
>
<table
class="w-full text-sm text-left text-gray-500 dark:text-gray-400 table-auto max-w-full rounded-sm"
>
<thead
class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-850 dark:text-gray-400 -translate-y-0.5"
>
<tr class="">
<th
scope="col"
class="px-3 py-1.5 cursor-pointer select-none"
on:click={() => setSortKey('role')}
>
<div class="flex gap-1.5 items-center">
{$i18n.t('Role')}
{#if orderBy === 'role'}
<span class="font-normal"
>{#if direction === 'asc'}
<ChevronUp className="size-2" />
{:else}
<ChevronDown className="size-2" />
{/if}
</span>
{:else}
<span class="invisible">
<ChevronUp className="size-2" />
</span>
{/if}
<div class="flex gap-1">
<div class=" flex w-full space-x-2">
<div class="flex flex-1">
<div class=" self-center ml-1 mr-3">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z"
clip-rule="evenodd"
/>
</svg>
</div>
</th>
<th
scope="col"
class="px-3 py-1.5 cursor-pointer select-none"
on:click={() => setSortKey('name')}
>
<div class="flex gap-1.5 items-center">
{$i18n.t('Name')}
<input
class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-hidden bg-transparent"
bind:value={query}
placeholder={$i18n.t('Search')}
/>
</div>
{#if orderBy === 'name'}
<span class="font-normal"
>{#if direction === 'asc'}
<ChevronUp className="size-2" />
{:else}
<ChevronDown className="size-2" />
{/if}
</span>
{:else}
<span class="invisible">
<ChevronUp className="size-2" />
</span>
{/if}
</div>
</th>
<th
scope="col"
class="px-3 py-1.5 cursor-pointer select-none"
on:click={() => setSortKey('email')}
>
<div class="flex gap-1.5 items-center">
{$i18n.t('Email')}
{#if orderBy === 'email'}
<span class="font-normal"
>{#if direction === 'asc'}
<ChevronUp className="size-2" />
{:else}
<ChevronDown className="size-2" />
{/if}
</span>
{:else}
<span class="invisible">
<ChevronUp className="size-2" />
</span>
{/if}
</div>
</th>
<th
scope="col"
class="px-3 py-1.5 cursor-pointer select-none"
on:click={() => setSortKey('last_active_at')}
>
<div class="flex gap-1.5 items-center">
{$i18n.t('Last Active')}
{#if orderBy === 'last_active_at'}
<span class="font-normal"
>{#if direction === 'asc'}
<ChevronUp className="size-2" />
{:else}
<ChevronDown className="size-2" />
{/if}
</span>
{:else}
<span class="invisible">
<ChevronUp className="size-2" />
</span>
{/if}
</div>
</th>
<th
scope="col"
class="px-3 py-1.5 cursor-pointer select-none"
on:click={() => setSortKey('created_at')}
>
<div class="flex gap-1.5 items-center">
{$i18n.t('Created at')}
{#if orderBy === 'created_at'}
<span class="font-normal"
>{#if direction === 'asc'}
<ChevronUp className="size-2" />
{:else}
<ChevronDown className="size-2" />
{/if}
</span>
{:else}
<span class="invisible">
<ChevronUp className="size-2" />
</span>
{/if}
</div>
</th>
<th
scope="col"
class="px-3 py-1.5 cursor-pointer select-none"
on:click={() => setSortKey('oauth_sub')}
>
<div class="flex gap-1.5 items-center">
{$i18n.t('OAuth ID')}
{#if orderBy === 'oauth_sub'}
<span class="font-normal"
>{#if direction === 'asc'}
<ChevronUp className="size-2" />
{:else}
<ChevronDown className="size-2" />
{/if}
</span>
{:else}
<span class="invisible">
<ChevronUp className="size-2" />
</span>
{/if}
</div>
</th>
<th scope="col" class="px-3 py-2 text-right" />
</tr>
</thead>
<tbody class="">
{#each users as user, userIdx}
<tr class="bg-white dark:bg-gray-900 dark:border-gray-850 text-xs">
<td class="px-3 py-1 min-w-[7rem] w-28">
<div>
<Tooltip content={$i18n.t('Add User')}>
<button
class=" translate-y-0.5"
class=" p-2 rounded-xl hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 transition font-medium text-sm flex items-center space-x-1"
on:click={() => {
selectedUser = user;
showUpdateRoleModal = true;
showAddUserModal = !showAddUserModal;
}}
>
<Badge
type={user.role === 'admin' ? 'info' : user.role === 'user' ? 'success' : 'muted'}
content={$i18n.t(user.role)}
/>
<Plus className="size-3.5" />
</button>
</td>
<td class="px-3 py-1 font-medium text-gray-900 dark:text-white w-max">
<div class="flex flex-row w-max">
<img
class=" rounded-full w-6 h-6 object-cover mr-2.5"
src={user.profile_image_url.startsWith(WEBUI_BASE_URL) ||
user.profile_image_url.startsWith('https://www.gravatar.com/avatar/') ||
user.profile_image_url.startsWith('data:')
? user.profile_image_url
: `/user.png`}
alt="user"
/>
</Tooltip>
</div>
</div>
</div>
</div>
<div class=" font-medium self-center">{user.name}</div>
</div>
</td>
<td class=" px-3 py-1"> {user.email} </td>
<div
class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full rounded-sm pt-0.5"
>
<table
class="w-full text-sm text-left text-gray-500 dark:text-gray-400 table-auto max-w-full rounded-sm"
>
<thead
class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-850 dark:text-gray-400 -translate-y-0.5"
>
<tr class="">
<th
scope="col"
class="px-3 py-1.5 cursor-pointer select-none"
on:click={() => setSortKey('role')}
>
<div class="flex gap-1.5 items-center">
{$i18n.t('Role')}
<td class=" px-3 py-1">
{dayjs(user.last_active_at * 1000).fromNow()}
</td>
<td class=" px-3 py-1">
{dayjs(user.created_at * 1000).format('LL')}
</td>
<td class=" px-3 py-1"> {user.oauth_sub ?? ''} </td>
<td class="px-3 py-1 text-right">
<div class="flex justify-end w-full">
{#if $config.features.enable_admin_chat_access && user.role !== 'admin'}
<Tooltip content={$i18n.t('Chats')}>
<button
class="self-center w-fit text-sm px-2 py-2 hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
on:click={async () => {
showUserChatsModal = !showUserChatsModal;
selectedUser = user;
}}
>
<ChatBubbles />
</button>
</Tooltip>
{#if orderBy === 'role'}
<span class="font-normal"
>{#if direction === 'asc'}
<ChevronUp className="size-2" />
{:else}
<ChevronDown className="size-2" />
{/if}
</span>
{:else}
<span class="invisible">
<ChevronUp className="size-2" />
</span>
{/if}
</div>
</th>
<th
scope="col"
class="px-3 py-1.5 cursor-pointer select-none"
on:click={() => setSortKey('name')}
>
<div class="flex gap-1.5 items-center">
{$i18n.t('Name')}
<Tooltip content={$i18n.t('Edit User')}>
<button
class="self-center w-fit text-sm px-2 py-2 hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
on:click={async () => {
showEditUserModal = !showEditUserModal;
selectedUser = user;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125"
/>
</svg>
</button>
</Tooltip>
{#if orderBy === 'name'}
<span class="font-normal"
>{#if direction === 'asc'}
<ChevronUp className="size-2" />
{:else}
<ChevronDown className="size-2" />
{/if}
</span>
{:else}
<span class="invisible">
<ChevronUp className="size-2" />
</span>
{/if}
</div>
</th>
<th
scope="col"
class="px-3 py-1.5 cursor-pointer select-none"
on:click={() => setSortKey('email')}
>
<div class="flex gap-1.5 items-center">
{$i18n.t('Email')}
{#if user.role !== 'admin'}
<Tooltip content={$i18n.t('Delete User')}>
{#if orderBy === 'email'}
<span class="font-normal"
>{#if direction === 'asc'}
<ChevronUp className="size-2" />
{:else}
<ChevronDown className="size-2" />
{/if}
</span>
{:else}
<span class="invisible">
<ChevronUp className="size-2" />
</span>
{/if}
</div>
</th>
<th
scope="col"
class="px-3 py-1.5 cursor-pointer select-none"
on:click={() => setSortKey('last_active_at')}
>
<div class="flex gap-1.5 items-center">
{$i18n.t('Last Active')}
{#if orderBy === 'last_active_at'}
<span class="font-normal"
>{#if direction === 'asc'}
<ChevronUp className="size-2" />
{:else}
<ChevronDown className="size-2" />
{/if}
</span>
{:else}
<span class="invisible">
<ChevronUp className="size-2" />
</span>
{/if}
</div>
</th>
<th
scope="col"
class="px-3 py-1.5 cursor-pointer select-none"
on:click={() => setSortKey('created_at')}
>
<div class="flex gap-1.5 items-center">
{$i18n.t('Created at')}
{#if orderBy === 'created_at'}
<span class="font-normal"
>{#if direction === 'asc'}
<ChevronUp className="size-2" />
{:else}
<ChevronDown className="size-2" />
{/if}
</span>
{:else}
<span class="invisible">
<ChevronUp className="size-2" />
</span>
{/if}
</div>
</th>
<th
scope="col"
class="px-3 py-1.5 cursor-pointer select-none"
on:click={() => setSortKey('oauth_sub')}
>
<div class="flex gap-1.5 items-center">
{$i18n.t('OAuth ID')}
{#if orderBy === 'oauth_sub'}
<span class="font-normal"
>{#if direction === 'asc'}
<ChevronUp className="size-2" />
{:else}
<ChevronDown className="size-2" />
{/if}
</span>
{:else}
<span class="invisible">
<ChevronUp className="size-2" />
</span>
{/if}
</div>
</th>
<th scope="col" class="px-3 py-2 text-right" />
</tr>
</thead>
<tbody class="">
{#each users as user, userIdx}
<tr class="bg-white dark:bg-gray-900 dark:border-gray-850 text-xs">
<td class="px-3 py-1 min-w-[7rem] w-28">
<button
class=" translate-y-0.5"
on:click={() => {
selectedUser = user;
showUpdateRoleModal = true;
}}
>
<Badge
type={user.role === 'admin' ? 'info' : user.role === 'user' ? 'success' : 'muted'}
content={$i18n.t(user.role)}
/>
</button>
</td>
<td class="px-3 py-1 font-medium text-gray-900 dark:text-white w-max">
<div class="flex flex-row w-max">
<img
class=" rounded-full w-6 h-6 object-cover mr-2.5"
src={user.profile_image_url.startsWith(WEBUI_BASE_URL) ||
user.profile_image_url.startsWith('https://www.gravatar.com/avatar/') ||
user.profile_image_url.startsWith('data:')
? user.profile_image_url
: `/user.png`}
alt="user"
/>
<div class=" font-medium self-center">{user.name}</div>
</div>
</td>
<td class=" px-3 py-1"> {user.email} </td>
<td class=" px-3 py-1">
{dayjs(user.last_active_at * 1000).fromNow()}
</td>
<td class=" px-3 py-1">
{dayjs(user.created_at * 1000).format('LL')}
</td>
<td class=" px-3 py-1"> {user.oauth_sub ?? ''} </td>
<td class="px-3 py-1 text-right">
<div class="flex justify-end w-full">
{#if $config.features.enable_admin_chat_access && user.role !== 'admin'}
<Tooltip content={$i18n.t('Chats')}>
<button
class="self-center w-fit text-sm px-2 py-2 hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
on:click={async () => {
showUserChatsModal = !showUserChatsModal;
selectedUser = user;
}}
>
<ChatBubbles />
</button>
</Tooltip>
{/if}
<Tooltip content={$i18n.t('Edit User')}>
<button
class="self-center w-fit text-sm px-2 py-2 hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
on:click={async () => {
showDeleteConfirmDialog = true;
showEditUserModal = !showEditUserModal;
selectedUser = user;
}}
>
@ -503,25 +483,52 @@
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125"
/>
</svg>
</button>
</Tooltip>
{/if}
</div>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
<div class=" text-gray-500 text-xs mt-1.5 text-right">
{$i18n.t("Click on the user role button to change a user's role.")}
</div>
{#if user.role !== 'admin'}
<Tooltip content={$i18n.t('Delete User')}>
<button
class="self-center w-fit text-sm px-2 py-2 hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
on:click={async () => {
showDeleteConfirmDialog = true;
selectedUser = user;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
/>
</svg>
</button>
</Tooltip>
{/if}
</div>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
<Pagination bind:page count={total} perPage={10} />
<div class=" text-gray-500 text-xs mt-1.5 text-right">
{$i18n.t("Click on the user role button to change a user's role.")}
</div>
<Pagination bind:page count={total} perPage={30} />
{/if}
{#if !$config?.license_metadata}
{#if total > 50}

View file

@ -440,8 +440,10 @@
}
}
loading = false;
await tick();
if (!chatIdProp) {
loading = false;
await tick();
}
showControls.subscribe(async (value) => {
if (controlPane && !$mobile) {

View file

@ -86,7 +86,7 @@
$: onChange({
prompt,
files,
files: files.filter((file) => file.type !== 'image'),
selectedToolIds,
imageGenerationEnabled,
webSearchEnabled,
@ -604,7 +604,7 @@
<div class="px-2.5">
{#if $settings?.richTextInput ?? true}
<div
class="scrollbar-hidden text-left bg-transparent dark:text-gray-100 outline-hidden w-full pt-3 px-1 resize-none h-fit max-h-80 overflow-auto"
class="scrollbar-hidden rtl:text-right ltr:text-left bg-transparent dark:text-gray-100 outline-hidden w-full pt-3 px-1 resize-none h-fit max-h-80 overflow-auto"
id="chat-input-container"
>
<RichTextInput

View file

@ -33,6 +33,7 @@
let tools = {};
let show = false;
let showAllTools = false;
$: if (show) {
init();
@ -102,7 +103,7 @@
transition={flyAndScale}
>
{#if Object.keys(tools).length > 0}
<div class=" max-h-28 overflow-y-auto scrollbar-hidden">
<div class="{showAllTools ? '' : 'max-h-28'} overflow-y-auto scrollbar-thin">
{#each Object.keys(tools) as toolId}
<button
class="flex w-full justify-between gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer rounded-xl"
@ -141,7 +142,29 @@
</button>
{/each}
</div>
{#if Object.keys(tools).length > 3}
<button
class="flex w-full justify-center items-center text-sm font-medium cursor-pointer rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800"
on:click={() => {
showAllTools = !showAllTools;
}}
title={showAllTools ? $i18n.t('Show Less') : $i18n.t('Show All')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2.5"
stroke="currentColor"
class="size-3 transition-transform duration-200 {showAllTools
? 'rotate-180'
: ''} text-gray-300 dark:text-gray-600"
>
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5"
></path>
</svg>
</button>
{/if}
<hr class="border-black/5 dark:border-white/5 my-1" />
{/if}

View file

@ -32,6 +32,7 @@
title="Video player"
frameborder="0"
referrerpolicy="strict-origin-when-cross-origin"
controls
allowfullscreen
></video>
{:else}

View file

@ -54,6 +54,9 @@
height: ''
};
// chat export
let stylizedPdfExport = true;
// Admin - Show Update Available Toast
let showUpdateToast = true;
let showChangelog = true;
@ -152,6 +155,11 @@
saveSettings({ hapticFeedback: hapticFeedback });
};
const toggleStylizedPdfExport = async () => {
stylizedPdfExport = !stylizedPdfExport;
saveSettings({ stylizedPdfExport: stylizedPdfExport });
};
const toggleUserLocation = async () => {
userLocation = !userLocation;
@ -302,6 +310,11 @@
notificationSound = $settings?.notificationSound ?? true;
notificationSoundAlways = $settings?.notificationSoundAlways ?? false;
iframeSandboxAllowSameOrigin = $settings?.iframeSandboxAllowSameOrigin ?? false;
iframeSandboxAllowForms = $settings?.iframeSandboxAllowForms ?? false;
stylizedPdfExport = $settings?.stylizedPdfExport ?? true;
hapticFeedback = $settings.hapticFeedback ?? false;
ctrlEnterToSend = $settings.ctrlEnterToSend ?? false;
@ -964,6 +977,28 @@
</div>
</div>
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs">
{$i18n.t('Stylized PDF Export')}
</div>
<button
class="p-1 px-3 text-xs flex rounded-sm transition"
on:click={() => {
toggleStylizedPdfExport();
}}
type="button"
>
{#if stylizedPdfExport === 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 class=" my-1.5 text-sm font-medium">{$i18n.t('Voice')}</div>
<div>

View file

@ -3,7 +3,7 @@
import { fade } from 'svelte/transition';
import { flyAndScale } from '$lib/utils/transitions';
import * as FocusTrap from 'focus-trap';
export let show = true;
export let size = 'md';
export let containerClassName = 'p-3';
@ -11,6 +11,10 @@
let modalElement = null;
let mounted = false;
// Create focus trap to trap user tabs inside modal
// https://www.w3.org/WAI/WCAG21/Understanding/focus-order.html
// https://www.w3.org/WAI/WCAG21/Understanding/keyboard.html
let focusTrap: FocusTrap.FocusTrap | null = null;
const sizeToWidth = (size) => {
if (size === 'full') {
@ -45,9 +49,12 @@
$: if (show && modalElement) {
document.body.appendChild(modalElement);
focusTrap = FocusTrap.createFocusTrap(modalElement);
focusTrap.activate();
window.addEventListener('keydown', handleKeyDown);
document.body.style.overflow = 'hidden';
} else if (modalElement) {
focusTrap.deactivate();
window.removeEventListener('keydown', handleKeyDown);
document.body.removeChild(modalElement);
document.body.style.overflow = 'unset';
@ -55,6 +62,9 @@
onDestroy(() => {
show = false;
if (focusTrap) {
focusTrap.deactivate();
}
if (modalElement) {
document.body.removeChild(modalElement);
}
@ -66,6 +76,8 @@
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
bind:this={modalElement}
aria-modal="true"
role="dialog"
class="modal fixed top-0 right-0 left-0 bottom-0 bg-black/60 w-full h-screen max-h-[100dvh] {containerClassName} flex justify-center z-9999 overflow-y-auto overscroll-contain"
in:fade={{ duration: 10 }}
on:mousedown={() => {

View file

@ -19,7 +19,8 @@
mobile,
temporaryChatEnabled,
theme,
user
user,
settings
} from '$lib/stores';
import { flyAndScale } from '$lib/utils/transitions';
@ -63,75 +64,124 @@
};
const downloadPdf = async () => {
const containerElement = document.getElementById('messages-container');
if ($settings?.stylizedPdfExport ?? true) {
const containerElement = document.getElementById('messages-container');
if (containerElement) {
try {
const isDarkMode = document.documentElement.classList.contains('dark');
if (containerElement) {
try {
const isDarkMode = document.documentElement.classList.contains('dark');
const virtualWidth = 800; // Fixed width in px
const pagePixelHeight = 1200; // Each slice height (adjust to avoid canvas bugs; generally 24k is safe)
console.log('isDarkMode', isDarkMode);
// Clone & style once
const clonedElement = containerElement.cloneNode(true);
clonedElement.classList.add('text-black');
clonedElement.classList.add('dark:text-white');
clonedElement.style.width = `${virtualWidth}px`;
clonedElement.style.position = 'absolute';
clonedElement.style.left = '-9999px'; // Offscreen
clonedElement.style.height = 'auto';
document.body.appendChild(clonedElement);
// Define a fixed virtual screen size
const virtualWidth = 800; // Fixed width (adjust as needed)
// Clone the container to avoid layout shifts
const clonedElement = containerElement.cloneNode(true);
clonedElement.classList.add('text-black');
clonedElement.classList.add('dark:text-white');
clonedElement.style.width = `${virtualWidth}px`; // Apply fixed width
clonedElement.style.height = 'auto'; // Allow content to expand
// Get total height after attached to DOM
const totalHeight = clonedElement.scrollHeight;
let offsetY = 0;
let page = 0;
document.body.appendChild(clonedElement); // Temporarily add to DOM
// Prepare PDF
const pdf = new jsPDF('p', 'mm', 'a4');
const imgWidth = 210; // A4 mm
const pageHeight = 297; // A4 mm
// Render to canvas with predefined width
const canvas = await html2canvas(clonedElement, {
backgroundColor: isDarkMode ? '#000' : '#fff',
useCORS: true,
scale: 2, // Keep at 1x to avoid unexpected enlargements
width: virtualWidth, // Set fixed virtual screen width
windowWidth: virtualWidth // Ensure consistent rendering
});
while (offsetY < totalHeight) {
// For each slice, adjust scrollTop to show desired part
clonedElement.scrollTop = offsetY;
document.body.removeChild(clonedElement); // Clean up temp element
// Optionally: mask/hide overflowing content via CSS if needed
clonedElement.style.maxHeight = `${pagePixelHeight}px`;
// Only render the visible part
const canvas = await html2canvas(clonedElement, {
backgroundColor: isDarkMode ? '#000' : '#fff',
useCORS: true,
scale: 2,
width: virtualWidth,
height: Math.min(pagePixelHeight, totalHeight - offsetY),
// Optionally: y offset for correct region?
windowWidth: virtualWidth
//windowHeight: pagePixelHeight,
});
const imgData = canvas.toDataURL('image/png');
// Maintain aspect ratio
const imgHeight = (canvas.height * imgWidth) / canvas.width;
const position = 0; // Always first line, since we've clipped vertically
const imgData = canvas.toDataURL('image/png');
if (page > 0) pdf.addPage();
// A4 page settings
const pdf = new jsPDF('p', 'mm', 'a4');
const imgWidth = 210; // A4 width in mm
const pageHeight = 297; // A4 height in mm
// Set page background for dark mode
if (isDarkMode) {
pdf.setFillColor(0, 0, 0);
pdf.rect(0, 0, imgWidth, pageHeight, 'F'); // black bg
}
// Maintain aspect ratio
const imgHeight = (canvas.height * imgWidth) / canvas.width;
let heightLeft = imgHeight;
let position = 0;
pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
// Set page background for dark mode
if (isDarkMode) {
pdf.setFillColor(0, 0, 0);
pdf.rect(0, 0, imgWidth, pageHeight, 'F'); // Apply black bg
}
pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
heightLeft -= pageHeight;
// Handle additional pages
while (heightLeft > 0) {
position -= pageHeight;
pdf.addPage();
if (isDarkMode) {
pdf.setFillColor(0, 0, 0);
pdf.rect(0, 0, imgWidth, pageHeight, 'F');
offsetY += pagePixelHeight;
page++;
}
pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
heightLeft -= pageHeight;
}
document.body.removeChild(clonedElement);
pdf.save(`chat-${chat.chat.title}.pdf`);
} catch (error) {
console.error('Error generating PDF', error);
pdf.save(`chat-${chat.chat.title}.pdf`);
} catch (error) {
console.error('Error generating PDF', error);
}
}
} else {
console.log('Downloading PDF');
const chatText = await getChatAsText();
const doc = new jsPDF();
// Margins
const left = 15;
const top = 20;
const right = 15;
const bottom = 20;
const pageWidth = doc.internal.pageSize.getWidth();
const pageHeight = doc.internal.pageSize.getHeight();
const usableWidth = pageWidth - left - right;
const usableHeight = pageHeight - top - bottom;
// Font size and line height
const fontSize = 8;
doc.setFontSize(fontSize);
const lineHeight = fontSize * 1; // adjust if needed
// Split the markdown into lines (handles \n)
const paragraphs = chatText.split('\n');
let y = top;
for (let paragraph of paragraphs) {
// Wrap each paragraph to fit the width
const lines = doc.splitTextToSize(paragraph, usableWidth);
for (let line of lines) {
// If the line would overflow the bottom, add a new page
if (y + lineHeight > pageHeight - bottom) {
doc.addPage();
y = top;
}
doc.text(line, left, y);
y += lineHeight * 0.5;
}
// Add empty line at paragraph breaks
y += lineHeight * 0.1;
}
doc.save(`chat-${chat.chat.title}.pdf`);
}
};

View file

@ -26,7 +26,7 @@
getChatPinnedStatusById,
toggleChatPinnedStatusById
} from '$lib/apis/chats';
import { chats, theme, user } from '$lib/stores';
import { chats, settings, theme, user } from '$lib/stores';
import { createMessagesList } from '$lib/utils';
import { downloadChatAsPDF } from '$lib/apis/utils';
import Download from '$lib/components/icons/Download.svelte';
@ -81,74 +81,124 @@
const downloadPdf = async () => {
const chat = await getChatById(localStorage.token, chatId);
const containerElement = document.getElementById('messages-container');
if ($settings?.stylizedPdfExport ?? true) {
const containerElement = document.getElementById('messages-container');
if (containerElement) {
try {
const isDarkMode = $theme.includes('dark'); // Check theme mode
if (containerElement) {
try {
const isDarkMode = document.documentElement.classList.contains('dark');
const virtualWidth = 800; // Fixed width in px
const pagePixelHeight = 1200; // Each slice height (adjust to avoid canvas bugs; generally 24k is safe)
// Define a fixed virtual screen size
const virtualWidth = 1024; // Fixed width (adjust as needed)
const virtualHeight = 1400; // Fixed height (adjust as needed)
// Clone & style once
const clonedElement = containerElement.cloneNode(true);
clonedElement.classList.add('text-black');
clonedElement.classList.add('dark:text-white');
clonedElement.style.width = `${virtualWidth}px`;
clonedElement.style.position = 'absolute';
clonedElement.style.left = '-9999px'; // Offscreen
clonedElement.style.height = 'auto';
document.body.appendChild(clonedElement);
// Clone the container to avoid layout shifts
const clonedElement = containerElement.cloneNode(true);
clonedElement.style.width = `${virtualWidth}px`; // Apply fixed width
clonedElement.style.height = 'auto'; // Allow content to expand
// Get total height after attached to DOM
const totalHeight = clonedElement.scrollHeight;
let offsetY = 0;
let page = 0;
document.body.appendChild(clonedElement); // Temporarily add to DOM
// Prepare PDF
const pdf = new jsPDF('p', 'mm', 'a4');
const imgWidth = 210; // A4 mm
const pageHeight = 297; // A4 mm
// Render to canvas with predefined width
const canvas = await html2canvas(clonedElement, {
backgroundColor: isDarkMode ? '#000' : '#fff',
useCORS: true,
scale: 2, // Keep at 1x to avoid unexpected enlargements
width: virtualWidth, // Set fixed virtual screen width
windowWidth: virtualWidth, // Ensure consistent rendering
windowHeight: virtualHeight
});
while (offsetY < totalHeight) {
// For each slice, adjust scrollTop to show desired part
clonedElement.scrollTop = offsetY;
document.body.removeChild(clonedElement); // Clean up temp element
// Optionally: mask/hide overflowing content via CSS if needed
clonedElement.style.maxHeight = `${pagePixelHeight}px`;
// Only render the visible part
const canvas = await html2canvas(clonedElement, {
backgroundColor: isDarkMode ? '#000' : '#fff',
useCORS: true,
scale: 2,
width: virtualWidth,
height: Math.min(pagePixelHeight, totalHeight - offsetY),
// Optionally: y offset for correct region?
windowWidth: virtualWidth
//windowHeight: pagePixelHeight,
});
const imgData = canvas.toDataURL('image/png');
// Maintain aspect ratio
const imgHeight = (canvas.height * imgWidth) / canvas.width;
const position = 0; // Always first line, since we've clipped vertically
const imgData = canvas.toDataURL('image/png');
if (page > 0) pdf.addPage();
// A4 page settings
const pdf = new jsPDF('p', 'mm', 'a4');
const imgWidth = 210; // A4 width in mm
const pageHeight = 297; // A4 height in mm
// Set page background for dark mode
if (isDarkMode) {
pdf.setFillColor(0, 0, 0);
pdf.rect(0, 0, imgWidth, pageHeight, 'F'); // black bg
}
// Maintain aspect ratio
const imgHeight = (canvas.height * imgWidth) / canvas.width;
let heightLeft = imgHeight;
let position = 0;
pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
// Set page background for dark mode
if (isDarkMode) {
pdf.setFillColor(0, 0, 0);
pdf.rect(0, 0, imgWidth, pageHeight, 'F'); // Apply black bg
}
pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
heightLeft -= pageHeight;
// Handle additional pages
while (heightLeft > 0) {
position -= pageHeight;
pdf.addPage();
if (isDarkMode) {
pdf.setFillColor(0, 0, 0);
pdf.rect(0, 0, imgWidth, pageHeight, 'F');
offsetY += pagePixelHeight;
page++;
}
pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
heightLeft -= pageHeight;
}
document.body.removeChild(clonedElement);
pdf.save(`chat-${chat.chat.title}.pdf`);
} catch (error) {
console.error('Error generating PDF', error);
pdf.save(`chat-${chat.chat.title}.pdf`);
} catch (error) {
console.error('Error generating PDF', error);
}
}
} else {
console.log('Downloading PDF');
const chatText = await getChatAsText(chat);
const doc = new jsPDF();
// Margins
const left = 15;
const top = 20;
const right = 15;
const bottom = 20;
const pageWidth = doc.internal.pageSize.getWidth();
const pageHeight = doc.internal.pageSize.getHeight();
const usableWidth = pageWidth - left - right;
const usableHeight = pageHeight - top - bottom;
// Font size and line height
const fontSize = 8;
doc.setFontSize(fontSize);
const lineHeight = fontSize * 1; // adjust if needed
// Split the markdown into lines (handles \n)
const paragraphs = chatText.split('\n');
let y = top;
for (let paragraph of paragraphs) {
// Wrap each paragraph to fit the width
const lines = doc.splitTextToSize(paragraph, usableWidth);
for (let line of lines) {
// If the line would overflow the bottom, add a new page
if (y + lineHeight > pageHeight - bottom) {
doc.addPage();
y = top;
}
doc.text(line, left, y);
y += lineHeight;
}
// Add empty line at paragraph breaks
y += lineHeight * 0.5;
}
doc.save(`chat-${chat.chat.title}.pdf`);
}
};

View file

@ -180,7 +180,10 @@
return;
}
const model = $models.find((model) => model.id === selectedModelId);
const model = $models
.filter((model) => model.id === selectedModelId && !(model?.info?.meta?.hidden ?? false))
.find((model) => model.id === selectedModelId);
if (!model) {
selectedModelId = '';
return;
@ -599,6 +602,16 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
selectedModelId = '';
}
if (selectedModelId) {
const model = $models
.filter((model) => model.id === selectedModelId && !(model?.info?.meta?.hidden ?? false))
.find((model) => model.id === selectedModelId);
if (!model) {
selectedModelId = '';
}
}
const dropzoneElement = document.getElementById('note-editor');
dropzoneElement?.addEventListener('dragover', onDragOver);
@ -660,7 +673,10 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
class="w-full bg-transparent text-sm outline-hidden"
bind:value={selectedModelId}
>
{#each $models as model}
<option value="" class="bg-gray-50 dark:bg-gray-700" disabled>
{$i18n.t('Select a model')}
</option>
{#each $models.filter((model) => !(model?.info?.meta?.hidden ?? false)) as model}
<option value={model.id} class="bg-gray-50 dark:bg-gray-700">{model.name}</option>
{/each}
</select>

View file

@ -764,7 +764,7 @@
className="input-prose-sm"
bind:value={selectedFileContent}
placeholder={$i18n.t('Add content here')}
preserveBreaks={true}
preserveBreaks={false}
/>
{/key}
</div>
@ -822,7 +822,7 @@
className="input-prose-sm"
bind:value={selectedFileContent}
placeholder={$i18n.t('Add content here')}
preserveBreaks={true}
preserveBreaks={false}
/>
{/key}
</div>

View file

@ -959,9 +959,8 @@
"Repeat Penalty (Ollama)": "",
"Reply in Thread": "",
"Request Mode": "وضع الطلب",
"Reranking Engine": "",
"Reranking Model": "إعادة تقييم النموذج",
"Reranking model disabled": "تم تعطيل نموذج إعادة الترتيب",
"Reranking model set to \"{{reranking_model}}\"": "تم ضبط نموذج إعادة الترتيب على \"{{reranking_model}}\"",
"Reset": "",
"Reset All Models": "",
"Reset Upload Directory": "",
@ -1069,6 +1068,8 @@
"Show": "عرض",
"Show \"What's New\" modal on login": "",
"Show Admin Details in Account Pending Overlay": "",
"Show All": "",
"Show Less": "",
"Show Model": "",
"Show shortcuts": "إظهار الاختصارات",
"Show your support!": "",
@ -1092,6 +1093,7 @@
"Stream Chat Response": "",
"STT Model": "",
"STT Settings": "STT اعدادات",
"Stylized PDF Export": "",
"Subtitle (e.g. about the Roman Empire)": "(e.g. about the Roman Empire) الترجمة",
"Success": "نجاح",
"Successfully updated.": "تم التحديث بنجاح",

View file

@ -959,9 +959,8 @@
"Repeat Penalty (Ollama)": "عقوبة التكرار (Ollama)",
"Reply in Thread": "الرد داخل سلسلة الرسائل",
"Request Mode": "وضع الطلب",
"Reranking Engine": "",
"Reranking Model": "إعادة تقييم النموذج",
"Reranking model disabled": "تم تعطيل نموذج إعادة الترتيب",
"Reranking model set to \"{{reranking_model}}\"": "تم ضبط نموذج إعادة الترتيب على \"{{reranking_model}}\"",
"Reset": "إعادة تعيين",
"Reset All Models": "إعادة تعيين جميع النماذج",
"Reset Upload Directory": "إعادة تعيين مجلد التحميل",
@ -1069,6 +1068,8 @@
"Show": "عرض",
"Show \"What's New\" modal on login": "عرض نافذة \"ما الجديد\" عند تسجيل الدخول",
"Show Admin Details in Account Pending Overlay": "عرض تفاصيل المشرف في نافذة \"الحساب قيد الانتظار\"",
"Show All": "",
"Show Less": "",
"Show Model": "",
"Show shortcuts": "إظهار الاختصارات",
"Show your support!": "أظهر دعمك!",
@ -1092,6 +1093,7 @@
"Stream Chat Response": "بث استجابة الدردشة",
"STT Model": "نموذج تحويل الصوت إلى نص (STT)",
"STT Settings": "STT اعدادات",
"Stylized PDF Export": "",
"Subtitle (e.g. about the Roman Empire)": "(e.g. about the Roman Empire) الترجمة",
"Success": "نجاح",
"Successfully updated.": "تم التحديث بنجاح",

View file

@ -959,9 +959,8 @@
"Repeat Penalty (Ollama)": "Наказание за повторение (Ollama)",
"Reply in Thread": "Отговори в тред",
"Request Mode": "Режим на заявка",
"Reranking Engine": "",
"Reranking Model": "Модел за преподреждане",
"Reranking model disabled": "Моделът за преподреждане е деактивиран",
"Reranking model set to \"{{reranking_model}}\"": "Моделът за преподреждане е зададен на \"{{reranking_model}}\"",
"Reset": "Нулиране",
"Reset All Models": "Нулиране на всички модели",
"Reset Upload Directory": "Нулиране на директорията за качване",
@ -1069,6 +1068,8 @@
"Show": "Покажи",
"Show \"What's New\" modal on login": "Покажи модалния прозорец \"Какво е ново\" при вписване",
"Show Admin Details in Account Pending Overlay": "Покажи детайлите на администратора в наслагването на изчакващ акаунт",
"Show All": "",
"Show Less": "",
"Show Model": "",
"Show shortcuts": "Покажи преки пътища",
"Show your support!": "Покажете вашата подкрепа!",
@ -1092,6 +1093,7 @@
"Stream Chat Response": "Поточен чат отговор",
"STT Model": "STT Модел",
"STT Settings": "STT Настройки",
"Stylized PDF Export": "",
"Subtitle (e.g. about the Roman Empire)": "Подтитул (напр. за Римска империя)",
"Success": "Успех",
"Successfully updated.": "Успешно обновено.",

View file

@ -959,9 +959,8 @@
"Repeat Penalty (Ollama)": "",
"Reply in Thread": "",
"Request Mode": "রিকোয়েস্ট মোড",
"Reranking Engine": "",
"Reranking Model": "রির্যাক্টিং মডেল",
"Reranking model disabled": "রির্যাক্টিং মডেল নিষ্ক্রিয় করা",
"Reranking model set to \"{{reranking_model}}\"": "রির ্যাঙ্কিং মডেল \"{{reranking_model}}\" -এ সেট করা আছে",
"Reset": "",
"Reset All Models": "",
"Reset Upload Directory": "",
@ -1069,6 +1068,8 @@
"Show": "দেখান",
"Show \"What's New\" modal on login": "",
"Show Admin Details in Account Pending Overlay": "",
"Show All": "",
"Show Less": "",
"Show Model": "",
"Show shortcuts": "শর্টকাটগুলো দেখান",
"Show your support!": "",
@ -1092,6 +1093,7 @@
"Stream Chat Response": "",
"STT Model": "",
"STT Settings": "STT সেটিংস",
"Stylized PDF Export": "",
"Subtitle (e.g. about the Roman Empire)": "সাবটাইটল (রোমান ইম্পার্টের সম্পর্কে)",
"Success": "সফল",
"Successfully updated.": "সফলভাবে আপডেট হয়েছে",

View file

@ -959,9 +959,8 @@
"Repeat Penalty (Ollama)": "བསྐྱར་ཟློས་ཀྱི་ཆད་པ། (Ollama)",
"Reply in Thread": "བརྗོད་གཞིའི་ནང་ལན་འདེབས།",
"Request Mode": "རེ་ཞུའི་མ་དཔེ།",
"Reranking Engine": "",
"Reranking Model": "བསྐྱར་སྒྲིག་དཔེ་དབྱིབས།",
"Reranking model disabled": "བསྐྱར་སྒྲིག་དཔེ་དབྱིབས་ནུས་མེད་བཏང་།",
"Reranking model set to \"{{reranking_model}}\"": "བསྐྱར་སྒྲིག་དཔེ་དབྱིབས་ \"{{reranking_model}}\" ལ་བཀོད་སྒྲིག་བྱས།",
"Reset": "སླར་སྒྲིག",
"Reset All Models": "དཔེ་དབྱིབས་ཡོངས་རྫོགས་སླར་སྒྲིག",
"Reset Upload Directory": "སྤར་བའི་ཐོ་འཚོལ་སླར་སྒྲིག",
@ -1069,6 +1068,8 @@
"Show": "སྟོན་པ།",
"Show \"What's New\" modal on login": "ནང་འཛུལ་སྐབས་ \"གསར་པ་ཅི་ཡོད\" modal སྟོན་པ།",
"Show Admin Details in Account Pending Overlay": "རྩིས་ཁྲ་སྒུག་བཞིན་པའི་གཏོགས་ངོས་སུ་དོ་དམ་པའི་ཞིབ་ཕྲ་སྟོན་པ།",
"Show All": "",
"Show Less": "",
"Show Model": "དཔེ་དབྱིབས་སྟོན་པ།",
"Show shortcuts": "མྱུར་ལམ་སྟོན་པ།",
"Show your support!": "ཁྱེད་ཀྱི་རྒྱབ་སྐྱོར་སྟོན་པ།",
@ -1092,6 +1093,7 @@
"Stream Chat Response": "ཁ་བརྡའི་ལན་རྒྱུག་པ།",
"STT Model": "STT དཔེ་དབྱིབས།",
"STT Settings": "STT སྒྲིག་འགོད།",
"Stylized PDF Export": "",
"Subtitle (e.g. about the Roman Empire)": "ཁ་བྱང་ཕལ་པ། (དཔེར་ན། རོམ་མའི་གོང་མའི་རྒྱལ་ཁབ་སྐོར།)",
"Success": "ལེགས་འགྲུབ།",
"Successfully updated.": "ལེགས་པར་གསར་སྒྱུར་བྱས།",

View file

@ -959,9 +959,8 @@
"Repeat Penalty (Ollama)": "Penalització per repetició (Ollama)",
"Reply in Thread": "Respondre al fil",
"Request Mode": "Mode de sol·licitud",
"Reranking Engine": "",
"Reranking Model": "Model de reavaluació",
"Reranking model disabled": "Model de reavaluació desactivat",
"Reranking model set to \"{{reranking_model}}\"": "Model de reavaluació establert a \"{{reranking_model}}\"",
"Reset": "Restableix",
"Reset All Models": "Restablir tots els models",
"Reset Upload Directory": "Restableix el directori de pujades",
@ -1069,6 +1068,8 @@
"Show": "Mostrar",
"Show \"What's New\" modal on login": "Veure 'Què hi ha de nou' a l'entrada",
"Show Admin Details in Account Pending Overlay": "Mostrar els detalls de l'administrador a la superposició del compte pendent",
"Show All": "",
"Show Less": "",
"Show Model": "Mostrar el model",
"Show shortcuts": "Mostrar dreceres",
"Show your support!": "Mostra el teu suport!",
@ -1092,6 +1093,7 @@
"Stream Chat Response": "Fer streaming de la resposta del xat",
"STT Model": "Model SST",
"STT Settings": "Preferències de STT",
"Stylized PDF Export": "",
"Subtitle (e.g. about the Roman Empire)": "Subtítol (per exemple, sobre l'Imperi Romà)",
"Success": "Èxit",
"Successfully updated.": "Actualitzat correctament.",

View file

@ -959,9 +959,8 @@
"Repeat Penalty (Ollama)": "",
"Reply in Thread": "",
"Request Mode": "Query mode",
"Reranking Engine": "",
"Reranking Model": "",
"Reranking model disabled": "",
"Reranking model set to \"{{reranking_model}}\"": "",
"Reset": "",
"Reset All Models": "",
"Reset Upload Directory": "",
@ -1069,6 +1068,8 @@
"Show": "Pagpakita",
"Show \"What's New\" modal on login": "",
"Show Admin Details in Account Pending Overlay": "",
"Show All": "",
"Show Less": "",
"Show Model": "",
"Show shortcuts": "Ipakita ang mga shortcut",
"Show your support!": "",
@ -1092,6 +1093,7 @@
"Stream Chat Response": "",
"STT Model": "",
"STT Settings": "Mga setting sa STT",
"Stylized PDF Export": "",
"Subtitle (e.g. about the Roman Empire)": "",
"Success": "Kalampusan",
"Successfully updated.": "Malampuson nga na-update.",

View file

@ -959,9 +959,8 @@
"Repeat Penalty (Ollama)": "",
"Reply in Thread": "",
"Request Mode": "Režim žádosti",
"Reranking Engine": "",
"Reranking Model": "Model pro přehodnocení pořadí",
"Reranking model disabled": "Přeřazovací model je deaktivován",
"Reranking model set to \"{{reranking_model}}\"": "Model pro přeřazení nastaven na \"{{reranking_model}}\"",
"Reset": "režim Reset",
"Reset All Models": "",
"Reset Upload Directory": "Resetovat adresář nahrávání",
@ -1069,6 +1068,8 @@
"Show": "Zobrazit",
"Show \"What's New\" modal on login": "",
"Show Admin Details in Account Pending Overlay": "Zobrazit podrobnosti administrátora v překryvném okně s čekajícím účtem",
"Show All": "",
"Show Less": "",
"Show Model": "",
"Show shortcuts": "Zobrazit klávesové zkratky",
"Show your support!": "Vyjadřete svou podporu!",
@ -1092,6 +1093,7 @@
"Stream Chat Response": "Odezva chatu Stream",
"STT Model": "Model rozpoznávání řeči na text (STT)",
"STT Settings": "Nastavení STT (Rozpoznávání řeči)",
"Stylized PDF Export": "",
"Subtitle (e.g. about the Roman Empire)": "Titulky (např. o Římské říši)",
"Success": "Úspěch",
"Successfully updated.": "Úspěšně aktualizováno.",

File diff suppressed because it is too large Load diff

View file

@ -959,9 +959,8 @@
"Repeat Penalty (Ollama)": "Wiederholungsstrafe (Ollama)",
"Reply in Thread": "Im Thread antworten",
"Request Mode": "Anforderungsmodus",
"Reranking Engine": "",
"Reranking Model": "Reranking-Modell",
"Reranking model disabled": "Reranking-Modell deaktiviert",
"Reranking model set to \"{{reranking_model}}\"": "Reranking-Modell \"{{reranking_model}}\" fesgelegt",
"Reset": "Zurücksetzen",
"Reset All Models": "Alle Modelle zurücksetzen",
"Reset Upload Directory": "Upload-Verzeichnis zurücksetzen",
@ -1069,6 +1068,8 @@
"Show": "Anzeigen",
"Show \"What's New\" modal on login": "\"Was gibt's Neues\"-Modal beim Anmelden anzeigen",
"Show Admin Details in Account Pending Overlay": "Admin-Details im Account-Pending-Overlay anzeigen",
"Show All": "",
"Show Less": "",
"Show Model": "Modell anzeigen",
"Show shortcuts": "Verknüpfungen anzeigen",
"Show your support!": "Zeigen Sie Ihre Unterstützung!",
@ -1092,6 +1093,7 @@
"Stream Chat Response": "Chat-Antwort streamen",
"STT Model": "STT-Modell",
"STT Settings": "STT-Einstellungen",
"Stylized PDF Export": "",
"Subtitle (e.g. about the Roman Empire)": "Untertitel (z. B. über das Römische Reich)",
"Success": "Erfolg",
"Successfully updated.": "Erfolgreich aktualisiert.",

View file

@ -959,9 +959,8 @@
"Repeat Penalty (Ollama)": "",
"Reply in Thread": "",
"Request Mode": "Request Bark",
"Reranking Engine": "",
"Reranking Model": "",
"Reranking model disabled": "",
"Reranking model set to \"{{reranking_model}}\"": "",
"Reset": "",
"Reset All Models": "",
"Reset Upload Directory": "",
@ -1069,6 +1068,8 @@
"Show": "Show much show",
"Show \"What's New\" modal on login": "",
"Show Admin Details in Account Pending Overlay": "",
"Show All": "",
"Show Less": "",
"Show Model": "",
"Show shortcuts": "Show shortcuts much shortcut",
"Show your support!": "",
@ -1092,6 +1093,7 @@
"Stream Chat Response": "",
"STT Model": "",
"STT Settings": "STT Settings very settings",
"Stylized PDF Export": "",
"Subtitle (e.g. about the Roman Empire)": "",
"Success": "Success very success",
"Successfully updated.": "Successfully updated. Very updated.",

View file

@ -959,9 +959,8 @@
"Repeat Penalty (Ollama)": "",
"Reply in Thread": "",
"Request Mode": "Λειτουργία Αιτήματος",
"Reranking Engine": "",
"Reranking Model": "Μοντέλο Επαναταξινόμησης",
"Reranking model disabled": "Το μοντέλο επαναταξινόμησης απενεργοποιήθηκε",
"Reranking model set to \"{{reranking_model}}\"": "Το μοντέλο επαναταξινόμησης ορίστηκε σε \"{{reranking_model}}\"",
"Reset": "Επαναφορά",
"Reset All Models": "Επαναφορά Όλων των Μοντέλων",
"Reset Upload Directory": "Επαναφορά Καταλόγου Ανεβάσματος",
@ -1069,6 +1068,8 @@
"Show": "Εμφάνιση",
"Show \"What's New\" modal on login": "Εμφάνιση του παράθυρου \"Τι νέο υπάρχει\" κατά την είσοδο",
"Show Admin Details in Account Pending Overlay": "Εμφάνιση Λεπτομερειών Διαχειριστή στο Υπέρθεση Εκκρεμής Λογαριασμού",
"Show All": "",
"Show Less": "",
"Show Model": "",
"Show shortcuts": "Εμφάνιση συντομεύσεων",
"Show your support!": "Δείξτε την υποστήριξή σας!",
@ -1092,6 +1093,7 @@
"Stream Chat Response": "Συνομιλία Ροής Απάντησης",
"STT Model": "Μοντέλο STT",
"STT Settings": "Ρυθμίσεις STT",
"Stylized PDF Export": "",
"Subtitle (e.g. about the Roman Empire)": "Υπότιτλος (π.χ. για την Ρωμαϊκή Αυτοκρατορία)",
"Success": "Επιτυχία",
"Successfully updated.": "Επιτυχώς ενημερώθηκε.",

View file

@ -959,9 +959,8 @@
"Repeat Penalty (Ollama)": "",
"Reply in Thread": "",
"Request Mode": "",
"Reranking Engine": "",
"Reranking Model": "",
"Reranking model disabled": "",
"Reranking model set to \"{{reranking_model}}\"": "",
"Reset": "",
"Reset All Models": "",
"Reset Upload Directory": "",
@ -1069,6 +1068,8 @@
"Show": "",
"Show \"What's New\" modal on login": "",
"Show Admin Details in Account Pending Overlay": "",
"Show All": "",
"Show Less": "",
"Show Model": "",
"Show shortcuts": "",
"Show your support!": "",
@ -1092,6 +1093,7 @@
"Stream Chat Response": "",
"STT Model": "",
"STT Settings": "",
"Stylized PDF Export": "",
"Subtitle (e.g. about the Roman Empire)": "",
"Success": "",
"Successfully updated.": "",

View file

@ -959,9 +959,8 @@
"Repeat Penalty (Ollama)": "",
"Reply in Thread": "",
"Request Mode": "",
"Reranking Engine": "",
"Reranking Model": "",
"Reranking model disabled": "",
"Reranking model set to \"{{reranking_model}}\"": "",
"Reset": "",
"Reset All Models": "",
"Reset Upload Directory": "",
@ -1069,6 +1068,8 @@
"Show": "",
"Show \"What's New\" modal on login": "",
"Show Admin Details in Account Pending Overlay": "",
"Show All": "",
"Show Less": "",
"Show Model": "",
"Show shortcuts": "",
"Show your support!": "",
@ -1092,6 +1093,7 @@
"Stream Chat Response": "",
"STT Model": "",
"STT Settings": "",
"Stylized PDF Export": "",
"Subtitle (e.g. about the Roman Empire)": "",
"Success": "",
"Successfully updated.": "",

View file

@ -959,9 +959,8 @@
"Repeat Penalty (Ollama)": "Penalización Repetición (Ollama)",
"Reply in Thread": "Responder en Hilo",
"Request Mode": "Modo de Petición",
"Reranking Engine": "",
"Reranking Model": "Modelo de Reclasificación",
"Reranking model disabled": "Modelo de reclasificacioń deshabilitado",
"Reranking model set to \"{{reranking_model}}\"": "Modelo de reclasificación establecido a \"{{reranking_model}}\"",
"Reset": "Reiniciar",
"Reset All Models": "Reiniciar Todos los Modelos",
"Reset Upload Directory": "Reiniciar Directorio de Subidas",
@ -1069,6 +1068,8 @@
"Show": "Mostrar",
"Show \"What's New\" modal on login": "Mostrar modal \"Qué hay de Nuevo\" al iniciar sesión",
"Show Admin Details in Account Pending Overlay": "Mostrar Detalles Admin en la sobrecapa de 'Cuenta Pendiente'",
"Show All": "",
"Show Less": "",
"Show Model": "Mostrar Modelo",
"Show shortcuts": "Mostrar Atajos",
"Show your support!": "¡Muestra tu apoyo!",
@ -1092,6 +1093,7 @@
"Stream Chat Response": "Transmisión Directa de la Respuesta del Chat",
"STT Model": "Modelo STT",
"STT Settings": "Ajustes Voz a Texto (STT)",
"Stylized PDF Export": "",
"Subtitle (e.g. about the Roman Empire)": "Subtítulo (p.ej. sobre el Imperio Romano)",
"Success": "Correcto",
"Successfully updated.": "Actualizado correctamente.",

View file

@ -959,9 +959,8 @@
"Repeat Penalty (Ollama)": "Korduse karistus (Ollama)",
"Reply in Thread": "Vasta lõimes",
"Request Mode": "Päringu režiim",
"Reranking Engine": "",
"Reranking Model": "Ümberjärjestamise mudel",
"Reranking model disabled": "Ümberjärjestamise mudel keelatud",
"Reranking model set to \"{{reranking_model}}\"": "Ümberjärjestamise mudel määratud kui \"{{reranking_model}}\"",
"Reset": "Lähtesta",
"Reset All Models": "Lähtesta kõik mudelid",
"Reset Upload Directory": "Lähtesta üleslaadimiste kataloog",
@ -1069,6 +1068,8 @@
"Show": "Näita",
"Show \"What's New\" modal on login": "Näita \"Mis on uut\" modaalakent sisselogimisel",
"Show Admin Details in Account Pending Overlay": "Näita administraatori üksikasju konto ootel kattekihil",
"Show All": "",
"Show Less": "",
"Show Model": "",
"Show shortcuts": "Näita otseteid",
"Show your support!": "Näita oma toetust!",
@ -1092,6 +1093,7 @@
"Stream Chat Response": "Voogedasta vestluse vastust",
"STT Model": "STT mudel",
"STT Settings": "STT seaded",
"Stylized PDF Export": "",
"Subtitle (e.g. about the Roman Empire)": "Alampealkiri (nt Rooma impeeriumi kohta)",
"Success": "Õnnestus",
"Successfully updated.": "Edukalt uuendatud.",

View file

@ -959,9 +959,8 @@
"Repeat Penalty (Ollama)": "",
"Reply in Thread": "",
"Request Mode": "Eskaera modua",
"Reranking Engine": "",
"Reranking Model": "Berrantolatze modeloa",
"Reranking model disabled": "Berrantolatze modeloa desgaituta",
"Reranking model set to \"{{reranking_model}}\"": "Berrantolatze modeloa \"{{reranking_model}}\"-era ezarrita",
"Reset": "Berrezarri",
"Reset All Models": "",
"Reset Upload Directory": "Berrezarri karga direktorioa",
@ -1069,6 +1068,8 @@
"Show": "Erakutsi",
"Show \"What's New\" modal on login": "Erakutsi \"Berritasunak\" modala saioa hastean",
"Show Admin Details in Account Pending Overlay": "Erakutsi administratzaile xehetasunak kontu zain geruzan",
"Show All": "",
"Show Less": "",
"Show Model": "",
"Show shortcuts": "Erakutsi lasterbideak",
"Show your support!": "Erakutsi zure babesa!",
@ -1092,6 +1093,7 @@
"Stream Chat Response": "Transmititu txat erantzuna",
"STT Model": "STT modeloa",
"STT Settings": "STT ezarpenak",
"Stylized PDF Export": "",
"Subtitle (e.g. about the Roman Empire)": "Azpititulua (adib. Erromatar Inperioari buruz)",
"Success": "Arrakasta",
"Successfully updated.": "Ongi eguneratu da.",

View file

@ -959,9 +959,8 @@
"Repeat Penalty (Ollama)": "جریمه تکرار (ollama)",
"Reply in Thread": "پاسخ در رشته",
"Request Mode": "حالت درخواست",
"Reranking Engine": "",
"Reranking Model": "مدل ری\u200cشناسی مجدد غیرفعال است",
"Reranking model disabled": "مدل ری\u200cشناسی مجدد غیرفعال است",
"Reranking model set to \"{{reranking_model}}\"": "مدل ری\u200cشناسی مجدد به \"{{reranking_model}}\" تنظیم شده است",
"Reset": "بازنشانی",
"Reset All Models": "بازنشانی همه مدل\u200cها",
"Reset Upload Directory": "بازنشانی پوشه آپلود",
@ -1069,6 +1068,8 @@
"Show": "نمایش",
"Show \"What's New\" modal on login": "نمایش مودال \"موارد جدید\" هنگام ورود",
"Show Admin Details in Account Pending Overlay": "نمایش جزئیات مدیر در پوشش حساب در انتظار",
"Show All": "",
"Show Less": "",
"Show Model": "نمایش مدل",
"Show shortcuts": "نمایش میانبرها",
"Show your support!": "حمایت خود را نشان دهید!",
@ -1092,6 +1093,7 @@
"Stream Chat Response": "پاسخ چت جریانی",
"STT Model": "مدل تبدیل صدا به متن",
"STT Settings": "تنظیمات تبدیل صدا به متن",
"Stylized PDF Export": "",
"Subtitle (e.g. about the Roman Empire)": "زیرنویس (برای مثال: درباره رمانی)",
"Success": "موفقیت",
"Successfully updated.": "با موفقیت به\u200cروز شد",

View file

@ -959,9 +959,8 @@
"Repeat Penalty (Ollama)": "Toisto rangaistus (Ollama)",
"Reply in Thread": "Vastauksia ",
"Request Mode": "Pyyntötila",
"Reranking Engine": "",
"Reranking Model": "Uudelleenpisteytymismalli",
"Reranking model disabled": "Uudelleenpisteytymismalli poistettu käytöstä",
"Reranking model set to \"{{reranking_model}}\"": "\"{{reranking_model}}\" valittu uudelleenpisteytysmalliksi",
"Reset": "Palauta",
"Reset All Models": "Palauta kaikki mallit",
"Reset Upload Directory": "Palauta latauspolku",
@ -1069,6 +1068,8 @@
"Show": "Näytä",
"Show \"What's New\" modal on login": "Näytä \"Mitä uutta\" -modaali kirjautumisen yhteydessä",
"Show Admin Details in Account Pending Overlay": "Näytä ylläpitäjän tiedot odottavan tilin päällä",
"Show All": "",
"Show Less": "",
"Show Model": "Näytä malli",
"Show shortcuts": "Näytä pikanäppäimet",
"Show your support!": "Osoita tukesi!",
@ -1092,6 +1093,7 @@
"Stream Chat Response": "Streamaa keskusteluvastaus",
"STT Model": "Puheentunnistusmalli",
"STT Settings": "Puheentunnistuksen asetukset",
"Stylized PDF Export": "",
"Subtitle (e.g. about the Roman Empire)": "Alaotsikko (esim. Rooman valtakunta)",
"Success": "Onnistui",
"Successfully updated.": "Päivitetty onnistuneesti.",

View file

@ -959,9 +959,8 @@
"Repeat Penalty (Ollama)": "",
"Reply in Thread": "",
"Request Mode": "Mode de Requête",
"Reranking Engine": "",
"Reranking Model": "Modèle de ré-ranking",
"Reranking model disabled": "Modèle de ré-ranking désactivé",
"Reranking model set to \"{{reranking_model}}\"": "Modèle de ré-ranking défini sur « {{reranking_model}} »",
"Reset": "Réinitialiser",
"Reset All Models": "",
"Reset Upload Directory": "Répertoire de téléchargement réinitialisé",
@ -1069,6 +1068,8 @@
"Show": "Montrer",
"Show \"What's New\" modal on login": "",
"Show Admin Details in Account Pending Overlay": "Afficher les détails de l'administrateur dans la superposition en attente du compte",
"Show All": "",
"Show Less": "",
"Show Model": "",
"Show shortcuts": "Afficher les raccourcis",
"Show your support!": "Montre ton soutien !",
@ -1092,6 +1093,7 @@
"Stream Chat Response": "",
"STT Model": "Modèle de STT",
"STT Settings": "Paramètres de STT",
"Stylized PDF Export": "",
"Subtitle (e.g. about the Roman Empire)": "Sous-titres (par ex. sur l'Empire romain)",
"Success": "Réussite",
"Successfully updated.": "Mise à jour réussie.",

View file

@ -959,9 +959,8 @@
"Repeat Penalty (Ollama)": "Pénalité de répétition (Ollama)",
"Reply in Thread": "Répondre dans le fil de discussion",
"Request Mode": "Mode de requête",
"Reranking Engine": "",
"Reranking Model": "Modèle de ré-ranking",
"Reranking model disabled": "Modèle de ré-ranking désactivé",
"Reranking model set to \"{{reranking_model}}\"": "Modèle de ré-ranking défini sur « {{reranking_model}} »",
"Reset": "Réinitialiser",
"Reset All Models": "Réinitialiser tous les modèles",
"Reset Upload Directory": "Réinitialiser le répertoire de téléchargement",
@ -1069,6 +1068,8 @@
"Show": "Afficher",
"Show \"What's New\" modal on login": "Afficher la fenêtre modale \"Quoi de neuf\" lors de la connexion",
"Show Admin Details in Account Pending Overlay": "Afficher les coordonnées de l'administrateur aux comptes en attente",
"Show All": "",
"Show Less": "",
"Show Model": "Afficher le model",
"Show shortcuts": "Afficher les raccourcis",
"Show your support!": "Montrez votre soutien !",
@ -1092,6 +1093,7 @@
"Stream Chat Response": "Streamer la réponse de la conversation",
"STT Model": "Modèle de Speech-to-Text",
"STT Settings": "Paramètres de Speech-to-Text",
"Stylized PDF Export": "",
"Subtitle (e.g. about the Roman Empire)": "Sous-titres (par ex. sur l'Empire romain)",
"Success": "Réussite",
"Successfully updated.": "Mise à jour réussie.",

View file

@ -959,9 +959,8 @@
"Repeat Penalty (Ollama)": "",
"Reply in Thread": "",
"Request Mode": "מצב בקשה",
"Reranking Engine": "",
"Reranking Model": "מודל דירוג מחדש",
"Reranking model disabled": "מודל דירוג מחדש מושבת",
"Reranking model set to \"{{reranking_model}}\"": "מודל דירוג מחדש הוגדר ל-\"{{reranking_model}}\"",
"Reset": "",
"Reset All Models": "",
"Reset Upload Directory": "",
@ -1069,6 +1068,8 @@
"Show": "הצג",
"Show \"What's New\" modal on login": "",
"Show Admin Details in Account Pending Overlay": "",
"Show All": "",
"Show Less": "",
"Show Model": "",
"Show shortcuts": "הצג קיצורי דרך",
"Show your support!": "",
@ -1092,6 +1093,7 @@
"Stream Chat Response": "",
"STT Model": "",
"STT Settings": "הגדרות חקירה של TTS",
"Stylized PDF Export": "",
"Subtitle (e.g. about the Roman Empire)": "תחקור (לדוגמה: על מעמד הרומי)",
"Success": "הצלחה",
"Successfully updated.": "עדכון הצלחה.",

View file

@ -959,9 +959,8 @@
"Repeat Penalty (Ollama)": "",
"Reply in Thread": "",
"Request Mode": "अनुरोध मोड",
"Reranking Engine": "",
"Reranking Model": "रीरैकिंग मोड",
"Reranking model disabled": "पुनर्रैंकिंग मॉडल अक्षम किया गया",
"Reranking model set to \"{{reranking_model}}\"": "रीरैंकिंग मॉडल को \"{{reranking_model}}\" पर \u200b\u200bसेट किया गया",
"Reset": "",
"Reset All Models": "",
"Reset Upload Directory": "",
@ -1069,6 +1068,8 @@
"Show": "दिखाओ",
"Show \"What's New\" modal on login": "",
"Show Admin Details in Account Pending Overlay": "",
"Show All": "",
"Show Less": "",
"Show Model": "",
"Show shortcuts": "शॉर्टकट दिखाएँ",
"Show your support!": "",
@ -1092,6 +1093,7 @@
"Stream Chat Response": "",
"STT Model": "",
"STT Settings": "STT सेटिंग्स ",
"Stylized PDF Export": "",
"Subtitle (e.g. about the Roman Empire)": "उपशीर्षक (जैसे रोमन साम्राज्य के बारे में)",
"Success": "संपन्न",
"Successfully updated.": "सफलतापूर्वक उत्परिवर्तित।",

View file

@ -959,9 +959,8 @@
"Repeat Penalty (Ollama)": "",
"Reply in Thread": "",
"Request Mode": "Način zahtjeva",
"Reranking Engine": "",
"Reranking Model": "Model za ponovno rangiranje",
"Reranking model disabled": "Model za ponovno rangiranje onemogućen",
"Reranking model set to \"{{reranking_model}}\"": "Model za ponovno rangiranje postavljen na \"{{reranking_model}}\"",
"Reset": "",
"Reset All Models": "",
"Reset Upload Directory": "Poništi upload direktorij",
@ -1069,6 +1068,8 @@
"Show": "Pokaži",
"Show \"What's New\" modal on login": "",
"Show Admin Details in Account Pending Overlay": "",
"Show All": "",
"Show Less": "",
"Show Model": "",
"Show shortcuts": "Pokaži prečace",
"Show your support!": "",
@ -1092,6 +1093,7 @@
"Stream Chat Response": "",
"STT Model": "STT model",
"STT Settings": "STT postavke",
"Stylized PDF Export": "",
"Subtitle (e.g. about the Roman Empire)": "Podnaslov (npr. o Rimskom carstvu)",
"Success": "Uspjeh",
"Successfully updated.": "Uspješno ažurirano.",

View file

@ -959,9 +959,8 @@
"Repeat Penalty (Ollama)": "Ismétlési büntetés (Ollama)",
"Reply in Thread": "Válasz szálban",
"Request Mode": "Kérési mód",
"Reranking Engine": "",
"Reranking Model": "Újrarangsoroló modell",
"Reranking model disabled": "Újrarangsoroló modell letiltva",
"Reranking model set to \"{{reranking_model}}\"": "Újrarangsoroló modell beállítva erre: \"{{reranking_model}}\"",
"Reset": "Visszaállítás",
"Reset All Models": "Minden modell visszaállítása",
"Reset Upload Directory": "Feltöltési könyvtár visszaállítása",
@ -1069,6 +1068,8 @@
"Show": "Mutat",
"Show \"What's New\" modal on login": "\"Mi újság\" modal megjelenítése bejelentkezéskor",
"Show Admin Details in Account Pending Overlay": "Admin részletek megjelenítése a függő fiók átfedésben",
"Show All": "",
"Show Less": "",
"Show Model": "Modell megjelenítése",
"Show shortcuts": "Gyorsbillentyűk megjelenítése",
"Show your support!": "Mutassa meg támogatását!",
@ -1092,6 +1093,7 @@
"Stream Chat Response": "Chat válasz streamelése",
"STT Model": "STT modell",
"STT Settings": "STT beállítások",
"Stylized PDF Export": "",
"Subtitle (e.g. about the Roman Empire)": "Alcím (pl. a Római Birodalomról)",
"Success": "Siker",
"Successfully updated.": "Sikeresen frissítve.",

View file

@ -959,9 +959,8 @@
"Repeat Penalty (Ollama)": "",
"Reply in Thread": "",
"Request Mode": "Mode Permintaan",
"Reranking Engine": "",
"Reranking Model": "Model Pemeringkatan Ulang",
"Reranking model disabled": "Model pemeringkatan ulang dinonaktifkan",
"Reranking model set to \"{{reranking_model}}\"": "Model pemeringkatan diatur ke \"{{reranking_model}}\"",
"Reset": "Atur Ulang",
"Reset All Models": "",
"Reset Upload Directory": "Setel Ulang Direktori Unggahan",
@ -1069,6 +1068,8 @@
"Show": "Tampilkan",
"Show \"What's New\" modal on login": "",
"Show Admin Details in Account Pending Overlay": "Tampilkan Detail Admin di Hamparan Akun Tertunda",
"Show All": "",
"Show Less": "",
"Show Model": "",
"Show shortcuts": "Tampilkan pintasan",
"Show your support!": "Tunjukkan dukungan Anda!",
@ -1092,6 +1093,7 @@
"Stream Chat Response": "",
"STT Model": "Model STT",
"STT Settings": "Pengaturan STT",
"Stylized PDF Export": "",
"Subtitle (e.g. about the Roman Empire)": "Subtitle (misalnya tentang Kekaisaran Romawi)",
"Success": "Berhasil",
"Successfully updated.": "Berhasil diperbarui.",

View file

@ -959,9 +959,8 @@
"Repeat Penalty (Ollama)": "Pionós Athrá (Ollama)",
"Reply in Thread": "Freagra i Snáithe",
"Request Mode": "Mód Iarratais",
"Reranking Engine": "",
"Reranking Model": "Múnla Athrangú",
"Reranking model disabled": "Samhail athrangú faoi mhíchumas",
"Reranking model set to \"{{reranking_model}}\"": "Samhail athrangú socraithe go \"{{reranking_model}}\"",
"Reset": "Athshocraigh",
"Reset All Models": "Athshocraigh Gach Múnla",
"Reset Upload Directory": "Athshocraigh Eolaire Uas",
@ -1069,6 +1068,8 @@
"Show": "Taispeáin",
"Show \"What's New\" modal on login": "Taispeáin módúil \"Cad atá Nua\" ar logáil isteach",
"Show Admin Details in Account Pending Overlay": "Taispeáin Sonraí Riaracháin sa Chuntas ar Feitheamh Forleagan",
"Show All": "",
"Show Less": "",
"Show Model": "Taispeáin Múnla",
"Show shortcuts": "Taispeáin aicearraí",
"Show your support!": "Taispeáin do thacaíocht!",
@ -1092,6 +1093,7 @@
"Stream Chat Response": "Freagra Comhrá Sruth",
"STT Model": "Múnla STT",
"STT Settings": "Socruithe STT",
"Stylized PDF Export": "",
"Subtitle (e.g. about the Roman Empire)": "Fotheideal (m.sh. faoin Impireacht Rómhánach)",
"Success": "Rath",
"Successfully updated.": "Nuashonraithe go rathúil.",

File diff suppressed because it is too large Load diff

View file

@ -959,9 +959,8 @@
"Repeat Penalty (Ollama)": "",
"Reply in Thread": "",
"Request Mode": "リクエストモード",
"Reranking Engine": "",
"Reranking Model": "モデルの再ランキング",
"Reranking model disabled": "再ランキングモデルが無効です",
"Reranking model set to \"{{reranking_model}}\"": "再ランキングモデルを \"{{reranking_model}}\" に設定しました",
"Reset": "",
"Reset All Models": "",
"Reset Upload Directory": "アップロードディレクトリをリセット",
@ -1069,6 +1068,8 @@
"Show": "表示",
"Show \"What's New\" modal on login": "",
"Show Admin Details in Account Pending Overlay": "",
"Show All": "",
"Show Less": "",
"Show Model": "",
"Show shortcuts": "表示",
"Show your support!": "",
@ -1092,6 +1093,7 @@
"Stream Chat Response": "",
"STT Model": "STTモデル",
"STT Settings": "STT設定",
"Stylized PDF Export": "",
"Subtitle (e.g. about the Roman Empire)": "タイトル (例: ローマ帝国)",
"Success": "成功",
"Successfully updated.": "正常に更新されました。",

View file

@ -959,9 +959,8 @@
"Repeat Penalty (Ollama)": "",
"Reply in Thread": "ნაკადში პასუხი",
"Request Mode": "მოთხოვნის რეჟიმი",
"Reranking Engine": "",
"Reranking Model": "Reranking მოდელი",
"Reranking model disabled": "Reranking მოდელი გათიშულია",
"Reranking model set to \"{{reranking_model}}\"": "Reranking model set to \"{{reranking_model}}\"",
"Reset": "ჩამოყრა",
"Reset All Models": "",
"Reset Upload Directory": "",
@ -1069,6 +1068,8 @@
"Show": "ჩვენება",
"Show \"What's New\" modal on login": "",
"Show Admin Details in Account Pending Overlay": "",
"Show All": "",
"Show Less": "",
"Show Model": "",
"Show shortcuts": "მალსახმობების ჩვენება",
"Show your support!": "",
@ -1092,6 +1093,7 @@
"Stream Chat Response": "",
"STT Model": "",
"STT Settings": "STT-ის მორგება",
"Stylized PDF Export": "",
"Subtitle (e.g. about the Roman Empire)": "სუბტიტრები (მაგ. რომის იმპერიის შესახებ)",
"Success": "წარმატება",
"Successfully updated.": "წარმატებით განახლდა.",

View file

@ -959,9 +959,8 @@
"Repeat Penalty (Ollama)": "",
"Reply in Thread": "스레드에 답글 달기",
"Request Mode": "요청 모드",
"Reranking Engine": "",
"Reranking Model": "Reranking 모델",
"Reranking model disabled": "Reranking 모델 비활성화",
"Reranking model set to \"{{reranking_model}}\"": "Reranking 모델을 \"{{reranking_model}}\"로 설정",
"Reset": "초기화",
"Reset All Models": "모든 모델 초기화",
"Reset Upload Directory": "업로드 디렉토리 초기화",
@ -1069,6 +1068,8 @@
"Show": "보기",
"Show \"What's New\" modal on login": "로그인시 \"새로운 기능\" 모달 보기",
"Show Admin Details in Account Pending Overlay": "사용자용 계정 보류 설명창에, 관리자 상세 정보 노출",
"Show All": "",
"Show Less": "",
"Show Model": "",
"Show shortcuts": "단축키 보기",
"Show your support!": "당신의 응원을 보내주세요!",
@ -1092,6 +1093,7 @@
"Stream Chat Response": "스트림 채팅 응답",
"STT Model": "STT 모델",
"STT Settings": "STT 설정",
"Stylized PDF Export": "",
"Subtitle (e.g. about the Roman Empire)": "자막 (예: 로마 황제)",
"Success": "성공",
"Successfully updated.": "성공적으로 업데이트되었습니다.",

View file

@ -959,9 +959,8 @@
"Repeat Penalty (Ollama)": "",
"Reply in Thread": "",
"Request Mode": "Užklausos rėžimas",
"Reranking Engine": "",
"Reranking Model": "Reranking modelis",
"Reranking model disabled": "Reranking modelis neleidžiamas",
"Reranking model set to \"{{reranking_model}}\"": "Nustatytas rereanking modelis: \"{{reranking_model}}\"",
"Reset": "Atkurti",
"Reset All Models": "",
"Reset Upload Directory": "Atkurti įkėlimų direktoiją",
@ -1069,6 +1068,8 @@
"Show": "Rodyti",
"Show \"What's New\" modal on login": "",
"Show Admin Details in Account Pending Overlay": "Rodyti administratoriaus duomenis laukiant paskyros patvirtinimo",
"Show All": "",
"Show Less": "",
"Show Model": "",
"Show shortcuts": "Rodyti trumpinius",
"Show your support!": "Palaikykite",
@ -1092,6 +1093,7 @@
"Stream Chat Response": "",
"STT Model": "STT modelis",
"STT Settings": "STT nustatymai",
"Stylized PDF Export": "",
"Subtitle (e.g. about the Roman Empire)": "Subtitras",
"Success": "Sėkmingai",
"Successfully updated.": "Sėkmingai atnaujinta.",

View file

@ -959,9 +959,8 @@
"Repeat Penalty (Ollama)": "",
"Reply in Thread": "",
"Request Mode": "Mod Permintaan",
"Reranking Engine": "",
"Reranking Model": "Model 'Reranking'",
"Reranking model disabled": "Model 'Reranking' dilumpuhkan",
"Reranking model set to \"{{reranking_model}}\"": "Model 'Reranking' ditetapkan kepada \"{{reranking_model}}\"",
"Reset": "Tetapkan Semula",
"Reset All Models": "",
"Reset Upload Directory": "Tetapkan Semula Direktori Muat Naik",
@ -1069,6 +1068,8 @@
"Show": "Tunjukkan",
"Show \"What's New\" modal on login": "",
"Show Admin Details in Account Pending Overlay": "Tunjukkan Butiran Pentadbir dalam Akaun Menunggu Tindanan",
"Show All": "",
"Show Less": "",
"Show Model": "",
"Show shortcuts": "Tunjukkan pintasan",
"Show your support!": "Tunjukkan sokongan anda!",
@ -1092,6 +1093,7 @@
"Stream Chat Response": "",
"STT Model": "Model STT",
"STT Settings": "Tetapan STT",
"Stylized PDF Export": "",
"Subtitle (e.g. about the Roman Empire)": "Sari kata (cth tentang Kesultanan Melaka)",
"Success": "Berjaya",
"Successfully updated.": "Berjaya Dikemaskini",

View file

@ -959,9 +959,8 @@
"Repeat Penalty (Ollama)": "Gjenta straff (Ollama)",
"Reply in Thread": "Svar i tråd",
"Request Mode": "Forespørselsmodus",
"Reranking Engine": "",
"Reranking Model": "Omrangeringsmodell",
"Reranking model disabled": "Omrangeringsmodell deaktivert",
"Reranking model set to \"{{reranking_model}}\"": "Omrangeringsmodell er angitt til \"{{reranking_model}}\"",
"Reset": "Tilbakestill",
"Reset All Models": "Tilbakestill alle modeller",
"Reset Upload Directory": "Tilbakestill opplastingskatalog",
@ -1069,6 +1068,8 @@
"Show": "Vis",
"Show \"What's New\" modal on login": "Vis \"Hva er nytt\"-modal ved innlogging",
"Show Admin Details in Account Pending Overlay": "Vis administratordetaljer i ventende kontovisning",
"Show All": "",
"Show Less": "",
"Show Model": "",
"Show shortcuts": "Vis snarveier",
"Show your support!": "Vis din støtte!",
@ -1092,6 +1093,7 @@
"Stream Chat Response": "Strømme chat-svar",
"STT Model": "STT-modell",
"STT Settings": "STT-innstillinger",
"Stylized PDF Export": "",
"Subtitle (e.g. about the Roman Empire)": "Undertittel (f.eks. om romerriket)",
"Success": "Suksess",
"Successfully updated.": "Oppdatert.",

View file

@ -959,9 +959,8 @@
"Repeat Penalty (Ollama)": "Herhalingsstraf (Ollama)",
"Reply in Thread": "Antwoord in draad",
"Request Mode": "Request Modus",
"Reranking Engine": "",
"Reranking Model": "Reranking Model",
"Reranking model disabled": "Reranking model uitgeschakeld",
"Reranking model set to \"{{reranking_model}}\"": "Reranking model ingesteld op \"{{reranking_model}}\"",
"Reset": "Herstellen",
"Reset All Models": "Herstel alle modellen",
"Reset Upload Directory": "Herstel Uploadmap",
@ -1069,6 +1068,8 @@
"Show": "Toon",
"Show \"What's New\" modal on login": "Toon \"Wat is nieuw\" bij inloggen",
"Show Admin Details in Account Pending Overlay": "Admin-details weergeven in overlay in afwachting van account",
"Show All": "",
"Show Less": "",
"Show Model": "Toon model",
"Show shortcuts": "Toon snelkoppelingen",
"Show your support!": "Toon je steun",
@ -1092,6 +1093,7 @@
"Stream Chat Response": "Stream chat-antwoord",
"STT Model": "STT Model",
"STT Settings": "STT Instellingen",
"Stylized PDF Export": "",
"Subtitle (e.g. about the Roman Empire)": "Ondertitel (bijv. over de Romeinse Empire)",
"Success": "Succes",
"Successfully updated.": "Succesvol bijgewerkt.",

View file

@ -959,9 +959,8 @@
"Repeat Penalty (Ollama)": "",
"Reply in Thread": "",
"Request Mode": "ਬੇਨਤੀ ਮੋਡ",
"Reranking Engine": "",
"Reranking Model": "ਮਾਡਲ ਮੁੜ ਰੈਂਕਿੰਗ",
"Reranking model disabled": "ਮਾਡਲ ਮੁੜ ਰੈਂਕਿੰਗ ਅਯੋਗ ਕੀਤਾ ਗਿਆ",
"Reranking model set to \"{{reranking_model}}\"": "ਮਾਡਲ ਮੁੜ ਰੈਂਕਿੰਗ ਨੂੰ \"{{reranking_model}}\" 'ਤੇ ਸੈੱਟ ਕੀਤਾ ਗਿਆ",
"Reset": "",
"Reset All Models": "",
"Reset Upload Directory": "",
@ -1069,6 +1068,8 @@
"Show": "ਦਿਖਾਓ",
"Show \"What's New\" modal on login": "",
"Show Admin Details in Account Pending Overlay": "",
"Show All": "",
"Show Less": "",
"Show Model": "",
"Show shortcuts": "ਸ਼ਾਰਟਕਟ ਦਿਖਾਓ",
"Show your support!": "",
@ -1092,6 +1093,7 @@
"Stream Chat Response": "",
"STT Model": "",
"STT Settings": "STT ਸੈਟਿੰਗਾਂ",
"Stylized PDF Export": "",
"Subtitle (e.g. about the Roman Empire)": "ਉਪਸਿਰਲੇਖ (ਉਦਾਹਰਣ ਲਈ ਰੋਮਨ ਸਾਮਰਾਜ ਬਾਰੇ)",
"Success": "ਸਫਲਤਾ",
"Successfully updated.": "ਸਫਲਤਾਪੂਰਵਕ ਅੱਪਡੇਟ ਕੀਤਾ ਗਿਆ।",

View file

@ -959,9 +959,8 @@
"Repeat Penalty (Ollama)": "Powtórzona kara (Ollama)",
"Reply in Thread": "Odpowiedz w wątku",
"Request Mode": "Tryb żądania",
"Reranking Engine": "",
"Reranking Model": "Poprawa rankingu modelu",
"Reranking model disabled": "Ponowny ranking modeli wyłączony",
"Reranking model set to \"{{reranking_model}}\"": "Ponowny ranking modeli ustawiony na \"{{reranking_model}}\".",
"Reset": "Resetuj",
"Reset All Models": "Resetuj wszystkie modele",
"Reset Upload Directory": "Resetuj katalog pobierania",
@ -1069,6 +1068,8 @@
"Show": "Wyświetl",
"Show \"What's New\" modal on login": "Wyświetl okno dialogowe \"What's New\" podczas logowania",
"Show Admin Details in Account Pending Overlay": "Wyświetl szczegóły administratora w okienu informacyjnym o potrzebie zatwierdzenia przez administratora konta użytkownika",
"Show All": "",
"Show Less": "",
"Show Model": "",
"Show shortcuts": "Wyświetl skróty",
"Show your support!": "Wyraź swoje poparcie!",
@ -1092,6 +1093,7 @@
"Stream Chat Response": "Strumieniowanie odpowiedzi z czatu",
"STT Model": "Model STT",
"STT Settings": "Ustawienia STT",
"Stylized PDF Export": "",
"Subtitle (e.g. about the Roman Empire)": "Podtytuł (np. o Imperium Rzymskim)",
"Success": "Sukces",
"Successfully updated.": "Uaktualniono pomyślnie.",

View file

@ -959,9 +959,8 @@
"Repeat Penalty (Ollama)": "",
"Reply in Thread": "",
"Request Mode": "Modo de Solicitação",
"Reranking Engine": "",
"Reranking Model": "Modelo de Reclassificação",
"Reranking model disabled": "Modelo de Reclassificação desativado",
"Reranking model set to \"{{reranking_model}}\"": "Modelo de Reclassificação definido como \"{{reranking_model}}\"",
"Reset": "Redefinir",
"Reset All Models": "",
"Reset Upload Directory": "Redefinir Diretório de Upload",
@ -1069,6 +1068,8 @@
"Show": "Mostrar",
"Show \"What's New\" modal on login": "Mostrar \"O que há de Novo\" no login",
"Show Admin Details in Account Pending Overlay": "Mostrar Detalhes do Administrador na Sobreposição de Conta Pendentes",
"Show All": "",
"Show Less": "",
"Show Model": "",
"Show shortcuts": "Mostrar atalhos",
"Show your support!": "Mostre seu apoio!",
@ -1092,6 +1093,7 @@
"Stream Chat Response": "Stream Resposta do Chat",
"STT Model": "Modelo STT",
"STT Settings": "Configurações STT",
"Stylized PDF Export": "",
"Subtitle (e.g. about the Roman Empire)": "Subtítulo (por exemplo, sobre o Império Romano)",
"Success": "Sucesso",
"Successfully updated.": "Atualizado com sucesso.",

View file

@ -959,9 +959,8 @@
"Repeat Penalty (Ollama)": "",
"Reply in Thread": "",
"Request Mode": "Modo de Pedido",
"Reranking Engine": "",
"Reranking Model": "Modelo de Reranking",
"Reranking model disabled": "Modelo de Reranking desativado",
"Reranking model set to \"{{reranking_model}}\"": "Modelo de Reranking definido como \"{{reranking_model}}\"",
"Reset": "",
"Reset All Models": "",
"Reset Upload Directory": "Limpar Pasta de Carregamento",
@ -1069,6 +1068,8 @@
"Show": "Mostrar",
"Show \"What's New\" modal on login": "",
"Show Admin Details in Account Pending Overlay": "Mostrar Detalhes do Administrador na sobreposição de Conta Pendente",
"Show All": "",
"Show Less": "",
"Show Model": "",
"Show shortcuts": "Mostrar atalhos",
"Show your support!": "",
@ -1092,6 +1093,7 @@
"Stream Chat Response": "",
"STT Model": "Modelo STT",
"STT Settings": "Configurações STT",
"Stylized PDF Export": "",
"Subtitle (e.g. about the Roman Empire)": "Subtítulo (ex.: sobre o Império Romano)",
"Success": "Sucesso",
"Successfully updated.": "Atualizado com sucesso.",

View file

@ -959,9 +959,8 @@
"Repeat Penalty (Ollama)": "",
"Reply in Thread": "",
"Request Mode": "Mod de Cerere",
"Reranking Engine": "",
"Reranking Model": "Model de Rearanjare",
"Reranking model disabled": "Modelul de Rearanjare este dezactivat",
"Reranking model set to \"{{reranking_model}}\"": "Modelul de Rearanjare setat la \"{{reranking_model}}\"",
"Reset": "Resetează",
"Reset All Models": "",
"Reset Upload Directory": "Resetează Directorul de Încărcare",
@ -1069,6 +1068,8 @@
"Show": "Afișează",
"Show \"What's New\" modal on login": "",
"Show Admin Details in Account Pending Overlay": "Afișează Detaliile Administratorului în Suprapunerea Contului În Așteptare",
"Show All": "",
"Show Less": "",
"Show Model": "",
"Show shortcuts": "Afișează scurtături",
"Show your support!": "Arată-ți susținerea!",
@ -1092,6 +1093,7 @@
"Stream Chat Response": "Răspuns Stream Chat",
"STT Model": "Model STT",
"STT Settings": "Setări STT",
"Stylized PDF Export": "",
"Subtitle (e.g. about the Roman Empire)": "Subtitlu (de ex. despre Imperiul Roman)",
"Success": "Succes",
"Successfully updated.": "Actualizat cu succes.",

View file

@ -959,9 +959,8 @@
"Repeat Penalty (Ollama)": "Повторить наказание (Ollama)",
"Reply in Thread": "Ответить в обсуждении",
"Request Mode": "Режим запроса",
"Reranking Engine": "",
"Reranking Model": "Модель реранжирования",
"Reranking model disabled": "Модель реранжирования отключена",
"Reranking model set to \"{{reranking_model}}\"": "Модель реранжирования установлена на \"{{reranking_model}}\"",
"Reset": "Сбросить",
"Reset All Models": "Сбросить все модели",
"Reset Upload Directory": "Сбросить каталог загрузок",
@ -1069,6 +1068,8 @@
"Show": "Показать",
"Show \"What's New\" modal on login": "Показывать окно «Что нового» при входе в систему",
"Show Admin Details in Account Pending Overlay": "Показывать данные администратора в оверлее ожидающей учетной записи",
"Show All": "",
"Show Less": "",
"Show Model": "Показать модель",
"Show shortcuts": "Показать горячие клавиши",
"Show your support!": "Поддержите нас!",
@ -1092,6 +1093,7 @@
"Stream Chat Response": "Потоковый вывод ответа",
"STT Model": "Модель распознавания речи",
"STT Settings": "Настройки распознавания речи",
"Stylized PDF Export": "",
"Subtitle (e.g. about the Roman Empire)": "Подзаголовок (напр., о Римской империи)",
"Success": "Успех",
"Successfully updated.": "Успешно обновлено.",

View file

@ -959,9 +959,8 @@
"Repeat Penalty (Ollama)": "",
"Reply in Thread": "",
"Request Mode": "Režim žiadosti",
"Reranking Engine": "",
"Reranking Model": "Model na prehodnotenie poradia",
"Reranking model disabled": "Model na prehodnotenie poradia je deaktivovaný",
"Reranking model set to \"{{reranking_model}}\"": "Model na prehodnotenie poradia nastavený na \"{{reranking_model}}\"",
"Reset": "režim Reset",
"Reset All Models": "",
"Reset Upload Directory": "Resetovať adresár nahrávania",
@ -1069,6 +1068,8 @@
"Show": "Zobraziť",
"Show \"What's New\" modal on login": "",
"Show Admin Details in Account Pending Overlay": "Zobraziť podrobnosti administrátora v prekryvnom okne s čakajúcim účtom",
"Show All": "",
"Show Less": "",
"Show Model": "",
"Show shortcuts": "Zobraziť klávesové skratky",
"Show your support!": "Vyjadrite svoju podporu!",
@ -1092,6 +1093,7 @@
"Stream Chat Response": "Odozva chatu Stream",
"STT Model": "Model rozpoznávania reči na text (STT)",
"STT Settings": "Nastavenia STT (Rozpoznávanie reči)",
"Stylized PDF Export": "",
"Subtitle (e.g. about the Roman Empire)": "Titulky (napr. o Rímskej ríši)",
"Success": "Úspech",
"Successfully updated.": "Úspešne aktualizované.",

View file

@ -959,9 +959,8 @@
"Repeat Penalty (Ollama)": "",
"Reply in Thread": "",
"Request Mode": "Режим захтева",
"Reranking Engine": "",
"Reranking Model": "Модел поновног рангирања",
"Reranking model disabled": "Модел поновног рангирања онемогућен",
"Reranking model set to \"{{reranking_model}}\"": "Модел поновног рангирања подешен на \"{{reranking_model}}\"",
"Reset": "Поврати",
"Reset All Models": "Поврати све моделе",
"Reset Upload Directory": "",
@ -1069,6 +1068,8 @@
"Show": "Прикажи",
"Show \"What's New\" modal on login": "Прикажи \"Погледај шта је ново\" прозорче при пријави",
"Show Admin Details in Account Pending Overlay": "",
"Show All": "",
"Show Less": "",
"Show Model": "",
"Show shortcuts": "Прикажи пречице",
"Show your support!": "",
@ -1092,6 +1093,7 @@
"Stream Chat Response": "",
"STT Model": "STT модел",
"STT Settings": "STT подешавања",
"Stylized PDF Export": "",
"Subtitle (e.g. about the Roman Empire)": "Поднаслов (нпр. о Римском царству)",
"Success": "Успех",
"Successfully updated.": "Успешно ажурирано.",

View file

@ -959,9 +959,8 @@
"Repeat Penalty (Ollama)": "",
"Reply in Thread": "",
"Request Mode": "Frågeläge",
"Reranking Engine": "",
"Reranking Model": "Reranking modell",
"Reranking model disabled": "Reranking modell inaktiverad",
"Reranking model set to \"{{reranking_model}}\"": "Reranking modell inställd på \"{{reranking_model}}\"",
"Reset": "",
"Reset All Models": "",
"Reset Upload Directory": "Återställ uppladdningskatalog",
@ -1069,6 +1068,8 @@
"Show": "Visa",
"Show \"What's New\" modal on login": "",
"Show Admin Details in Account Pending Overlay": "Visa administratörsinformation till väntande konton",
"Show All": "",
"Show Less": "",
"Show Model": "",
"Show shortcuts": "Visa genvägar",
"Show your support!": "Visa ditt stöd!",
@ -1092,6 +1093,7 @@
"Stream Chat Response": "",
"STT Model": "Tal-till-text-modell",
"STT Settings": "Tal-till-text-inställningar",
"Stylized PDF Export": "",
"Subtitle (e.g. about the Roman Empire)": "Undertext (t.ex. om Romerska Imperiet)",
"Success": "Framgång",
"Successfully updated.": "Uppdaterades framgångsrikt.",

View file

@ -959,9 +959,8 @@
"Repeat Penalty (Ollama)": "",
"Reply in Thread": "",
"Request Mode": "โหมดคำขอ",
"Reranking Engine": "",
"Reranking Model": "จัดอันดับใหม่โมเดล",
"Reranking model disabled": "ปิดการใช้งานโมเดลการจัดอันดับใหม่",
"Reranking model set to \"{{reranking_model}}\"": "ตั้งค่าโมเดลการจัดอันดับใหม่เป็น \"{{reranking_model}}\"",
"Reset": "รีเซ็ต",
"Reset All Models": "",
"Reset Upload Directory": "รีเซ็ตไดเร็กทอรีการอัปโหลด",
@ -1069,6 +1068,8 @@
"Show": "แสดง",
"Show \"What's New\" modal on login": "",
"Show Admin Details in Account Pending Overlay": "แสดงรายละเอียดผู้ดูแลระบบในหน้าจอรอการอนุมัติบัญชี",
"Show All": "",
"Show Less": "",
"Show Model": "",
"Show shortcuts": "แสดงทางลัด",
"Show your support!": "แสดงการสนับสนุนของคุณ!",
@ -1092,6 +1093,7 @@
"Stream Chat Response": "",
"STT Model": "โมเดลแปลงเสียงเป็นข้อความ",
"STT Settings": "การตั้งค่าแปลงเสียงเป็นข้อความ",
"Stylized PDF Export": "",
"Subtitle (e.g. about the Roman Empire)": "คำบรรยาย (เช่น เกี่ยวกับจักรวรรดิโรมัน)",
"Success": "สำเร็จ",
"Successfully updated.": "อัปเดตเรียบร้อยแล้ว",

View file

@ -959,9 +959,8 @@
"Repeat Penalty (Ollama)": "",
"Reply in Thread": "",
"Request Mode": "",
"Reranking Engine": "",
"Reranking Model": "",
"Reranking model disabled": "",
"Reranking model set to \"{{reranking_model}}\"": "",
"Reset": "",
"Reset All Models": "",
"Reset Upload Directory": "",
@ -1069,6 +1068,8 @@
"Show": "",
"Show \"What's New\" modal on login": "",
"Show Admin Details in Account Pending Overlay": "",
"Show All": "",
"Show Less": "",
"Show Model": "",
"Show shortcuts": "",
"Show your support!": "",
@ -1092,6 +1093,7 @@
"Stream Chat Response": "",
"STT Model": "",
"STT Settings": "",
"Stylized PDF Export": "",
"Subtitle (e.g. about the Roman Empire)": "",
"Success": "",
"Successfully updated.": "",

View file

@ -959,9 +959,8 @@
"Repeat Penalty (Ollama)": "",
"Reply in Thread": "Konuya Yanıtla",
"Request Mode": "İstek Modu",
"Reranking Engine": "",
"Reranking Model": "Yeniden Sıralama Modeli",
"Reranking model disabled": "Yeniden sıralama modeli devre dışı bırakıldı",
"Reranking model set to \"{{reranking_model}}\"": "Yeniden sıralama modeli \"{{reranking_model}}\" olarak ayarlandı",
"Reset": "Sıfırla",
"Reset All Models": "Tüm Modelleri Sıfırla",
"Reset Upload Directory": "Yükleme Dizinini Sıfırla",
@ -1069,6 +1068,8 @@
"Show": "Göster",
"Show \"What's New\" modal on login": "Girişte \"Yenilikler\" modalını göster",
"Show Admin Details in Account Pending Overlay": "Yönetici Ayrıntılarını Hesap Bekliyor Ekranında Göster",
"Show All": "",
"Show Less": "",
"Show Model": "",
"Show shortcuts": "Kısayolları göster",
"Show your support!": "Desteğinizi gösterin!",
@ -1092,6 +1093,7 @@
"Stream Chat Response": "SAkış Sohbet Yanıtı",
"STT Model": "STT Modeli",
"STT Settings": "STT Ayarları",
"Stylized PDF Export": "",
"Subtitle (e.g. about the Roman Empire)": "Alt başlık (örn. Roma İmparatorluğu hakkında)",
"Success": "Başarılı",
"Successfully updated.": "Başarıyla güncellendi.",

View file

@ -959,9 +959,8 @@
"Repeat Penalty (Ollama)": "Штраф за повторення (Ollama)",
"Reply in Thread": "Відповісти в потоці",
"Request Mode": "Режим запиту",
"Reranking Engine": "",
"Reranking Model": "Модель переранжування",
"Reranking model disabled": "Модель переранжування вимкнена",
"Reranking model set to \"{{reranking_model}}\"": "Модель переранжування встановлено на \"{{reranking_model}}\"",
"Reset": "Скидання",
"Reset All Models": "Скинути усі моделі",
"Reset Upload Directory": "Скинути каталог завантажень",
@ -1069,6 +1068,8 @@
"Show": "Показати",
"Show \"What's New\" modal on login": "Показати модальне вікно \"Що нового\" під час входу",
"Show Admin Details in Account Pending Overlay": "Відобразити дані адміна у вікні очікування облікового запису",
"Show All": "",
"Show Less": "",
"Show Model": "Показати модель",
"Show shortcuts": "Показати клавіатурні скорочення",
"Show your support!": "Підтримайте нас!",
@ -1092,6 +1093,7 @@
"Stream Chat Response": "Відповідь стрім-чату",
"STT Model": "Модель STT ",
"STT Settings": "Налаштування STT",
"Stylized PDF Export": "",
"Subtitle (e.g. about the Roman Empire)": "Підзаголовок (напр., про Римську імперію)",
"Success": "Успіх",
"Successfully updated.": "Успішно оновлено.",

View file

@ -959,9 +959,8 @@
"Repeat Penalty (Ollama)": "",
"Reply in Thread": "",
"Request Mode": "درخواست کا موڈ",
"Reranking Engine": "",
"Reranking Model": "دوبارہ درجہ بندی کا ماڈل",
"Reranking model disabled": "دوبارہ درجہ بندی کا ماڈل غیر فعال کر دیا گیا",
"Reranking model set to \"{{reranking_model}}\"": "دوبارہ درجہ بندی کا ماڈل \"{{reranking_model}}\" پر مقرر کر دیا گیا ہے",
"Reset": "ری سیٹ",
"Reset All Models": "",
"Reset Upload Directory": "اپلوڈ ڈائریکٹری کو ری سیٹ کریں",
@ -1069,6 +1068,8 @@
"Show": "دکھائیں",
"Show \"What's New\" modal on login": "",
"Show Admin Details in Account Pending Overlay": "اکاؤنٹ پینڈنگ اوورلے میں ایڈمن کی تفصیلات دکھائیں",
"Show All": "",
"Show Less": "",
"Show Model": "",
"Show shortcuts": "شارٹ کٹ دکھائیں",
"Show your support!": "اپنی حمایت دکھائیں!",
@ -1092,6 +1093,7 @@
"Stream Chat Response": "اسٹریم چیٹ جواب",
"STT Model": "ایس ٹی ٹی ماڈل",
"STT Settings": "ایس ٹی ٹی ترتیبات",
"Stylized PDF Export": "",
"Subtitle (e.g. about the Roman Empire)": "ذیلی عنوان (جیسے رومن سلطنت کے بارے میں)",
"Success": "کامیابی",
"Successfully updated.": "کامیابی سے تازہ کاری ہو گئی",

View file

@ -959,9 +959,8 @@
"Repeat Penalty (Ollama)": "Hình phạt Lặp lại (Ollama)",
"Reply in Thread": "Trả lời trong Luồng",
"Request Mode": "Chế độ Yêu cầu",
"Reranking Engine": "",
"Reranking Model": "Reranking Model",
"Reranking model disabled": "Đã tắt mô hình reranking",
"Reranking model set to \"{{reranking_model}}\"": "Reranking model được đặt thành \"{{reranking_model}}\"",
"Reset": "Xóa toàn bộ",
"Reset All Models": "Đặt lại Tất cả Mô hình",
"Reset Upload Directory": "Xóa toàn bộ thư mục Upload",
@ -1069,6 +1068,8 @@
"Show": "Hiển thị",
"Show \"What's New\" modal on login": "Hiển thị cửa sổ \"Có gì mới\" khi đăng nhập",
"Show Admin Details in Account Pending Overlay": "Hiển thị thông tin của Quản trị viên trên màn hình hiển thị Tài khoản đang chờ xử lý",
"Show All": "",
"Show Less": "",
"Show Model": "Hiển thị Mô hình",
"Show shortcuts": "Hiển thị phím tắt",
"Show your support!": "Thể hiện sự ủng hộ của bạn!",
@ -1092,6 +1093,7 @@
"Stream Chat Response": "Truyền trực tiếp Phản hồi Chat",
"STT Model": "Mô hình STT",
"STT Settings": "Cài đặt Nhận dạng Giọng nói",
"Stylized PDF Export": "",
"Subtitle (e.g. about the Roman Empire)": "Phụ đề (ví dụ: về Đế chế La Mã)",
"Success": "Thành công",
"Successfully updated.": "Đã cập nhật thành công.",

View file

@ -105,7 +105,7 @@
"Are you sure you want to delete this channel?": "是否确认删除此频道?",
"Are you sure you want to delete this message?": "是否确认删除此消息?",
"Are you sure you want to unarchive all archived chats?": "是否确认取消所有已归档的对话?",
"Are you sure you want to update this user's role to **{{ROLE}}**?": "",
"Are you sure you want to update this user's role to **{{ROLE}}**?": "您确定要将此用户的角色更新为 **{{ROLE}}** 吗?",
"Are you sure?": "是否确定?",
"Arena Models": "启用竞技场匿名评价模型",
"Artifacts": "Artifacts",
@ -148,7 +148,7 @@
"Bing Search V7 Subscription Key": "Bing 搜索 V7 订阅密钥",
"Bocha Search API Key": "Bocha Search API 密钥",
"Boosting or penalizing specific tokens for constrained responses. Bias values will be clamped between -100 and 100 (inclusive). (Default: none)": "为受限响应提升或惩罚特定标记。偏置值将被限制在 -100 到 100包括两端之间。默认",
"Both Docling OCR Engine and Language(s) must be provided or both left empty.": "",
"Both Docling OCR Engine and Language(s) must be provided or both left empty.": "必须提供 Docling OCR Engine 和语言,或者都留空。",
"Brave Search API Key": "Brave Search API 密钥",
"By {{name}}": "由 {{name}} 提供",
"Bypass Embedding and Retrieval": "绕过嵌入和检索",
@ -159,7 +159,7 @@
"Cancel": "取消",
"Capabilities": "能力",
"Capture": "截图",
"Capture Audio": "",
"Capture Audio": "录制音频",
"Certificate Path": "证书路径",
"Change Password": "更改密码",
"Channel Name": "频道名称",
@ -269,8 +269,8 @@
"Create Knowledge": "创建知识",
"Create new key": "创建新密钥",
"Create new secret key": "创建新安全密钥",
"Create Note": "",
"Create your first note by clicking on the plus button below.": "",
"Create Note": "创建笔记",
"Create your first note by clicking on the plus button below.": "单击下面的加号按钮创建您的第一个笔记。",
"Created at": "创建于",
"Created At": "创建于",
"Created by": "作者",
@ -308,7 +308,7 @@
"Delete function?": "删除函数?",
"Delete Message": "删除消息",
"Delete message?": "删除消息?",
"Delete note?": "",
"Delete note?": "删除笔记?",
"Delete prompt?": "删除提示词?",
"delete this link": "此处删除这个链接",
"Delete tool?": "删除工具?",
@ -364,7 +364,7 @@
"Download Database": "下载数据库",
"Drag and drop a file to upload or select a file to view": "拖动文件上传或选择文件查看",
"Draw": "平局",
"Drop any files here to upload": "",
"Drop any files here to upload": "将任何文件拖放到此处进行上传",
"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "例如 '30s''10m'。有效的时间单位是秒:'s',分:'m',时:'h'。",
"e.g. \"json\" or a JSON schema": "例如 \"json\" 或一个 JSON schema",
"e.g. 60": "例如 '60'",
@ -374,7 +374,7 @@
"e.g. my_filter": "例如my_filter",
"e.g. my_tools": "例如my_tools",
"e.g. Tools for performing various operations": "例如:用于执行各种操作的工具",
"e.g., 3, 4, 5 (leave blank for default)": "",
"e.g., 3, 4, 5 (leave blank for default)": "例如3、4、5留空为默认值",
"e.g., en-US,ja-JP (leave blank for auto-detect)": "例如,'en-US,ja-JP'(留空以便自动检测)",
"e.g., westus (leave blank for eastus)": "",
"Edit": "编辑",
@ -406,7 +406,7 @@
"Enabled": "启用",
"Endpoint URL": "",
"Enforce Temporary Chat": "强制临时聊天",
"Enhance": "",
"Enhance": "增强",
"Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "确保您的 CSV 文件按以下顺序包含 4 列: 姓名、电子邮箱、密码、角色。",
"Enter {{role}} message here": "在此处输入 {{role}} 的对话内容",
"Enter a detail about yourself for your LLMs to recall": "输入一个关于你自己的详细信息,方便你的大语言模型记住这些内容",
@ -423,8 +423,8 @@
"Enter Chunk Size": "输入块大小 (Chunk Size)",
"Enter comma-separated \"token:bias_value\" pairs (example: 5432:100, 413:-100)": "输入以逗号分隔的“token:bias_value”对例如5432:100, 413:-100",
"Enter description": "输入简介描述",
"Enter Docling OCR Engine": "",
"Enter Docling OCR Language(s)": "",
"Enter Docling OCR Engine": "输入 Docling OCR Engine",
"Enter Docling OCR Language(s)": "输入 Docling OCR 语言",
"Enter Docling Server URL": "输入 Docling 服务器 URL",
"Enter Document Intelligence Endpoint": "输入 Document Intelligence 端点",
"Enter Document Intelligence Key": "输入 Document Intelligence 密钥",
@ -451,7 +451,7 @@
"Enter Model ID": "输入模型 ID",
"Enter model tag (e.g. {{modelTag}})": "输入模型标签 (例如:{{modelTag}})",
"Enter Mojeek Search API Key": "输入 Mojeek Search API 密钥",
"Enter New Password": "",
"Enter New Password": "输入新密码",
"Enter Number of Steps (e.g. 50)": "输入步骤数 (Steps) (例如50)",
"Enter Perplexity API Key": "输入 Perplexity API 密钥",
"Enter Playwright Timeout": "输入 Playwright 超时时间",
@ -488,15 +488,15 @@
"Enter Top K Reranker": "输入 Top K Reranker",
"Enter URL (e.g. http://127.0.0.1:7860/)": "输入地址 (例如http://127.0.0.1:7860/)",
"Enter URL (e.g. http://localhost:11434)": "输入地址 (例如http://localhost:11434)",
"Enter Yacy Password": "",
"Enter Yacy URL (e.g. http://yacy.example.com:8090)": "",
"Enter Yacy Username": "",
"Enter Yacy Password": "输入 Yacy 密码",
"Enter Yacy URL (e.g. http://yacy.example.com:8090)": "输入 Yacy URL例如http://yacy.example.com:8090",
"Enter Yacy Username": "输入 Yacy 用户名",
"Enter your current password": "输入当前密码",
"Enter Your Email": "输入您的电子邮箱",
"Enter Your Full Name": "输入您的名称",
"Enter your message": "输入您的消息",
"Enter your name": "输入您的名称",
"Enter Your Name": "",
"Enter Your Name": "输入你的名称",
"Enter your new password": "输入新的密码",
"Enter Your Password": "输入您的密码",
"Enter Your Role": "输入您的权限组",
@ -505,8 +505,8 @@
"Error": "错误",
"ERROR": "错误",
"Error accessing Google Drive: {{error}}": "访问 Google 云端硬盘 出错: {{error}}",
"Error accessing media devices.": "",
"Error starting recording.": "",
"Error accessing media devices.": "访问媒体设备时出错。",
"Error starting recording.": "开始录制时出错。",
"Error uploading file: {{error}}": "上传文件时出错: {{error}}",
"Evaluations": "竞技场评估",
"Exa API Key": "Exa API 密钥",
@ -545,7 +545,7 @@
"Failed to add file.": "添加文件失败。",
"Failed to connect to {{URL}} OpenAPI tool server": "无法连接到 {{URL}} OpenAPI 工具服务器",
"Failed to create API Key.": "无法创建 API 密钥。",
"Failed to delete note": "",
"Failed to delete note": "删除笔记失败",
"Failed to fetch models": "无法获取模型",
"Failed to load file content.": "无法加载文件内容。",
"Failed to read clipboard contents": "无法读取剪贴板内容",
@ -603,12 +603,12 @@
"Gemini API Config": "Gemini API 配置",
"Gemini API Key is required.": "需要 Gemini API 密钥。",
"General": "通用",
"Generate": "",
"Generate": "生成",
"Generate an image": "生成图像",
"Generate Image": "生成图像",
"Generate prompt pair": "生成提示对",
"Generating search query": "生成搜索查询",
"Generating...": "",
"Generating...": "生成中...",
"Get started": "开始使用",
"Get started with {{WEBUI_NAME}}": "开始使用 {{WEBUI_NAME}}",
"Global": "全局",
@ -655,7 +655,7 @@
"Import Config from JSON File": "导入 JSON 文件中的配置信息",
"Import Functions": "导入函数",
"Import Models": "导入模型",
"Import Notes": "",
"Import Notes": "导入笔记",
"Import Presets": "导入预设",
"Import Prompts": "导入提示词",
"Import Tools": "导入工具",
@ -670,7 +670,7 @@
"Instant Auto-Send After Voice Transcription": "语音转录文字后即时自动发送",
"Integration": "集成",
"Interface": "界面",
"Invalid file content": "",
"Invalid file content": "无效的文件内容",
"Invalid file format.": "无效文件格式。",
"Invalid JSON schema": "无效的 JSON schema",
"Invalid Tag": "无效标签",
@ -741,7 +741,7 @@
"Manage Pipelines": "管理 Pipeline",
"Manage Tool Servers": "管理工具服务器",
"March": "三月",
"Max Speakers": "",
"Max Speakers": "最大扬声器数量",
"Max Tokens (num_predict)": "最大 Token 数量 (num_predict)",
"Max Upload Count": "最大上传数量",
"Max Upload Size": "最大上传大小",
@ -793,16 +793,16 @@
"Mojeek Search API Key": "Mojeek Search API 密钥",
"more": "更多",
"More": "更多",
"My Notes": "",
"My Notes": "我的笔记",
"Name": "名称",
"Name your knowledge base": "为您的知识库命名",
"Native": "原生",
"New Chat": "新对话",
"New Folder": "新文件夹",
"New Note": "",
"New Note": "新笔记",
"New Password": "新密码",
"new-channel": "新频道",
"No content": "",
"No content": "没有内容",
"No content found": "未发现内容",
"No content found in file.": "文件中未找到内容",
"No content to speak": "没有内容可朗读",
@ -817,7 +817,7 @@
"No model IDs": "没有模型 ID",
"No models found": "未找到任何模型",
"No models selected": "未选择任何模型",
"No Notes": "",
"No Notes": "没有笔记",
"No results found": "未找到结果",
"No search query generated": "未生成搜索查询",
"No source available": "没有可用来源",
@ -826,7 +826,7 @@
"None": "无",
"Not factually correct": "事实并非如此",
"Not helpful": "无帮助",
"Note deleted successfully": "",
"Note deleted successfully": "笔记删除成功",
"Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "注意:如果设置了最低分数,搜索只会返回分数大于或等于最低分数的文档。",
"Notes": "笔记",
"Notification Sound": "通知提示音",
@ -849,7 +849,7 @@
"Only alphanumeric characters and hyphens are allowed": "只允许使用英文字母,数字 (0-9) 以及连字符 (-)",
"Only alphanumeric characters and hyphens are allowed in the command string.": "命令字符串中只允许使用英文字母,数字 (0-9) 以及连字符 (-)。",
"Only collections can be edited, create a new knowledge base to edit/add documents.": "只能编辑文件集,创建一个新的知识库来编辑/添加文件。",
"Only markdown files are allowed": "",
"Only markdown files are allowed": "仅允许使用 markdown 文件",
"Only select users and groups with permission can access": "只有具有权限的用户和组才能访问",
"Oops! Looks like the URL is invalid. Please double-check and try again.": "此链接似乎为无效链接。请检查后重试。",
"Oops! There are files still uploading. Please wait for the upload to complete.": "稍等!还有文件正在上传。请等待上传完成。",
@ -895,8 +895,8 @@
"Pipelines": "Pipeline",
"Pipelines Not Detected": "未检测到 Pipeline",
"Pipelines Valves": "Pipeline 值",
"Plain text (.md)": "",
"Plain text (.txt)": "TXT 文档 (.txt)",
"Plain text (.md)": "纯文本文档(.md",
"Plain text (.txt)": "纯文本文档 (.txt)",
"Playground": "AI 对话游乐场",
"Playwright Timeout (ms)": "Playwright 超时时间 (ms)",
"Playwright WebSocket URL": "Playwright WebSocket URL",
@ -938,7 +938,7 @@
"Read": "只读",
"Read Aloud": "朗读",
"Reasoning Effort": "推理努力",
"Record": "",
"Record": "录制",
"Record voice": "录音",
"Redirecting you to Open WebUI Community": "正在将您重定向到 OpenWebUI 社区",
"Reduces the probability of generating nonsense. A higher value (e.g. 100) will give more diverse answers, while a lower value (e.g. 10) will be more conservative.": "降低生成无意义内容的概率。较高的值如100将产生更多样化的回答而较低的值如10则更加保守。",
@ -959,9 +959,8 @@
"Repeat Penalty (Ollama)": "重复惩罚 (Ollama)",
"Reply in Thread": "在主题中回复",
"Request Mode": "请求模式",
"Reranking Engine": "",
"Reranking Model": "重排模型",
"Reranking model disabled": "重排模型已禁用",
"Reranking model set to \"{{reranking_model}}\"": "重排模型设置为 \"{{reranking_model}}\"",
"Reset": "重置",
"Reset All Models": "重置所有模型",
"Reset Upload Directory": "重置上传目录",
@ -1008,7 +1007,7 @@
"Searched {{count}} sites": "已搜索 {{count}} 个网站",
"Searching \"{{searchQuery}}\"": "搜索 \"{{searchQuery}}\" 中",
"Searching Knowledge for \"{{searchQuery}}\"": "检索有关 \"{{searchQuery}}\" 的知识中",
"Searching the web...": "",
"Searching the web...": "正在搜索网络...",
"Searxng Query URL": "Searxng 查询 URL",
"See readme.md for instructions": "查看 readme.md 以获取说明",
"See what's new": "查阅最新更新内容",
@ -1069,6 +1068,8 @@
"Show": "显示",
"Show \"What's New\" modal on login": "在登录时显示“更新内容”弹窗",
"Show Admin Details in Account Pending Overlay": "在用户待激活界面中显示管理员邮箱等详细信息",
"Show All": "",
"Show Less": "",
"Show Model": "显示模型",
"Show shortcuts": "显示快捷方式",
"Show your support!": "表达你的支持!",
@ -1092,6 +1093,7 @@
"Stream Chat Response": "以流式返回对话响应",
"STT Model": "语音转文本模型",
"STT Settings": "语音转文本设置",
"Stylized PDF Export": "",
"Subtitle (e.g. about the Roman Empire)": "副标题(例如:关于罗马帝国的副标题)",
"Success": "成功",
"Successfully updated.": "成功更新。",
@ -1211,7 +1213,7 @@
"Unpin": "取消置顶",
"Unravel secrets": "解开秘密",
"Untagged": "无标签",
"Untitled": "",
"Untitled": "无标题",
"Update": "更新",
"Update and Copy Link": "更新和复制链接",
"Update for the latest features and improvements.": "更新来获得最新功能与改进。",
@ -1222,7 +1224,7 @@
"Upgrade to a licensed plan for enhanced capabilities, including custom theming and branding, and dedicated support.": "升级到授权计划以获得增强功能,包括自定义主题与品牌以及专属支持。",
"Upload": "上传",
"Upload a GGUF model": "上传一个 GGUF 模型",
"Upload Audio": "",
"Upload Audio": "上传音频",
"Upload directory": "上传目录",
"Upload files": "上传文件",
"Upload Files": "上传文件",
@ -1296,9 +1298,9 @@
"Write a summary in 50 words that summarizes [topic or keyword].": "用 50 个字写一个总结 [主题或关键词]。",
"Write something...": "单击以键入内容...",
"Write your model template content here": "在此写入模型模板内容",
"Yacy Instance URL": "",
"Yacy Password": "",
"Yacy Username": "",
"Yacy Instance URL": "Yacy Instance URL",
"Yacy Password": "Yacy 密码",
"Yacy Username": "Yacy 用户名",
"Yesterday": "昨天",
"You": "你",
"You are currently using a trial license. Please contact support to upgrade your license.": "当前为试用许可证,请联系支持人员升级许可证。",

View file

@ -959,9 +959,8 @@
"Repeat Penalty (Ollama)": "重複懲罰 (Ollama)",
"Reply in Thread": "在討論串中回覆",
"Request Mode": "請求模式",
"Reranking Engine": "",
"Reranking Model": "重新排序模型",
"Reranking model disabled": "已停用重新排序模型",
"Reranking model set to \"{{reranking_model}}\"": "重新排序模型已設定為 \"{{reranking_model}}\"",
"Reset": "重設",
"Reset All Models": "重設所有模型",
"Reset Upload Directory": "重設上傳目錄",
@ -1069,6 +1068,8 @@
"Show": "顯示",
"Show \"What's New\" modal on login": "登入時顯示「新功能」對話框",
"Show Admin Details in Account Pending Overlay": "在帳號待審覆蓋層中顯示管理員詳細資訊",
"Show All": "",
"Show Less": "",
"Show Model": "顯示模型",
"Show shortcuts": "顯示快捷鍵",
"Show your support!": "表達您的支持!",
@ -1092,6 +1093,7 @@
"Stream Chat Response": "串流式對話回應",
"STT Model": "語音轉文字 (STT) 模型",
"STT Settings": "語音轉文字 (STT) 設定",
"Stylized PDF Export": "",
"Subtitle (e.g. about the Roman Empire)": "副標題(例如:關於羅馬帝國)",
"Success": "成功",
"Successfully updated.": "更新成功。",

View file

@ -10,6 +10,10 @@ const DELIMITER_LIST = [
{ left: '\\begin{equation}', right: '\\end{equation}', display: true }
];
// Defines characters that are allowed to immediately precede or follow a math delimiter.
const ALLOWED_SURROUNDING_CHARS =
'\\s?。,、;!-\\/:-@\\[-`{-~\\p{Script=Han}\\p{Script=Hiragana}\\p{Script=Katakana}\\p{Script=Hangul}';
// const DELIMITER_LIST = [
// { left: '$$', right: '$$', display: false },
// { left: '$', right: '$', display: false },
@ -44,10 +48,13 @@ function generateRegexRules(delimiters) {
// Math formulas can end in special characters
const inlineRule = new RegExp(
`^(${inlinePatterns.join('|')})(?=[\\s?。,!-\/:-@[-\`{-~]|$)`,
`^(${inlinePatterns.join('|')})(?=[${ALLOWED_SURROUNDING_CHARS}]|$)`,
'u'
);
const blockRule = new RegExp(
`^(${blockPatterns.join('|')})(?=[${ALLOWED_SURROUNDING_CHARS}]|$)`,
'u'
);
const blockRule = new RegExp(`^(${blockPatterns.join('|')})(?=[\\s?。,!-\/:-@[-\`{-~]|$)`, 'u');
return { inlineRule, blockRule };
}
@ -91,7 +98,9 @@ function katexStart(src, displayMode: boolean) {
// Check if the delimiter is preceded by a special character.
// If it does, then it's potentially a math formula.
const f = index === 0 || indexSrc.charAt(index - 1).match(/[\s?。,!-\/:-@[-`{-~]/);
const f =
index === 0 ||
indexSrc.charAt(index - 1).match(new RegExp(`[${ALLOWED_SURROUNDING_CHARS}]`, 'u'));
if (f) {
const possibleKatex = indexSrc.substring(index);

View file

@ -6,6 +6,7 @@ class OneDriveConfig {
private static instance: OneDriveConfig;
private clientId: string = '';
private sharepointUrl: string = '';
private sharepointTenantId: string = '';
private msalInstance: PublicClientApplication | null = null;
private currentAuthorityType: 'personal' | 'organizations' = 'personal';
@ -48,6 +49,7 @@ class OneDriveConfig {
const newClientId = config.onedrive?.client_id;
const newSharepointUrl = config.onedrive?.sharepoint_url;
const newSharepointTenantId = config.onedrive?.sharepoint_tenant_id;
if (!newClientId) {
throw new Error('OneDrive configuration is incomplete');
@ -55,6 +57,7 @@ class OneDriveConfig {
this.clientId = newClientId;
this.sharepointUrl = newSharepointUrl;
this.sharepointTenantId = newSharepointTenantId;
}
public async getMsalInstance(
@ -64,7 +67,9 @@ class OneDriveConfig {
if (!this.msalInstance) {
const authorityEndpoint =
this.currentAuthorityType === 'organizations' ? 'common' : 'consumers';
this.currentAuthorityType === 'organizations'
? this.sharepointTenantId || 'common'
: 'consumers';
const msalParams = {
auth: {
authority: `https://login.microsoftonline.com/${authorityEndpoint}`,
@ -89,6 +94,10 @@ class OneDriveConfig {
return this.sharepointUrl;
}
public getSharepointTenantId(): string {
return this.sharepointTenantId;
}
public getBaseUrl(): string {
if (this.currentAuthorityType === 'organizations') {
if (!this.sharepointUrl || this.sharepointUrl === '') {

View file

@ -48,7 +48,6 @@
import NotificationToast from '$lib/components/NotificationToast.svelte';
import AppSidebar from '$lib/components/app/AppSidebar.svelte';
import { chatCompletion } from '$lib/apis/openai';
import { setupSocket } from '$lib/utils/websocket';
setContext('i18n', i18n);
@ -59,6 +58,53 @@
const BREAKPOINT = 768;
const setupSocket = async (enableWebsocket) => {
const _socket = io(`${WEBUI_BASE_URL}` || undefined, {
reconnection: true,
reconnectionDelay: 1000,
reconnectionDelayMax: 5000,
randomizationFactor: 0.5,
path: '/ws/socket.io',
transports: enableWebsocket ? ['websocket'] : ['polling', 'websocket'],
auth: { token: localStorage.token }
});
await socket.set(_socket);
_socket.on('connect_error', (err) => {
console.log('connect_error', err);
});
_socket.on('connect', () => {
console.log('connected', _socket.id);
});
_socket.on('reconnect_attempt', (attempt) => {
console.log('reconnect_attempt', attempt);
});
_socket.on('reconnect_failed', () => {
console.log('reconnect_failed');
});
_socket.on('disconnect', (reason, details) => {
console.log(`Socket ${_socket.id} disconnected due to ${reason}`);
if (details) {
console.log('Additional details:', details);
}
});
_socket.on('user-list', (data) => {
console.log('user-list', data);
activeUserIds.set(data.user_ids);
});
_socket.on('usage', (data) => {
console.log('usage', data);
USAGE_POOL.set(data['models']);
});
};
const executePythonAsWorker = async (id, code, cb) => {
let result = null;
let stdout = null;
@ -515,6 +561,8 @@
await WEBUI_NAME.set(backendConfig.name);
if ($config) {
await setupSocket($config.features?.enable_websocket ?? true);
const currentUrl = `${window.location.pathname}${window.location.search}`;
const encodedUrl = encodeURIComponent(currentUrl);
@ -526,7 +574,6 @@
});
if (sessionUser) {
await setupSocket($config.features?.enable_websocket ?? true);
// Save Session User to Store
$socket.emit('user-join', { auth: { token: sessionUser.token } });

View file

@ -12,7 +12,6 @@
import { WEBUI_NAME, config, user, socket } from '$lib/stores';
import { generateInitialsImage, canvasPixelTest } from '$lib/utils';
import { setupSocket } from '$lib/utils/websocket';
import Spinner from '$lib/components/common/Spinner.svelte';
import OnBoarding from '$lib/components/OnBoarding.svelte';
@ -42,10 +41,6 @@
if (sessionUser.token) {
localStorage.token = sessionUser.token;
}
if (!$socket) {
await setupSocket($config.features?.enable_websocket ?? true);
}
$socket.emit('user-join', { auth: { token: sessionUser.token } });
await user.set(sessionUser);
await config.set(await getBackendConfig());
@ -188,7 +183,7 @@
crossorigin="anonymous"
src="{WEBUI_BASE_URL}/static/splash.png"
class=" w-6 rounded-full"
alt="logo"
alt=""
/>
</div>
</div>
@ -235,7 +230,7 @@
</div>
{#if $config?.onboarding ?? false}
<div class=" mt-1 text-xs font-medium text-gray-500">
<div class="mt-1 text-xs font-medium text-gray-600 dark:text-gray-500">
{$WEBUI_NAME}
{$i18n.t(
'does not make any external connections, and your data stays securely on your locally hosted server.'
@ -248,10 +243,13 @@
<div class="flex flex-col mt-4">
{#if mode === 'signup'}
<div class="mb-2">
<div class=" text-sm font-medium text-left mb-1">{$i18n.t('Name')}</div>
<label for="name" class="text-sm font-medium text-left mb-1 block"
>{$i18n.t('Name')}</label
>
<input
bind:value={name}
type="text"
id="name"
class="my-0.5 w-full text-sm outline-hidden bg-transparent"
autocomplete="name"
placeholder={$i18n.t('Enter Your Full Name')}
@ -262,23 +260,29 @@
{#if mode === 'ldap'}
<div class="mb-2">
<div class=" text-sm font-medium text-left mb-1">{$i18n.t('Username')}</div>
<label for="username" class="text-sm font-medium text-left mb-1 block"
>{$i18n.t('Username')}</label
>
<input
bind:value={ldapUsername}
type="text"
class="my-0.5 w-full text-sm outline-hidden bg-transparent"
autocomplete="username"
name="username"
id="username"
placeholder={$i18n.t('Enter Your Username')}
required
/>
</div>
{:else}
<div class="mb-2">
<div class=" text-sm font-medium text-left mb-1">{$i18n.t('Email')}</div>
<label for="email" class="text-sm font-medium text-left mb-1 block"
>{$i18n.t('Email')}</label
>
<input
bind:value={email}
type="email"
id="email"
class="my-0.5 w-full text-sm outline-hidden bg-transparent"
autocomplete="email"
name="email"
@ -289,11 +293,13 @@
{/if}
<div>
<div class=" text-sm font-medium text-left mb-1">{$i18n.t('Password')}</div>
<label for="password" class="text-sm font-medium text-left mb-1 block"
>{$i18n.t('Password')}</label
>
<input
bind:value={password}
type="password"
id="password"
class="my-0.5 w-full text-sm outline-hidden bg-transparent"
placeholder={$i18n.t('Enter Your Password')}
autocomplete="current-password"

View file

@ -24,7 +24,7 @@
html,
pre {
font-family: -apple-system, BlinkMacSystemFont, 'Inter', ui-sans-serif, system-ui, 'Segoe UI',
Roboto, Ubuntu, Cantarell, 'Noto Sans', sans-serif, 'Helvetica Neue', Arial,
Roboto, Ubuntu, Cantarell, 'Vazirmatn', 'Noto Sans', sans-serif, 'Helvetica Neue', Arial,
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
}

Binary file not shown.