mirror of
https://github.com/open-webui/open-webui.git
synced 2025-12-13 04:45:19 +00:00
commit
ef301aa16b
96 changed files with 3002 additions and 2264 deletions
34
CHANGELOG.md
34
CHANGELOG.md
|
|
@ -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 don’t 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 there’s 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
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
**(
|
||||
|
|
|
|||
58
backend/open_webui/retrieval/models/external.py
Normal file
58
backend/open_webui/retrieval/models/external.py
Normal 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
|
||||
|
|
@ -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}")
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ router = APIRouter()
|
|||
############################
|
||||
|
||||
|
||||
PAGE_ITEM_COUNT = 10
|
||||
PAGE_ITEM_COUNT = 30
|
||||
|
||||
|
||||
@router.get("/", response_model=UserListResponse)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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]:
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -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
74
contribution_stats.py
Normal 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
12
package-lock.json
generated
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -440,8 +440,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
loading = false;
|
||||
await tick();
|
||||
if (!chatIdProp) {
|
||||
loading = false;
|
||||
await tick();
|
||||
}
|
||||
|
||||
showControls.subscribe(async (value) => {
|
||||
if (controlPane && !$mobile) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@
|
|||
title="Video player"
|
||||
frameborder="0"
|
||||
referrerpolicy="strict-origin-when-cross-origin"
|
||||
controls
|
||||
allowfullscreen
|
||||
></video>
|
||||
{:else}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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={() => {
|
||||
|
|
|
|||
|
|
@ -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 2–4k 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`);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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 2–4k 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`);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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.": "تم التحديث بنجاح",
|
||||
|
|
|
|||
|
|
@ -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.": "تم التحديث بنجاح",
|
||||
|
|
|
|||
|
|
@ -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.": "Успешно обновено.",
|
||||
|
|
|
|||
|
|
@ -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.": "সফলভাবে আপডেট হয়েছে",
|
||||
|
|
|
|||
|
|
@ -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.": "ལེགས་པར་གསར་སྒྱུར་བྱས།",
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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.": "Επιτυχώς ενημερώθηκε.",
|
||||
|
|
|
|||
|
|
@ -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.": "",
|
||||
|
|
|
|||
|
|
@ -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.": "",
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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روز شد",
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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.": "עדכון הצלחה.",
|
||||
|
|
|
|||
|
|
@ -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.": "सफलतापूर्वक उत्परिवर्तित।",
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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.": "正常に更新されました。",
|
||||
|
|
|
|||
|
|
@ -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.": "წარმატებით განახლდა.",
|
||||
|
|
|
|||
|
|
@ -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.": "성공적으로 업데이트되었습니다.",
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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.": "ਸਫਲਤਾਪੂਰਵਕ ਅੱਪਡੇਟ ਕੀਤਾ ਗਿਆ।",
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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.": "Успешно обновлено.",
|
||||
|
|
|
|||
|
|
@ -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é.",
|
||||
|
|
|
|||
|
|
@ -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.": "Успешно ажурирано.",
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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.": "อัปเดตเรียบร้อยแล้ว",
|
||||
|
|
|
|||
|
|
@ -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.": "",
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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.": "Успішно оновлено.",
|
||||
|
|
|
|||
|
|
@ -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.": "کامیابی سے تازہ کاری ہو گئی",
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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.": "当前为试用许可证,请联系支持人员升级许可证。",
|
||||
|
|
|
|||
|
|
@ -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.": "更新成功。",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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 === '') {
|
||||
|
|
|
|||
|
|
@ -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 } });
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
|
||||
|
|
|
|||
BIN
static/assets/fonts/Vazirmatn-Variable.ttf
Normal file
BIN
static/assets/fonts/Vazirmatn-Variable.ttf
Normal file
Binary file not shown.
Loading…
Reference in a new issue