Merge pull request #15738 from open-webui/dev
Release / release (push) Waiting to run Details
Deploy to HuggingFace Spaces / check-secret (push) Waiting to run Details
Deploy to HuggingFace Spaces / deploy (push) Blocked by required conditions Details
Create and publish Docker images with specific build args / build-main-image (linux/amd64, ubuntu-latest) (push) Waiting to run Details
Create and publish Docker images with specific build args / build-main-image (linux/arm64, ubuntu-24.04-arm) (push) Waiting to run Details
Create and publish Docker images with specific build args / build-cuda-image (linux/amd64, ubuntu-latest) (push) Waiting to run Details
Create and publish Docker images with specific build args / build-cuda-image (linux/arm64, ubuntu-24.04-arm) (push) Waiting to run Details
Create and publish Docker images with specific build args / build-cuda126-image (linux/amd64, ubuntu-latest) (push) Waiting to run Details
Create and publish Docker images with specific build args / build-cuda126-image (linux/arm64, ubuntu-24.04-arm) (push) Waiting to run Details
Create and publish Docker images with specific build args / build-ollama-image (linux/amd64, ubuntu-latest) (push) Waiting to run Details
Create and publish Docker images with specific build args / build-ollama-image (linux/arm64, ubuntu-24.04-arm) (push) Waiting to run Details
Create and publish Docker images with specific build args / merge-main-images (push) Blocked by required conditions Details
Create and publish Docker images with specific build args / merge-cuda-images (push) Blocked by required conditions Details
Create and publish Docker images with specific build args / merge-cuda126-images (push) Blocked by required conditions Details
Create and publish Docker images with specific build args / merge-ollama-images (push) Blocked by required conditions Details
Python CI / Format Backend (3.11.x) (push) Waiting to run Details
Python CI / Format Backend (3.12.x) (push) Waiting to run Details
Frontend Build / Format & Build Frontend (push) Waiting to run Details
Frontend Build / Frontend Unit Tests (push) Waiting to run Details
Release to PyPI / release (push) Waiting to run Details

0.6.17
This commit is contained in:
Tim Jaeryang Baek 2025-07-19 21:39:46 +04:00 committed by GitHub
commit b249809d2d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
144 changed files with 6833 additions and 3578 deletions

View File

@ -32,7 +32,7 @@ jobs:
node-version: '22'
- name: Install Dependencies
run: npm install
run: npm install --force
- name: Format Frontend
run: npm run format
@ -59,7 +59,7 @@ jobs:
node-version: '22'
- name: Install Dependencies
run: npm ci
run: npm ci --force
- name: Run vitest
run: npm run test:frontend

View File

@ -5,6 +5,53 @@ 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.17] - 2025-07-19
### Added
- 📂 **Dedicated Folder View with Chat List**: Clicking a folder now reveals a brand-new landing page showcasing a list of all chats within that folder, making navigation simpler and giving teams immediate visibility into project-specific conversations.
- 🆕 **Streamlined Folder Creation Modal**: Creating a new folder is now a seamless, unified experience with a dedicated modal that visually and functionally matches the edit folder flow, making workspace organization more intuitive and error-free for all users.
- 🗃️ **Direct File Uploads to Folder Knowledge**: You can now upload files straight to a folders knowledge—empowering you to enrich project spaces by adding resources and documents directly, without the need to pre-create knowledge bases beforehand.
- 🔎 **Chat Preview in Search**: When searching chats, instantly preview results in context without having to open them—making discovery, auditing, and recall dramatically quicker, especially in large, active teams.
- 🖼️ **Image Upload and Inline Insertion in Notes**: Notes now support inserting images directly among your text, letting you create rich, visually structured documentation, brainstorms, or reports in a more natural and engaging way—no more images just as attachments.
- 📱 **Enhanced Note Selection Editing and Q&A**: Select any portion of your notes to either edit just the highlighted part or ask focused questions about that content—streamlining workflows, boosting productivity, and making reviews or AI-powered enhancements more targeted.
- 📝 **Copy Notes as Rich Text**: Copy entire notes—including all formatting, images, and structure—directly as rich text for seamless pasting into emails, reports, or other tools, maintaining clarity and consistency outside the WebUI.
- ⚡ **Fade-In Streaming Text Experience**: Live-generated responses now elegantly fade in as the AI streams them, creating a more natural and visually engaging reading experience; easily toggled off in Interface settings if you prefer static displays.
- 🔄 **Settings for Follow-Up Prompts**: Fine-tune your follow-up prompt experience—with new controls, you can choose to keep them visible or have them inserted directly into the message input instead of auto-submitting, giving you more flexibility and control over your workflow.
- 🔗 **Prompt Variable Documentation Quick Link**: Access documentation for prompt variables in one click from the prompt editor modal—shortening the learning curve and making advanced prompt-building more accessible.
- 📈 **Active and Total User Metrics for Telemetry**: Gain valuable insights into usage patterns and platform engagement with new metrics tracking active and total users—enhancing auditability and planning for large organizations.
- 🏷️ **Traceability with Log Trace and Span IDs**: Each log entry now carries detailed trace and span IDs, making it much easier for admins to pinpoint and resolve issues across distributed systems or in complex troubleshooting.
- 👥 **User Group Add/Remove Endpoints**: Effortlessly add or remove users from groups with new, improved endpoints—giving admins and team leads faster, clearer control over collaboration and permissions.
- ⚙️ **Note Settings and Controls Streamlined**: The main “Settings” for notes are now simply called “Controls”, and note files now reside in a dedicated controls section, decluttering navigation and making it easier to find and configure note-related options.
- 🚀 **Faster Admin User Page Loads**: The user list endpoint for admins has been optimized to exclude heavy profile images, speeding up load times for large teams and reducing waiting during administrative tasks.
- 📡 **Chat ID Header Forwarding**: Ollama and OpenAI router requests now include the chat ID in request headers, enabling better request correlation and debugging capabilities across AI model integrations.
- 🧠 **Enhanced Reasoning Tag Processing**: Improved and expanded reasoning tag parsing to handle various tag formats more robustly, including standard XML-style tags and custom delimiters, ensuring better AI reasoning transparency and debugging capabilities.
- 🔐 **OAuth Token Endpoint Authentication Method**: Added configurable OAuth token endpoint authentication method support, providing enhanced flexibility and security options for enterprise OAuth integrations and identity provider compatibility.
- 🛡️ **Redis Sentinel High Availability Support**: Comprehensive Redis Sentinel failover implementation with automatic master discovery, intelligent retry logic for connection failures, and seamless operation during master node outages—eliminating single points of failure and ensuring continuous service availability in production deployments.
- 🌐 **Localization & Internationalization Improvements**: Refined and expanded translations for Simplified Chinese, Traditional Chinese, French, German, Korean, and Polish, ensuring a more fluent and native experience for global users across all supported languages.
### Fixed
- 🏷️ **Hybrid Search Functionality Restored**: Hybrid search now works seamlessly again—enabling more accurate, relevant, and comprehensive knowledge discovery across all RAG-powered workflows.
- 🚦 **Note Chat - Edit Button Disabled During AI Generation**: The edit button when chatting with a note is now disabled while the AI is responding—preventing accidental edits and ensuring workflow clarity during chat sessions.
- 🧹 **Cleaner Database Credentials**: Database connection no longer duplicates @ in credentials, preventing potential connection issues and ensuring smoother, more reliable integrations.
- 🧑‍💻 **File Deletion Now Removes Related Vector Data**: When files are deleted from storage, they are now purged from the vector database as well, ensuring clean data management and preventing clutter or stale search results.
- 📁 **Files Modal Translation Issues Fixed**: All modal dialog strings—including “Using Entire Document” and “Using Focused Retrieval”—are now fully translated for a more consistent and localized UI experience.
- 🚫 **Drag-and-Drop File Upload Disabled for Unsupported Models**: File upload by drag-and-drop is disabled when using models that do not support attachments—removing confusion and preventing workflow interruptions.
- 🔑 **Ollama Tool Calls Now Reliable**: Fixed issues with Ollama-based tool calls, ensuring uninterrupted AI augmentation and tool use for every chat.
- 📄 **MIME Type Help String Correction**: Cleaned up mimetype help text by removing extraneous characters, providing clearer guidance for file upload configurations.
- 📝 **Note Editor Permission Fix**: Removed unnecessary admin-only restriction from note chat functionality, allowing all authorized users to access note editing features as intended.
- 📋 **Chat Sources Handling Improved**: Fixed sources handling logic to prevent duplicate source assignments in chat messages, ensuring cleaner and more accurate source attribution during conversations.
- 😀 **Emoji Generation Error Handling**: Improved error handling in audio router and fixed metadata structure for emoji generation tasks, preventing crashes and ensuring more reliable emoji generation functionality.
- 🔒 **Folder System Prompt Permission Enforcement**: System prompt fields in folder edit modal are now properly hidden for users without system prompt permissions, ensuring consistent security policy enforcement across all folder management interfaces.
- 🌐 **WebSocket Redis Lock Timeout Type Conversion**: Fixed proper integer type conversion for WebSocket Redis lock timeout configuration with robust error handling, preventing potential configuration errors and ensuring stable WebSocket connections.
- 📦 **PostHog Dependency Added**: Added PostHog 5.4.0 library to resolve ChromaDB compatibility issues, ensuring stable vector database operations and preventing library version conflicts during deployment.
### Changed
- 👀 **Tiptap Editor Upgraded to v3**: The underlying rich text editor has been updated for future-proofing, though some supporting libraries remain on v2 for compatibility. For now, please install dependencies using 'npm install --force' to avoid installation errors.
- 🚫 **Removed Redundant or Unused Strings and Elements**: Miscellaneous unused, duplicate, or obsolete code and translations have been cleaned up to maintain a streamlined and high-performance experience.
## [0.6.16] - 2025-07-14
### Added

View File

@ -30,7 +30,7 @@ WORKDIR /app
RUN apk add --no-cache git
COPY package.json package-lock.json ./
RUN npm ci
RUN npm ci --force
COPY . .
ENV APP_BUILD_HASH=${BUILD_HASH}

53
LICENSE_HISTORY Normal file
View File

@ -0,0 +1,53 @@
All code and materials created before commit `60d84a3aae9802339705826e9095e272e3c83623` are subject to the following copyright and license:
Copyright (c) 2023-2025 Timothy Jaeryang Baek
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
All code and materials created before commit `a76068d69cd59568b920dfab85dc573dbbb8f131` are subject to the following copyright and license:
MIT License
Copyright (c) 2023 Timothy Jaeryang Baek
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -445,6 +445,12 @@ OAUTH_TIMEOUT = PersistentConfig(
os.environ.get("OAUTH_TIMEOUT", ""),
)
OAUTH_TOKEN_ENDPOINT_AUTH_METHOD = PersistentConfig(
"OAUTH_TOKEN_ENDPOINT_AUTH_METHOD",
"oauth.oidc.token_endpoint_auth_method",
os.environ.get("OAUTH_TOKEN_ENDPOINT_AUTH_METHOD", None),
)
OAUTH_CODE_CHALLENGE_METHOD = PersistentConfig(
"OAUTH_CODE_CHALLENGE_METHOD",
"oauth.oidc.code_challenge_method",
@ -636,6 +642,13 @@ def load_oauth_providers():
def oidc_oauth_register(client: OAuth):
client_kwargs = {
"scope": OAUTH_SCOPES.value,
**(
{
"token_endpoint_auth_method": OAUTH_TOKEN_ENDPOINT_AUTH_METHOD.value
}
if OAUTH_TOKEN_ENDPOINT_AUTH_METHOD.value
else {}
),
**(
{"timeout": int(OAUTH_TIMEOUT.value)} if OAUTH_TIMEOUT.value else {}
),
@ -676,6 +689,17 @@ load_oauth_providers()
STATIC_DIR = Path(os.getenv("STATIC_DIR", OPEN_WEBUI_DIR / "static")).resolve()
try:
if STATIC_DIR.exists():
for item in STATIC_DIR.iterdir():
if item.is_file() or item.is_symlink():
try:
item.unlink()
except Exception as e:
pass
except Exception as e:
pass
for file_path in (FRONTEND_BUILD_DIR / "static").glob("**/*"):
if file_path.is_file():
target_path = STATIC_DIR / file_path.relative_to(

View File

@ -276,9 +276,6 @@ if DATABASE_USER:
DATABASE_CRED += f"{DATABASE_USER}"
if DATABASE_PASSWORD:
DATABASE_CRED += f":{DATABASE_PASSWORD}"
if DATABASE_CRED:
DATABASE_CRED += "@"
DB_VARS = {
"db_type": DATABASE_TYPE,
@ -352,6 +349,15 @@ REDIS_KEY_PREFIX = os.environ.get("REDIS_KEY_PREFIX", "open-webui")
REDIS_SENTINEL_HOSTS = os.environ.get("REDIS_SENTINEL_HOSTS", "")
REDIS_SENTINEL_PORT = os.environ.get("REDIS_SENTINEL_PORT", "26379")
# Maximum number of retries for Redis operations when using Sentinel fail-over
REDIS_SENTINEL_MAX_RETRY_COUNT = os.environ.get("REDIS_SENTINEL_MAX_RETRY_COUNT", "2")
try:
REDIS_SENTINEL_MAX_RETRY_COUNT = int(REDIS_SENTINEL_MAX_RETRY_COUNT)
if REDIS_SENTINEL_MAX_RETRY_COUNT < 1:
REDIS_SENTINEL_MAX_RETRY_COUNT = 2
except ValueError:
REDIS_SENTINEL_MAX_RETRY_COUNT = 2
####################################
# UVICORN WORKERS
####################################
@ -450,7 +456,13 @@ ENABLE_WEBSOCKET_SUPPORT = (
WEBSOCKET_MANAGER = os.environ.get("WEBSOCKET_MANAGER", "")
WEBSOCKET_REDIS_URL = os.environ.get("WEBSOCKET_REDIS_URL", REDIS_URL)
WEBSOCKET_REDIS_LOCK_TIMEOUT = os.environ.get("WEBSOCKET_REDIS_LOCK_TIMEOUT", 60)
websocket_redis_lock_timeout = os.environ.get("WEBSOCKET_REDIS_LOCK_TIMEOUT", "60")
try:
WEBSOCKET_REDIS_LOCK_TIMEOUT = int(websocket_redis_lock_timeout)
except ValueError:
WEBSOCKET_REDIS_LOCK_TIMEOUT = 60
WEBSOCKET_SENTINEL_HOSTS = os.environ.get("WEBSOCKET_SENTINEL_HOSTS", "")

View File

@ -63,7 +63,7 @@ class FolderForm(BaseModel):
class FolderTable:
def insert_new_folder(
self, user_id: str, name: str, parent_id: Optional[str] = None
self, user_id: str, form_data: FolderForm, parent_id: Optional[str] = None
) -> Optional[FolderModel]:
with get_db() as db:
id = str(uuid.uuid4())
@ -71,7 +71,7 @@ class FolderTable:
**{
"id": id,
"user_id": user_id,
"name": name,
**(form_data.model_dump(exclude_unset=True) or {}),
"parent_id": parent_id,
"created_at": int(time.time()),
"updated_at": int(time.time()),

View File

@ -83,10 +83,14 @@ class GroupForm(BaseModel):
permissions: Optional[dict] = None
class GroupUpdateForm(GroupForm):
class UserIdsForm(BaseModel):
user_ids: Optional[list[str]] = None
class GroupUpdateForm(GroupForm, UserIdsForm):
pass
class GroupTable:
def insert_new_group(
self, user_id: str, form_data: GroupForm
@ -275,5 +279,53 @@ class GroupTable:
log.exception(e)
return False
def add_users_to_group(
self, id: str, user_ids: Optional[list[str]] = None
) -> Optional[GroupModel]:
try:
with get_db() as db:
group = db.query(Group).filter_by(id=id).first()
if not group:
return None
if not group.user_ids:
group.user_ids = []
for user_id in user_ids:
if user_id not in group.user_ids:
group.user_ids.append(user_id)
group.updated_at = int(time.time())
db.commit()
db.refresh(group)
return GroupModel.model_validate(group)
except Exception as e:
log.exception(e)
return None
def remove_users_from_group(
self, id: str, user_ids: Optional[list[str]] = None
) -> Optional[GroupModel]:
try:
with get_db() as db:
group = db.query(Group).filter_by(id=id).first()
if not group:
return None
if not group.user_ids:
return GroupModel.model_validate(group)
for user_id in user_ids:
if user_id in group.user_ids:
group.user_ids.remove(user_id)
group.updated_at = int(time.time())
db.commit()
db.refresh(group)
return GroupModel.model_validate(group)
except Exception as e:
log.exception(e)
return None
Groups = GroupTable()

View File

@ -74,6 +74,18 @@ class UserListResponse(BaseModel):
total: int
class UserInfoResponse(BaseModel):
id: str
name: str
email: str
role: str
class UserInfoListResponse(BaseModel):
users: list[UserInfoResponse]
total: int
class UserResponse(BaseModel):
id: str
name: str

View File

@ -611,6 +611,9 @@ def get_sources_from_items(
elif item.get("collection_name"):
# Direct Collection Name
collection_names.append(item["collection_name"])
elif item.get("collection_names"):
# Collection Names List
collection_names.extend(item["collection_names"])
# If query_result is None
# Fallback to collection names and vector search the collections

View File

@ -376,9 +376,13 @@ async def speech(request: Request, user=Depends(get_verified_user)):
if r is not None:
status_code = r.status
res = await r.json()
if "error" in res:
detail = f"External: {res['error'].get('message', '')}"
try:
res = await r.json()
if "error" in res:
detail = f"External: {res['error']}"
except Exception:
detail = f"External: {e}"
raise HTTPException(
status_code=status_code,

View File

@ -39,13 +39,21 @@ router = APIRouter()
async def get_session_user_chat_list(
user=Depends(get_verified_user), page: Optional[int] = None
):
if page is not None:
limit = 60
skip = (page - 1) * limit
try:
if page is not None:
limit = 60
skip = (page - 1) * limit
return Chats.get_chat_title_id_list_by_user_id(user.id, skip=skip, limit=limit)
else:
return Chats.get_chat_title_id_list_by_user_id(user.id)
return Chats.get_chat_title_id_list_by_user_id(
user.id, skip=skip, limit=limit
)
else:
return Chats.get_chat_title_id_list_by_user_id(user.id)
except Exception as e:
log.exception(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
)
############################

View File

@ -21,6 +21,7 @@ from fastapi import (
from fastapi.responses import FileResponse, StreamingResponse
from open_webui.constants import ERROR_MESSAGES
from open_webui.env import SRC_LOG_LEVELS
from open_webui.retrieval.vector.factory import VECTOR_DB_CLIENT
from open_webui.models.users import Users
from open_webui.models.files import (
@ -286,6 +287,7 @@ async def delete_all_files(user=Depends(get_admin_user)):
if result:
try:
Storage.delete_all_files()
VECTOR_DB_CLIENT.reset()
except Exception as e:
log.exception(e)
log.error("Error deleting files")
@ -603,12 +605,12 @@ async def delete_file_by_id(id: str, user=Depends(get_verified_user)):
or user.role == "admin"
or has_access_to_file(id, "write", user)
):
# We should add Chroma cleanup here
result = Files.delete_file_by_id(id)
if result:
try:
Storage.delete_file(file.path)
VECTOR_DB_CLIENT.delete(collection_name=f"file-{id}")
except Exception as e:
log.exception(e)
log.error("Error deleting files")

View File

@ -49,7 +49,7 @@ async def get_folders(user=Depends(get_verified_user)):
**folder.model_dump(),
"items": {
"chats": [
{"title": chat.title, "id": chat.id}
{"title": chat.title, "id": chat.id, "updated_at": chat.updated_at}
for chat in Chats.get_chats_by_folder_id_and_user_id(
folder.id, user.id
)
@ -78,7 +78,7 @@ def create_folder(form_data: FolderForm, user=Depends(get_verified_user)):
)
try:
folder = Folders.insert_new_folder(user.id, form_data.name)
folder = Folders.insert_new_folder(user.id, form_data)
return folder
except Exception as e:
log.exception(e)

View File

@ -9,6 +9,7 @@ from open_webui.models.groups import (
GroupForm,
GroupUpdateForm,
GroupResponse,
UserIdsForm,
)
from open_webui.config import CACHE_DIR
@ -107,6 +108,56 @@ async def update_group_by_id(
)
############################
# AddUserToGroupByUserIdAndGroupId
############################
@router.post("/id/{id}/users/add", response_model=Optional[GroupResponse])
async def add_user_to_group(
id: str, form_data: UserIdsForm, user=Depends(get_admin_user)
):
try:
if form_data.user_ids:
form_data.user_ids = Users.get_valid_user_ids(form_data.user_ids)
group = Groups.add_users_to_group(id, form_data.user_ids)
if group:
return group
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error adding users to group"),
)
except Exception as e:
log.exception(f"Error adding users to group {id}: {e}")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
)
@router.post("/id/{id}/users/remove", response_model=Optional[GroupResponse])
async def remove_users_from_group(
id: str, form_data: UserIdsForm, user=Depends(get_admin_user)
):
try:
group = Groups.remove_users_from_group(id, form_data.user_ids)
if group:
return group
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error removing users from group"),
)
except Exception as e:
log.exception(f"Error removing users from group {id}: {e}")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
)
############################
# DeleteGroupById
############################

View File

@ -6,6 +6,9 @@ from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Request, status, BackgroundTasks
from pydantic import BaseModel
from open_webui.socket.main import sio
from open_webui.models.users import Users, UserResponse
from open_webui.models.notes import Notes, NoteModel, NoteForm, NoteUserResponse
@ -170,6 +173,12 @@ async def update_note_by_id(
try:
note = Notes.update_note_by_id(id, form_data)
await sio.emit(
"note-events",
note.model_dump(),
to=f"note:{note.id}",
)
return note
except Exception as e:
log.exception(e)

View File

@ -124,6 +124,7 @@ async def send_post_request(
key: Optional[str] = None,
content_type: Optional[str] = None,
user: UserModel = None,
metadata: Optional[dict] = None,
):
r = None
@ -144,6 +145,11 @@ async def send_post_request(
"X-OpenWebUI-User-Id": user.id,
"X-OpenWebUI-User-Email": user.email,
"X-OpenWebUI-User-Role": user.role,
**(
{"X-OpenWebUI-Chat-Id": metadata.get("chat_id")}
if metadata and metadata.get("chat_id")
else {}
),
}
if ENABLE_FORWARD_USER_INFO_HEADERS and user
else {}
@ -184,7 +190,6 @@ async def send_post_request(
)
else:
res = await r.json()
await cleanup_response(r, session)
return res
except HTTPException as e:
@ -196,6 +201,9 @@ async def send_post_request(
status_code=r.status if r else 500,
detail=detail if e else "Open WebUI: Server Connection Error",
)
finally:
if not stream:
await cleanup_response(r, session)
def get_api_key(idx, url, configs):
@ -1363,6 +1371,7 @@ async def generate_chat_completion(
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
content_type="application/x-ndjson",
user=user,
metadata=metadata,
)
@ -1401,6 +1410,8 @@ async def generate_openai_completion(
url_idx: Optional[int] = None,
user=Depends(get_verified_user),
):
metadata = form_data.pop("metadata", None)
try:
form_data = OpenAICompletionForm(**form_data)
except Exception as e:
@ -1466,6 +1477,7 @@ async def generate_openai_completion(
stream=payload.get("stream", False),
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
user=user,
metadata=metadata,
)
@ -1547,6 +1559,7 @@ async def generate_openai_chat_completion(
stream=payload.get("stream", False),
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
user=user,
metadata=metadata,
)

View File

@ -822,6 +822,11 @@ async def generate_chat_completion(
"X-OpenWebUI-User-Id": user.id,
"X-OpenWebUI-User-Email": user.email,
"X-OpenWebUI-User-Role": user.role,
**(
{"X-OpenWebUI-Chat-Id": metadata.get("chat_id")}
if metadata and metadata.get("chat_id")
else {}
),
}
if ENABLE_FORWARD_USER_INFO_HEADERS
else {}
@ -893,10 +898,8 @@ async def generate_chat_completion(
detail=detail if detail else "Open WebUI: Server Connection Error",
)
finally:
if not streaming and session:
if r:
r.close()
await session.close()
if not streaming:
await cleanup_response(r, session)
async def embeddings(request: Request, form_data: dict, user):
@ -975,10 +978,8 @@ async def embeddings(request: Request, form_data: dict, user):
detail=detail if detail else "Open WebUI: Server Connection Error",
)
finally:
if not streaming and session:
if r:
r.close()
await session.close()
if not streaming:
await cleanup_response(r, session)
@router.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
@ -1074,7 +1075,5 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
detail=detail if detail else "Open WebUI: Server Connection Error",
)
finally:
if not streaming and session:
if r:
r.close()
await session.close()
if not streaming:
await cleanup_response(r, session)

View File

@ -815,7 +815,11 @@ async def update_rag_config(
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
request.app.state.config.RAG_RERANKING_MODEL = (
form_data.RAG_RERANKING_MODEL
if form_data.RAG_RERANKING_MODEL is not None
else request.app.state.config.RAG_RERANKING_MODEL
)
try:
request.app.state.rf = get_rf(
@ -2050,11 +2054,13 @@ def query_doc_handler(
),
k=form_data.k if form_data.k else request.app.state.config.TOP_K,
reranking_function=(
lambda sentences: (
request.app.state.RERANKING_FUNCTION(sentences, user=user)
if request.app.state.RERANKING_FUNCTION
else None
(
lambda sentences: request.app.state.RERANKING_FUNCTION(
sentences, user=user
)
)
if request.app.state.RERANKING_FUNCTION
else None
),
k_reranker=form_data.k_reranker
or request.app.state.config.TOP_K_RERANKER,
@ -2112,8 +2118,14 @@ def query_collection_handler(
query, prefix=prefix, user=user
),
k=form_data.k if form_data.k else request.app.state.config.TOP_K,
reranking_function=lambda sentences: request.app.state.RERANKING_FUNCTION(
sentences, user=user
reranking_function=(
(
lambda sentences: request.app.state.RERANKING_FUNCTION(
sentences, user=user
)
)
if request.app.state.RERANKING_FUNCTION
else None
),
k_reranker=form_data.k_reranker
or request.app.state.config.TOP_K_RERANKER,

View File

@ -695,11 +695,11 @@ async def generate_emoji(
"max_completion_tokens": 4,
}
),
"chat_id": form_data.get("chat_id", None),
"metadata": {
**(request.state.metadata if hasattr(request.state, "metadata") else {}),
"task": str(TASKS.EMOJI_GENERATION),
"task_body": form_data,
"chat_id": form_data.get("chat_id", None),
},
}

View File

@ -7,6 +7,7 @@ from open_webui.models.chats import Chats
from open_webui.models.users import (
UserModel,
UserListResponse,
UserInfoListResponse,
UserRoleUpdateForm,
Users,
UserSettings,
@ -83,7 +84,7 @@ async def get_users(
return Users.get_users(filter=filter, skip=skip, limit=limit)
@router.get("/all", response_model=UserListResponse)
@router.get("/all", response_model=UserInfoListResponse)
async def get_all_users(
user=Depends(get_admin_user),
):

View File

@ -316,6 +316,37 @@ async def join_channel(sid, data):
await sio.enter_room(sid, f"channel:{channel.id}")
@sio.on("join-note")
async def join_note(sid, data):
auth = data["auth"] if "auth" in data else None
if not auth or "token" not in auth:
return
token_data = decode_token(auth["token"])
if token_data is None or "id" not in token_data:
return
user = Users.get_user_by_id(token_data["id"])
if not user:
return
note = Notes.get_note_by_id(data["note_id"])
if not note:
log.error(f"Note {data['note_id']} not found for user {user.id}")
return
if (
user.role != "admin"
and user.id != note.user_id
and not has_access(user.id, type="read", access_control=note.access_control)
):
log.error(f"User {user.id} does not have access to note {data['note_id']}")
return
log.debug(f"Joining note {note.id} for user {user.id}")
await sio.enter_room(sid, f"note:{note.id}")
@sio.on("channel-events")
async def channel_events(sid, data):
room = f"channel:{data['channel_id']}"
@ -450,7 +481,7 @@ async def yjs_document_state(sid, data):
room = f"doc_{document_id}"
active_session_ids = get_session_ids_from_room(room)
print(active_session_ids)
if sid not in active_session_ids:
log.warning(f"Session {sid} not in room {room}. Cannot send state.")
return
@ -520,7 +551,8 @@ async def yjs_document_update(sid, data):
document_id, data.get("data", {}), SESSION_POOL.get(sid)
)
await create_task(REDIS, debounced_save(), document_id)
if data.get("data"):
await create_task(REDIS, debounced_save(), document_id)
except Exception as e:
log.error(f"Error in yjs_document_update: {e}")

View File

@ -0,0 +1,793 @@
import pytest
from unittest.mock import Mock, patch, AsyncMock
import redis
from open_webui.utils.redis import (
SentinelRedisProxy,
parse_redis_service_url,
get_redis_connection,
get_sentinels_from_env,
MAX_RETRY_COUNT,
)
import inspect
class TestSentinelRedisProxy:
"""Test Redis Sentinel failover functionality"""
def test_parse_redis_service_url_valid(self):
"""Test parsing valid Redis service URL"""
url = "redis://user:pass@mymaster:6379/0"
result = parse_redis_service_url(url)
assert result["username"] == "user"
assert result["password"] == "pass"
assert result["service"] == "mymaster"
assert result["port"] == 6379
assert result["db"] == 0
def test_parse_redis_service_url_defaults(self):
"""Test parsing Redis service URL with defaults"""
url = "redis://mymaster"
result = parse_redis_service_url(url)
assert result["username"] is None
assert result["password"] is None
assert result["service"] == "mymaster"
assert result["port"] == 6379
assert result["db"] == 0
def test_parse_redis_service_url_invalid_scheme(self):
"""Test parsing invalid URL scheme"""
with pytest.raises(ValueError, match="Invalid Redis URL scheme"):
parse_redis_service_url("http://invalid")
def test_get_sentinels_from_env(self):
"""Test parsing sentinel hosts from environment"""
hosts = "sentinel1,sentinel2,sentinel3"
port = "26379"
result = get_sentinels_from_env(hosts, port)
expected = [("sentinel1", 26379), ("sentinel2", 26379), ("sentinel3", 26379)]
assert result == expected
def test_get_sentinels_from_env_empty(self):
"""Test empty sentinel hosts"""
result = get_sentinels_from_env(None, "26379")
assert result == []
@patch("redis.sentinel.Sentinel")
def test_sentinel_redis_proxy_sync_success(self, mock_sentinel_class):
"""Test successful sync operation with SentinelRedisProxy"""
mock_sentinel = Mock()
mock_master = Mock()
mock_master.get.return_value = "test_value"
mock_sentinel.master_for.return_value = mock_master
proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=False)
# Test attribute access
get_method = proxy.__getattr__("get")
result = get_method("test_key")
assert result == "test_value"
mock_sentinel.master_for.assert_called_with("mymaster")
mock_master.get.assert_called_with("test_key")
@patch("redis.sentinel.Sentinel")
@pytest.mark.asyncio
async def test_sentinel_redis_proxy_async_success(self, mock_sentinel_class):
"""Test successful async operation with SentinelRedisProxy"""
mock_sentinel = Mock()
mock_master = Mock()
mock_master.get = AsyncMock(return_value="test_value")
mock_sentinel.master_for.return_value = mock_master
proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=True)
# Test async attribute access
get_method = proxy.__getattr__("get")
result = await get_method("test_key")
assert result == "test_value"
mock_sentinel.master_for.assert_called_with("mymaster")
mock_master.get.assert_called_with("test_key")
@patch("redis.sentinel.Sentinel")
def test_sentinel_redis_proxy_failover_retry(self, mock_sentinel_class):
"""Test retry mechanism during failover"""
mock_sentinel = Mock()
mock_master = Mock()
# First call fails, second succeeds
mock_master.get.side_effect = [
redis.exceptions.ConnectionError("Master down"),
"test_value",
]
mock_sentinel.master_for.return_value = mock_master
proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=False)
get_method = proxy.__getattr__("get")
result = get_method("test_key")
assert result == "test_value"
assert mock_master.get.call_count == 2
@patch("redis.sentinel.Sentinel")
def test_sentinel_redis_proxy_max_retries_exceeded(self, mock_sentinel_class):
"""Test failure after max retries exceeded"""
mock_sentinel = Mock()
mock_master = Mock()
# All calls fail
mock_master.get.side_effect = redis.exceptions.ConnectionError("Master down")
mock_sentinel.master_for.return_value = mock_master
proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=False)
get_method = proxy.__getattr__("get")
with pytest.raises(redis.exceptions.ConnectionError):
get_method("test_key")
assert mock_master.get.call_count == MAX_RETRY_COUNT
@patch("redis.sentinel.Sentinel")
def test_sentinel_redis_proxy_readonly_error_retry(self, mock_sentinel_class):
"""Test retry on ReadOnlyError"""
mock_sentinel = Mock()
mock_master = Mock()
# First call gets ReadOnlyError (old master), second succeeds (new master)
mock_master.get.side_effect = [
redis.exceptions.ReadOnlyError("Read only"),
"test_value",
]
mock_sentinel.master_for.return_value = mock_master
proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=False)
get_method = proxy.__getattr__("get")
result = get_method("test_key")
assert result == "test_value"
assert mock_master.get.call_count == 2
@patch("redis.sentinel.Sentinel")
def test_sentinel_redis_proxy_factory_methods(self, mock_sentinel_class):
"""Test factory methods are passed through directly"""
mock_sentinel = Mock()
mock_master = Mock()
mock_pipeline = Mock()
mock_master.pipeline.return_value = mock_pipeline
mock_sentinel.master_for.return_value = mock_master
proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=False)
# Factory methods should be passed through without wrapping
pipeline_method = proxy.__getattr__("pipeline")
result = pipeline_method()
assert result == mock_pipeline
mock_master.pipeline.assert_called_once()
@patch("redis.sentinel.Sentinel")
@patch("redis.from_url")
def test_get_redis_connection_with_sentinel(
self, mock_from_url, mock_sentinel_class
):
"""Test getting Redis connection with Sentinel"""
mock_sentinel = Mock()
mock_sentinel_class.return_value = mock_sentinel
sentinels = [("sentinel1", 26379), ("sentinel2", 26379)]
redis_url = "redis://user:pass@mymaster:6379/0"
result = get_redis_connection(
redis_url=redis_url, redis_sentinels=sentinels, async_mode=False
)
assert isinstance(result, SentinelRedisProxy)
mock_sentinel_class.assert_called_once()
mock_from_url.assert_not_called()
@patch("redis.Redis.from_url")
def test_get_redis_connection_without_sentinel(self, mock_from_url):
"""Test getting Redis connection without Sentinel"""
mock_redis = Mock()
mock_from_url.return_value = mock_redis
redis_url = "redis://localhost:6379/0"
result = get_redis_connection(
redis_url=redis_url, redis_sentinels=None, async_mode=False
)
assert result == mock_redis
mock_from_url.assert_called_once_with(redis_url, decode_responses=True)
@patch("redis.asyncio.from_url")
def test_get_redis_connection_without_sentinel_async(self, mock_from_url):
"""Test getting async Redis connection without Sentinel"""
mock_redis = Mock()
mock_from_url.return_value = mock_redis
redis_url = "redis://localhost:6379/0"
result = get_redis_connection(
redis_url=redis_url, redis_sentinels=None, async_mode=True
)
assert result == mock_redis
mock_from_url.assert_called_once_with(redis_url, decode_responses=True)
class TestSentinelRedisProxyCommands:
"""Test Redis commands through SentinelRedisProxy"""
@patch("redis.sentinel.Sentinel")
def test_hash_commands_sync(self, mock_sentinel_class):
"""Test Redis hash commands in sync mode"""
mock_sentinel = Mock()
mock_master = Mock()
# Mock hash command responses
mock_master.hset.return_value = 1
mock_master.hget.return_value = "test_value"
mock_master.hgetall.return_value = {"key1": "value1", "key2": "value2"}
mock_master.hdel.return_value = 1
mock_sentinel.master_for.return_value = mock_master
proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=False)
# Test hset
hset_method = proxy.__getattr__("hset")
result = hset_method("test_hash", "field1", "value1")
assert result == 1
mock_master.hset.assert_called_with("test_hash", "field1", "value1")
# Test hget
hget_method = proxy.__getattr__("hget")
result = hget_method("test_hash", "field1")
assert result == "test_value"
mock_master.hget.assert_called_with("test_hash", "field1")
# Test hgetall
hgetall_method = proxy.__getattr__("hgetall")
result = hgetall_method("test_hash")
assert result == {"key1": "value1", "key2": "value2"}
mock_master.hgetall.assert_called_with("test_hash")
# Test hdel
hdel_method = proxy.__getattr__("hdel")
result = hdel_method("test_hash", "field1")
assert result == 1
mock_master.hdel.assert_called_with("test_hash", "field1")
@patch("redis.sentinel.Sentinel")
@pytest.mark.asyncio
async def test_hash_commands_async(self, mock_sentinel_class):
"""Test Redis hash commands in async mode"""
mock_sentinel = Mock()
mock_master = Mock()
# Mock async hash command responses
mock_master.hset = AsyncMock(return_value=1)
mock_master.hget = AsyncMock(return_value="test_value")
mock_master.hgetall = AsyncMock(
return_value={"key1": "value1", "key2": "value2"}
)
mock_sentinel.master_for.return_value = mock_master
proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=True)
# Test hset
hset_method = proxy.__getattr__("hset")
result = await hset_method("test_hash", "field1", "value1")
assert result == 1
mock_master.hset.assert_called_with("test_hash", "field1", "value1")
# Test hget
hget_method = proxy.__getattr__("hget")
result = await hget_method("test_hash", "field1")
assert result == "test_value"
mock_master.hget.assert_called_with("test_hash", "field1")
# Test hgetall
hgetall_method = proxy.__getattr__("hgetall")
result = await hgetall_method("test_hash")
assert result == {"key1": "value1", "key2": "value2"}
mock_master.hgetall.assert_called_with("test_hash")
@patch("redis.sentinel.Sentinel")
def test_string_commands_sync(self, mock_sentinel_class):
"""Test Redis string commands in sync mode"""
mock_sentinel = Mock()
mock_master = Mock()
# Mock string command responses
mock_master.set.return_value = True
mock_master.get.return_value = "test_value"
mock_master.delete.return_value = 1
mock_master.exists.return_value = True
mock_sentinel.master_for.return_value = mock_master
proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=False)
# Test set
set_method = proxy.__getattr__("set")
result = set_method("test_key", "test_value")
assert result is True
mock_master.set.assert_called_with("test_key", "test_value")
# Test get
get_method = proxy.__getattr__("get")
result = get_method("test_key")
assert result == "test_value"
mock_master.get.assert_called_with("test_key")
# Test delete
delete_method = proxy.__getattr__("delete")
result = delete_method("test_key")
assert result == 1
mock_master.delete.assert_called_with("test_key")
# Test exists
exists_method = proxy.__getattr__("exists")
result = exists_method("test_key")
assert result is True
mock_master.exists.assert_called_with("test_key")
@patch("redis.sentinel.Sentinel")
def test_list_commands_sync(self, mock_sentinel_class):
"""Test Redis list commands in sync mode"""
mock_sentinel = Mock()
mock_master = Mock()
# Mock list command responses
mock_master.lpush.return_value = 1
mock_master.rpop.return_value = "test_value"
mock_master.llen.return_value = 5
mock_master.lrange.return_value = ["item1", "item2", "item3"]
mock_sentinel.master_for.return_value = mock_master
proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=False)
# Test lpush
lpush_method = proxy.__getattr__("lpush")
result = lpush_method("test_list", "item1")
assert result == 1
mock_master.lpush.assert_called_with("test_list", "item1")
# Test rpop
rpop_method = proxy.__getattr__("rpop")
result = rpop_method("test_list")
assert result == "test_value"
mock_master.rpop.assert_called_with("test_list")
# Test llen
llen_method = proxy.__getattr__("llen")
result = llen_method("test_list")
assert result == 5
mock_master.llen.assert_called_with("test_list")
# Test lrange
lrange_method = proxy.__getattr__("lrange")
result = lrange_method("test_list", 0, -1)
assert result == ["item1", "item2", "item3"]
mock_master.lrange.assert_called_with("test_list", 0, -1)
@patch("redis.sentinel.Sentinel")
def test_pubsub_commands_sync(self, mock_sentinel_class):
"""Test Redis pubsub commands in sync mode"""
mock_sentinel = Mock()
mock_master = Mock()
mock_pubsub = Mock()
# Mock pubsub responses
mock_master.pubsub.return_value = mock_pubsub
mock_master.publish.return_value = 1
mock_pubsub.subscribe.return_value = None
mock_pubsub.get_message.return_value = {"type": "message", "data": "test_data"}
mock_sentinel.master_for.return_value = mock_master
proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=False)
# Test pubsub (factory method - should pass through)
pubsub_method = proxy.__getattr__("pubsub")
result = pubsub_method()
assert result == mock_pubsub
mock_master.pubsub.assert_called_once()
# Test publish
publish_method = proxy.__getattr__("publish")
result = publish_method("test_channel", "test_message")
assert result == 1
mock_master.publish.assert_called_with("test_channel", "test_message")
@patch("redis.sentinel.Sentinel")
def test_pipeline_commands_sync(self, mock_sentinel_class):
"""Test Redis pipeline commands in sync mode"""
mock_sentinel = Mock()
mock_master = Mock()
mock_pipeline = Mock()
# Mock pipeline responses
mock_master.pipeline.return_value = mock_pipeline
mock_pipeline.set.return_value = mock_pipeline
mock_pipeline.get.return_value = mock_pipeline
mock_pipeline.execute.return_value = [True, "test_value"]
mock_sentinel.master_for.return_value = mock_master
proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=False)
# Test pipeline (factory method - should pass through)
pipeline_method = proxy.__getattr__("pipeline")
result = pipeline_method()
assert result == mock_pipeline
mock_master.pipeline.assert_called_once()
@patch("redis.sentinel.Sentinel")
def test_commands_with_failover_retry(self, mock_sentinel_class):
"""Test Redis commands with failover retry mechanism"""
mock_sentinel = Mock()
mock_master = Mock()
# First call fails with connection error, second succeeds
mock_master.hget.side_effect = [
redis.exceptions.ConnectionError("Connection failed"),
"recovered_value",
]
mock_sentinel.master_for.return_value = mock_master
proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=False)
# Test hget with retry
hget_method = proxy.__getattr__("hget")
result = hget_method("test_hash", "field1")
assert result == "recovered_value"
assert mock_master.hget.call_count == 2
# Verify both calls were made with same parameters
expected_calls = [(("test_hash", "field1"),), (("test_hash", "field1"),)]
actual_calls = [call.args for call in mock_master.hget.call_args_list]
assert actual_calls == expected_calls
@patch("redis.sentinel.Sentinel")
def test_commands_with_readonly_error_retry(self, mock_sentinel_class):
"""Test Redis commands with ReadOnlyError retry mechanism"""
mock_sentinel = Mock()
mock_master = Mock()
# First call fails with ReadOnlyError, second succeeds
mock_master.hset.side_effect = [
redis.exceptions.ReadOnlyError(
"READONLY You can't write against a read only replica"
),
1,
]
mock_sentinel.master_for.return_value = mock_master
proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=False)
# Test hset with retry
hset_method = proxy.__getattr__("hset")
result = hset_method("test_hash", "field1", "value1")
assert result == 1
assert mock_master.hset.call_count == 2
# Verify both calls were made with same parameters
expected_calls = [
(("test_hash", "field1", "value1"),),
(("test_hash", "field1", "value1"),),
]
actual_calls = [call.args for call in mock_master.hset.call_args_list]
assert actual_calls == expected_calls
@patch("redis.sentinel.Sentinel")
@pytest.mark.asyncio
async def test_async_commands_with_failover_retry(self, mock_sentinel_class):
"""Test async Redis commands with failover retry mechanism"""
mock_sentinel = Mock()
mock_master = Mock()
# First call fails with connection error, second succeeds
mock_master.hget = AsyncMock(
side_effect=[
redis.exceptions.ConnectionError("Connection failed"),
"recovered_value",
]
)
mock_sentinel.master_for.return_value = mock_master
proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=True)
# Test async hget with retry
hget_method = proxy.__getattr__("hget")
result = await hget_method("test_hash", "field1")
assert result == "recovered_value"
assert mock_master.hget.call_count == 2
# Verify both calls were made with same parameters
expected_calls = [(("test_hash", "field1"),), (("test_hash", "field1"),)]
actual_calls = [call.args for call in mock_master.hget.call_args_list]
assert actual_calls == expected_calls
class TestSentinelRedisProxyFactoryMethods:
"""Test Redis factory methods in async mode - these are special cases that remain sync"""
@patch("redis.sentinel.Sentinel")
@pytest.mark.asyncio
async def test_pubsub_factory_method_async(self, mock_sentinel_class):
"""Test pubsub factory method in async mode - should pass through without wrapping"""
mock_sentinel = Mock()
mock_master = Mock()
mock_pubsub = Mock()
# Mock pubsub factory method
mock_master.pubsub.return_value = mock_pubsub
mock_sentinel.master_for.return_value = mock_master
proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=True)
# Test pubsub factory method - should NOT be wrapped as async
pubsub_method = proxy.__getattr__("pubsub")
result = pubsub_method()
assert result == mock_pubsub
mock_master.pubsub.assert_called_once()
# Verify it's not wrapped as async (no await needed)
assert not inspect.iscoroutine(result)
@patch("redis.sentinel.Sentinel")
@pytest.mark.asyncio
async def test_pipeline_factory_method_async(self, mock_sentinel_class):
"""Test pipeline factory method in async mode - should pass through without wrapping"""
mock_sentinel = Mock()
mock_master = Mock()
mock_pipeline = Mock()
# Mock pipeline factory method
mock_master.pipeline.return_value = mock_pipeline
mock_pipeline.set.return_value = mock_pipeline
mock_pipeline.get.return_value = mock_pipeline
mock_pipeline.execute.return_value = [True, "test_value"]
mock_sentinel.master_for.return_value = mock_master
proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=True)
# Test pipeline factory method - should NOT be wrapped as async
pipeline_method = proxy.__getattr__("pipeline")
result = pipeline_method()
assert result == mock_pipeline
mock_master.pipeline.assert_called_once()
# Verify it's not wrapped as async (no await needed)
assert not inspect.iscoroutine(result)
# Test pipeline usage (these should also be sync)
pipeline_result = result.set("key", "value").get("key").execute()
assert pipeline_result == [True, "test_value"]
@patch("redis.sentinel.Sentinel")
@pytest.mark.asyncio
async def test_factory_methods_vs_regular_commands_async(self, mock_sentinel_class):
"""Test that factory methods behave differently from regular commands in async mode"""
mock_sentinel = Mock()
mock_master = Mock()
# Mock both factory method and regular command
mock_pubsub = Mock()
mock_master.pubsub.return_value = mock_pubsub
mock_master.get = AsyncMock(return_value="test_value")
mock_sentinel.master_for.return_value = mock_master
proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=True)
# Test factory method - should NOT be wrapped
pubsub_method = proxy.__getattr__("pubsub")
pubsub_result = pubsub_method()
# Test regular command - should be wrapped as async
get_method = proxy.__getattr__("get")
get_result = get_method("test_key")
# Factory method returns directly
assert pubsub_result == mock_pubsub
assert not inspect.iscoroutine(pubsub_result)
# Regular command returns coroutine
assert inspect.iscoroutine(get_result)
# Regular command needs await
actual_value = await get_result
assert actual_value == "test_value"
@patch("redis.sentinel.Sentinel")
@pytest.mark.asyncio
async def test_factory_methods_with_failover_async(self, mock_sentinel_class):
"""Test factory methods with failover in async mode"""
mock_sentinel = Mock()
mock_master = Mock()
# First call fails, second succeeds
mock_pubsub = Mock()
mock_master.pubsub.side_effect = [
redis.exceptions.ConnectionError("Connection failed"),
mock_pubsub,
]
mock_sentinel.master_for.return_value = mock_master
proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=True)
# Test pubsub factory method with failover
pubsub_method = proxy.__getattr__("pubsub")
result = pubsub_method()
assert result == mock_pubsub
assert mock_master.pubsub.call_count == 2 # Retry happened
# Verify it's still not wrapped as async after retry
assert not inspect.iscoroutine(result)
@patch("redis.sentinel.Sentinel")
@pytest.mark.asyncio
async def test_monitor_factory_method_async(self, mock_sentinel_class):
"""Test monitor factory method in async mode - should pass through without wrapping"""
mock_sentinel = Mock()
mock_master = Mock()
mock_monitor = Mock()
# Mock monitor factory method
mock_master.monitor.return_value = mock_monitor
mock_sentinel.master_for.return_value = mock_master
proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=True)
# Test monitor factory method - should NOT be wrapped as async
monitor_method = proxy.__getattr__("monitor")
result = monitor_method()
assert result == mock_monitor
mock_master.monitor.assert_called_once()
# Verify it's not wrapped as async (no await needed)
assert not inspect.iscoroutine(result)
@patch("redis.sentinel.Sentinel")
@pytest.mark.asyncio
async def test_client_factory_method_async(self, mock_sentinel_class):
"""Test client factory method in async mode - should pass through without wrapping"""
mock_sentinel = Mock()
mock_master = Mock()
mock_client = Mock()
# Mock client factory method
mock_master.client.return_value = mock_client
mock_sentinel.master_for.return_value = mock_master
proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=True)
# Test client factory method - should NOT be wrapped as async
client_method = proxy.__getattr__("client")
result = client_method()
assert result == mock_client
mock_master.client.assert_called_once()
# Verify it's not wrapped as async (no await needed)
assert not inspect.iscoroutine(result)
@patch("redis.sentinel.Sentinel")
@pytest.mark.asyncio
async def test_transaction_factory_method_async(self, mock_sentinel_class):
"""Test transaction factory method in async mode - should pass through without wrapping"""
mock_sentinel = Mock()
mock_master = Mock()
mock_transaction = Mock()
# Mock transaction factory method
mock_master.transaction.return_value = mock_transaction
mock_sentinel.master_for.return_value = mock_master
proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=True)
# Test transaction factory method - should NOT be wrapped as async
transaction_method = proxy.__getattr__("transaction")
result = transaction_method()
assert result == mock_transaction
mock_master.transaction.assert_called_once()
# Verify it's not wrapped as async (no await needed)
assert not inspect.iscoroutine(result)
@patch("redis.sentinel.Sentinel")
@pytest.mark.asyncio
async def test_all_factory_methods_async(self, mock_sentinel_class):
"""Test all factory methods in async mode - comprehensive test"""
mock_sentinel = Mock()
mock_master = Mock()
# Mock all factory methods
mock_objects = {
"pipeline": Mock(),
"pubsub": Mock(),
"monitor": Mock(),
"client": Mock(),
"transaction": Mock(),
}
for method_name, mock_obj in mock_objects.items():
setattr(mock_master, method_name, Mock(return_value=mock_obj))
mock_sentinel.master_for.return_value = mock_master
proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=True)
# Test all factory methods
for method_name, expected_obj in mock_objects.items():
method = proxy.__getattr__(method_name)
result = method()
assert result == expected_obj
assert not inspect.iscoroutine(result)
getattr(mock_master, method_name).assert_called_once()
# Reset mock for next iteration
getattr(mock_master, method_name).reset_mock()
@patch("redis.sentinel.Sentinel")
@pytest.mark.asyncio
async def test_mixed_factory_and_regular_commands_async(self, mock_sentinel_class):
"""Test using both factory methods and regular commands in async mode"""
mock_sentinel = Mock()
mock_master = Mock()
# Mock pipeline factory and regular commands
mock_pipeline = Mock()
mock_master.pipeline.return_value = mock_pipeline
mock_pipeline.set.return_value = mock_pipeline
mock_pipeline.get.return_value = mock_pipeline
mock_pipeline.execute.return_value = [True, "pipeline_value"]
mock_master.get = AsyncMock(return_value="regular_value")
mock_sentinel.master_for.return_value = mock_master
proxy = SentinelRedisProxy(mock_sentinel, "mymaster", async_mode=True)
# Use factory method (sync)
pipeline = proxy.__getattr__("pipeline")()
pipeline_result = pipeline.set("key1", "value1").get("key1").execute()
# Use regular command (async)
get_method = proxy.__getattr__("get")
regular_result = await get_method("key2")
# Verify both work correctly
assert pipeline_result == [True, "pipeline_value"]
assert regular_result == "regular_value"
# Verify calls
mock_master.pipeline.assert_called_once()
mock_master.get.assert_called_with("key2")

View File

@ -4,6 +4,7 @@ import sys
from typing import TYPE_CHECKING
from loguru import logger
from opentelemetry import trace
from open_webui.env import (
@ -12,6 +13,7 @@ from open_webui.env import (
AUDIT_LOG_LEVEL,
AUDIT_LOGS_FILE_PATH,
GLOBAL_LOG_LEVEL,
ENABLE_OTEL,
)
@ -60,9 +62,20 @@ class InterceptHandler(logging.Handler):
frame = frame.f_back
depth += 1
logger.opt(depth=depth, exception=record.exc_info).log(
level, record.getMessage()
)
logger.opt(depth=depth, exception=record.exc_info).bind(
**self._get_extras()
).log(level, record.getMessage())
def _get_extras(self):
if not ENABLE_OTEL:
return {}
extras = {}
context = trace.get_current_span().get_span_context()
if context.is_valid:
extras["trace_id"] = trace.format_trace_id(context.trace_id)
extras["span_id"] = trace.format_span_id(context.span_id)
return extras
def file_format(record: "Record"):

View File

@ -653,13 +653,13 @@ async def chat_completion_files_handler(
),
k=request.app.state.config.TOP_K,
reranking_function=(
lambda sentences: (
request.app.state.RERANKING_FUNCTION(
(
lambda sentences: request.app.state.RERANKING_FUNCTION(
sentences, user=user
)
if request.app.state.RERANKING_FUNCTION
else None
)
if request.app.state.RERANKING_FUNCTION
else None
),
k_reranker=request.app.state.config.TOP_K_RERANKER,
r=request.app.state.config.RELEVANCE_THRESHOLD,
@ -1472,12 +1472,12 @@ async def process_chat_response(
if reasoning_duration is not None:
if raw:
content = f'{content}\n<{block["start_tag"]}>{block["content"]}<{block["end_tag"]}>\n'
content = f'{content}\n{block["start_tag"]}{block["content"]}{block["end_tag"]}\n'
else:
content = f'{content}\n<details type="reasoning" done="true" duration="{reasoning_duration}">\n<summary>Thought for {reasoning_duration} seconds</summary>\n{reasoning_display_content}\n</details>\n'
else:
if raw:
content = f'{content}\n<{block["start_tag"]}>{block["content"]}<{block["end_tag"]}>\n'
content = f'{content}\n{block["start_tag"]}{block["content"]}{block["end_tag"]}\n'
else:
content = f'{content}\n<details type="reasoning" done="false">\n<summary>Thinking…</summary>\n{reasoning_display_content}\n</details>\n'
@ -1574,8 +1574,16 @@ async def process_chat_response(
if content_blocks[-1]["type"] == "text":
for start_tag, end_tag in tags:
# Match start tag e.g., <tag> or <tag attr="value">
start_tag_pattern = rf"<{re.escape(start_tag)}(\s.*?)?>"
start_tag_pattern = rf"{re.escape(start_tag)}"
if start_tag.startswith("<") and start_tag.endswith(">"):
# Match start tag e.g., <tag> or <tag attr="value">
# remove both '<' and '>' from start_tag
# Match start tag with attributes
start_tag_pattern = (
rf"<{re.escape(start_tag[1:-1])}(\s.*?)?>"
)
match = re.search(start_tag_pattern, content)
if match:
attr_content = (
@ -1626,8 +1634,13 @@ async def process_chat_response(
elif content_blocks[-1]["type"] == content_type:
start_tag = content_blocks[-1]["start_tag"]
end_tag = content_blocks[-1]["end_tag"]
# Match end tag e.g., </tag>
end_tag_pattern = rf"<{re.escape(end_tag)}>"
if end_tag.startswith("<") and end_tag.endswith(">"):
# Match end tag e.g., </tag>
end_tag_pattern = rf"{re.escape(end_tag)}"
else:
# Handle cases where end_tag is just a tag name
end_tag_pattern = rf"{re.escape(end_tag)}"
# Check if the content has the end tag
if re.search(end_tag_pattern, content):
@ -1699,8 +1712,17 @@ async def process_chat_response(
)
# Clean processed content
start_tag_pattern = rf"{re.escape(start_tag)}"
if start_tag.startswith("<") and start_tag.endswith(">"):
# Match start tag e.g., <tag> or <tag attr="value">
# remove both '<' and '>' from start_tag
# Match start tag with attributes
start_tag_pattern = (
rf"<{re.escape(start_tag[1:-1])}(\s.*?)?>"
)
content = re.sub(
rf"<{re.escape(start_tag)}(.*?)>(.|\n)*?<{re.escape(end_tag)}>",
rf"{start_tag_pattern}(.|\n)*?{re.escape(end_tag)}",
"",
content,
flags=re.DOTALL,
@ -1744,18 +1766,19 @@ async def process_chat_response(
)
reasoning_tags = [
("think", "/think"),
("thinking", "/thinking"),
("reason", "/reason"),
("reasoning", "/reasoning"),
("thought", "/thought"),
("Thought", "/Thought"),
("|begin_of_thought|", "|end_of_thought|"),
("<think>", "</think>"),
("<thinking>", "</thinking>"),
("<reason>", "</reason>"),
("<reasoning>", "</reasoning>"),
("<thought>", "</thought>"),
("<Thought>", "</Thought>"),
("<|begin_of_thought|>", "<|end_of_thought|>"),
("◁think▷", "◁/think▷"),
]
code_interpreter_tags = [("code_interpreter", "/code_interpreter")]
code_interpreter_tags = [("<code_interpreter>", "</code_interpreter>")]
solution_tags = [("|begin_of_solution|", "|end_of_solution|")]
solution_tags = [("<|begin_of_solution|>", "<|end_of_solution|>")]
try:
for event in events:
@ -2039,7 +2062,7 @@ async def process_chat_response(
if done:
pass
else:
log.debug("Error: ", e)
log.debug(f"Error: {e}")
continue
if content_blocks:

View File

@ -1,6 +1,94 @@
import socketio
import inspect
from urllib.parse import urlparse
from typing import Optional
import logging
import redis
from open_webui.env import REDIS_SENTINEL_MAX_RETRY_COUNT
log = logging.getLogger(__name__)
class SentinelRedisProxy:
def __init__(self, sentinel, service, *, async_mode: bool = True, **kw):
self._sentinel = sentinel
self._service = service
self._kw = kw
self._async_mode = async_mode
def _master(self):
return self._sentinel.master_for(self._service, **self._kw)
def __getattr__(self, item):
master = self._master()
orig_attr = getattr(master, item)
if not callable(orig_attr):
return orig_attr
FACTORY_METHODS = {"pipeline", "pubsub", "monitor", "client", "transaction"}
if item in FACTORY_METHODS:
return orig_attr
if self._async_mode:
async def _wrapped(*args, **kwargs):
for i in range(REDIS_SENTINEL_MAX_RETRY_COUNT):
try:
method = getattr(self._master(), item)
result = method(*args, **kwargs)
if inspect.iscoroutine(result):
return await result
return result
except (
redis.exceptions.ConnectionError,
redis.exceptions.ReadOnlyError,
) as e:
if i < REDIS_SENTINEL_MAX_RETRY_COUNT - 1:
log.debug(
"Redis sentinel fail-over (%s). Retry %s/%s",
type(e).__name__,
i + 1,
REDIS_SENTINEL_MAX_RETRY_COUNT,
)
continue
log.error(
"Redis operation failed after %s retries: %s",
REDIS_SENTINEL_MAX_RETRY_COUNT,
e,
)
raise e from e
return _wrapped
else:
def _wrapped(*args, **kwargs):
for i in range(REDIS_SENTINEL_MAX_RETRY_COUNT):
try:
method = getattr(self._master(), item)
return method(*args, **kwargs)
except (
redis.exceptions.ConnectionError,
redis.exceptions.ReadOnlyError,
) as e:
if i < REDIS_SENTINEL_MAX_RETRY_COUNT - 1:
log.debug(
"Redis sentinel fail-over (%s). Retry %s/%s",
type(e).__name__,
i + 1,
REDIS_SENTINEL_MAX_RETRY_COUNT,
)
continue
log.error(
"Redis operation failed after %s retries: %s",
REDIS_SENTINEL_MAX_RETRY_COUNT,
e,
)
raise e from e
return _wrapped
def parse_redis_service_url(redis_url):
@ -34,7 +122,11 @@ def get_redis_connection(
password=redis_config["password"],
decode_responses=decode_responses,
)
return sentinel.master_for(redis_config["service"])
return SentinelRedisProxy(
sentinel,
redis_config["service"],
async_mode=async_mode,
)
elif redis_url:
return redis.from_url(redis_url, decode_responses=decode_responses)
else:
@ -52,7 +144,11 @@ def get_redis_connection(
password=redis_config["password"],
decode_responses=decode_responses,
)
return sentinel.master_for(redis_config["service"])
return SentinelRedisProxy(
sentinel,
redis_config["service"],
async_mode=async_mode,
)
elif redis_url:
return redis.Redis.from_url(redis_url, decode_responses=decode_responses)
else:

View File

@ -6,18 +6,17 @@ from open_webui.utils.misc import (
)
def convert_ollama_tool_call_to_openai(tool_calls: dict) -> dict:
def convert_ollama_tool_call_to_openai(tool_calls: list) -> list:
openai_tool_calls = []
for tool_call in tool_calls:
function = tool_call.get("function", {})
openai_tool_call = {
"index": tool_call.get("index", 0),
"index": tool_call.get("index", function.get("index", 0)),
"id": tool_call.get("id", f"call_{str(uuid4())}"),
"type": "function",
"function": {
"name": tool_call.get("function", {}).get("name", ""),
"arguments": json.dumps(
tool_call.get("function", {}).get("arguments", {})
),
"name": function.get("name", ""),
"arguments": json.dumps(function.get("arguments", {})),
},
}
openai_tool_calls.append(openai_tool_call)

View File

@ -34,6 +34,8 @@ from opentelemetry.sdk.resources import SERVICE_NAME, Resource
from open_webui.env import OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT
from open_webui.socket.main import get_active_user_ids
from open_webui.models.users import Users
_EXPORT_INTERVAL_MILLIS = 10_000 # 10 seconds
@ -59,6 +61,12 @@ def _build_meter_provider() -> MeterProvider:
instrument_name="http.server.requests",
attribute_keys=["http.method", "http.route", "http.status_code"],
),
View(
instrument_name="webui.users.total",
),
View(
instrument_name="webui.users.active",
),
]
provider = MeterProvider(
@ -87,6 +95,38 @@ def setup_metrics(app: FastAPI) -> None:
unit="ms",
)
def observe_active_users(
options: metrics.CallbackOptions,
) -> Sequence[metrics.Observation]:
return [
metrics.Observation(
value=len(get_active_user_ids()),
)
]
def observe_total_registered_users(
options: metrics.CallbackOptions,
) -> Sequence[metrics.Observation]:
return [
metrics.Observation(
value=len(Users.get_users()["users"]),
)
]
meter.create_observable_gauge(
name="webui.users.total",
description="Total number of registered users",
unit="users",
callbacks=[observe_total_registered_users],
)
meter.create_observable_gauge(
name="webui.users.active",
description="Number of currently active users",
unit="users",
callbacks=[observe_active_users],
)
# FastAPI middleware
@app.middleware("http")
async def _metrics_middleware(request: Request, call_next):

View File

@ -51,6 +51,7 @@ langchain-community==0.3.26
fake-useragent==2.1.0
chromadb==0.6.3
posthog==5.4.0
pymilvus==2.5.0
qdrant-client==1.14.3
opensearch-py==2.8.0

647
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "open-webui",
"version": "0.6.16",
"version": "0.6.17",
"private": true,
"scripts": {
"dev": "npm run pyodide:fetch && vite dev --host",
@ -57,30 +57,28 @@
"@codemirror/lang-python": "^6.1.6",
"@codemirror/language-data": "^6.5.1",
"@codemirror/theme-one-dark": "^6.1.2",
"@floating-ui/dom": "^1.7.2",
"@huggingface/transformers": "^3.0.0",
"@mediapipe/tasks-vision": "^0.10.17",
"@pyscript/core": "^0.4.32",
"@sveltejs/adapter-node": "^2.0.0",
"@sveltejs/svelte-virtual-list": "^3.0.1",
"@tiptap/core": "^2.11.9",
"@tiptap/extension-bubble-menu": "^2.25.0",
"@tiptap/extension-character-count": "^2.25.0",
"@tiptap/extension-code-block-lowlight": "^2.11.9",
"@tiptap/extension-floating-menu": "^2.25.0",
"@tiptap/extension-highlight": "^2.10.0",
"@tiptap/extension-history": "^2.25.1",
"@tiptap/extension-link": "^2.25.0",
"@tiptap/extension-placeholder": "^2.10.0",
"@tiptap/extension-table": "^2.12.0",
"@tiptap/extension-table-cell": "^2.12.0",
"@tiptap/extension-table-header": "^2.12.0",
"@tiptap/extension-table-row": "^2.12.0",
"@tiptap/extension-task-item": "^2.25.0",
"@tiptap/extension-task-list": "^2.25.0",
"@tiptap/extension-typography": "^2.10.0",
"@tiptap/extension-underline": "^2.25.0",
"@tiptap/pm": "^2.11.7",
"@tiptap/starter-kit": "^2.10.0",
"@tiptap/core": "^3.0.7",
"@tiptap/extension-bubble-menu": "^2.26.1",
"@tiptap/extension-code-block-lowlight": "^3.0.7",
"@tiptap/extension-drag-handle": "^3.0.7",
"@tiptap/extension-file-handler": "^3.0.7",
"@tiptap/extension-floating-menu": "^2.26.1",
"@tiptap/extension-highlight": "^3.0.7",
"@tiptap/extension-image": "^3.0.7",
"@tiptap/extension-link": "^3.0.7",
"@tiptap/extension-list": "^3.0.7",
"@tiptap/extension-table": "^3.0.7",
"@tiptap/extension-typography": "^3.0.7",
"@tiptap/extension-youtube": "^3.0.7",
"@tiptap/extensions": "^3.0.7",
"@tiptap/pm": "^3.0.7",
"@tiptap/starter-kit": "^3.0.7",
"@xyflow/svelte": "^0.1.19",
"async": "^3.2.5",
"bits-ui": "^0.21.15",
@ -108,6 +106,7 @@
"katex": "^0.16.22",
"kokoro-js": "^1.1.1",
"leaflet": "^1.9.4",
"lowlight": "^3.3.0",
"marked": "^9.1.0",
"mermaid": "^11.6.0",
"paneforge": "^0.0.6",

View File

@ -136,6 +136,8 @@ dependencies = [
"moto[s3]>=5.0.26",
"posthog==5.4.0",
]
readme = "README.md"
requires-python = ">= 3.11, < 3.13.0a1"
@ -191,3 +193,8 @@ skip = '.git*,*.svg,package-lock.json,i18n,*.lock,*.css,*-bundle.js,locales,exam
check-hidden = true
# ignore-regex = ''
ignore-words-list = 'ans'
[dependency-groups]
dev = [
"pytest-asyncio>=1.0.0",
]

View File

@ -40,6 +40,11 @@ code {
width: auto;
}
.editor-selection {
background: rgba(180, 213, 255, 0.5);
border-radius: 2px;
}
.font-secondary {
font-family: 'InstrumentSerif', sans-serif;
}

View File

@ -1,6 +1,11 @@
import { WEBUI_API_BASE_URL } from '$lib/constants';
export const createNewFolder = async (token: string, name: string) => {
type FolderForm = {
name: string;
data?: Record<string, any>;
};
export const createNewFolder = async (token: string, folderForm: FolderForm) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/folders/`, {
@ -10,9 +15,7 @@ export const createNewFolder = async (token: string, name: string) => {
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
},
body: JSON.stringify({
name: name
})
body: JSON.stringify(folderForm)
})
.then(async (res) => {
if (!res.ok) throw await res.json();
@ -92,11 +95,6 @@ export const getFolderById = async (token: string, id: string) => {
return res;
};
type FolderForm = {
name: string;
data?: Record<string, any>;
};
export const updateFolderById = async (token: string, id: string, folderForm: FolderForm) => {
let error = null;

View File

@ -201,7 +201,7 @@
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
bind:value={STT_SUPPORTED_CONTENT_TYPES}
placeholder={$i18n.t(
'e.g., audio/wav,audio/mpeg,video/* (leave blank for defaults, * for all)'
'e.g., audio/wav,audio/mpeg,video/* (leave blank for defaults)'
)}
/>
</div>

View File

@ -1228,7 +1228,7 @@
await handleOpenAIError(error, message);
}
if (sources) {
if (sources && !message?.sources) {
message.sources = sources;
}
@ -1352,6 +1352,12 @@
);
history.messages[message.id] = message;
await tick();
if (autoScroll) {
scrollToBottom();
}
await chatCompletedHandler(
chatId,
message.model,
@ -1361,6 +1367,8 @@
}
console.log(data);
await tick();
if (autoScroll) {
scrollToBottom();
}
@ -2069,12 +2077,13 @@
>
{#if !loading}
<div in:fade={{ duration: 50 }} class="w-full h-full flex flex-col">
{#if $settings?.backgroundImageUrl ?? null}
{#if $settings?.backgroundImageUrl ?? $config?.license_metadata?.background_image_url ?? null}
<div
class="absolute {$showSidebar
? 'md:max-w-[calc(100%-260px)] md:translate-x-[260px]'
: ''} top-0 left-0 w-full h-full bg-cover bg-center bg-no-repeat"
style="background-image: url({$settings.backgroundImageUrl}) "
style="background-image: url({$settings?.backgroundImageUrl ??
$config?.license_metadata?.background_image_url}) "
/>
<div
@ -2105,8 +2114,8 @@
showBanners={!showCommands}
/>
<div class="flex flex-col flex-auto z-10 w-full @container">
{#if $settings?.landingPageMode === 'chat' || createMessagesList(history, history.currentId).length > 0}
<div class="flex flex-col flex-auto z-10 w-full @container overflow-auto">
{#if ($settings?.landingPageMode === 'chat' && !$selectedFolder) || createMessagesList(history, history.currentId).length > 0}
<div
class=" pb-2.5 flex flex-col justify-between w-full flex-auto overflow-auto h-0 max-w-full z-10 scrollbar-hidden"
id="messages-container"
@ -2123,6 +2132,9 @@
bind:history
bind:autoScroll
bind:prompt
setInputText={(text) => {
messageInput?.setText(text);
}}
{selectedModels}
{atSelectedModel}
{sendPrompt}
@ -2156,7 +2168,9 @@
bind:atSelectedModel
bind:showCommands
toolServers={$toolServers}
transparentBackground={$settings?.backgroundImageUrl ?? false}
transparentBackground={$settings?.backgroundImageUrl ??
$config?.license_metadata?.background_image_url ??
false}
{stopResponse}
{createMessagePair}
onChange={(input) => {
@ -2201,7 +2215,7 @@
</div>
</div>
{:else}
<div class="overflow-auto w-full h-full flex items-center">
<div class="flex items-center h-full">
<Placeholder
{history}
{selectedModels}
@ -2216,7 +2230,9 @@
bind:webSearchEnabled
bind:atSelectedModel
bind:showCommands
transparentBackground={$settings?.backgroundImageUrl ?? false}
transparentBackground={$settings?.backgroundImageUrl ??
$config?.license_metadata?.background_image_url ??
false}
toolServers={$toolServers}
{stopResponse}
{createMessagePair}

View File

@ -505,6 +505,11 @@
return null;
}
if (fileUploadCapableModels.length !== selectedModels.length) {
toast.error($i18n.t('Model(s) do not support file upload'));
return null;
}
const tempItemId = uuidv4();
const fileItem = {
type: 'file',
@ -1279,6 +1284,13 @@
};
reader.readAsDataURL(blob);
} else if (item?.kind === 'file') {
const file = item.getAsFile();
if (file) {
const _files = [file];
await inputFilesHandler(_files);
e.preventDefault();
}
} else if (item.type === 'text/plain') {
if (($settings?.largeTextAsFile ?? false) && !shiftKey) {
const text = clipboardData.getData('text/plain');
@ -1504,6 +1516,7 @@
if (clipboardData && clipboardData.items) {
for (const item of clipboardData.items) {
console.log(item);
if (item.type.indexOf('image') !== -1) {
const blob = item.getAsFile();
const reader = new FileReader();
@ -1519,6 +1532,13 @@
};
reader.readAsDataURL(blob);
} else if (item?.kind === 'file') {
const file = item.getAsFile();
if (file) {
const _files = [file];
await inputFilesHandler(_files);
e.preventDefault();
}
} else if (item.type === 'text/plain') {
if (($settings?.largeTextAsFile ?? false) && !shiftKey) {
const text = clipboardData.getData('text/plain');

View File

@ -36,6 +36,8 @@
let messages = [];
export let setInputText: Function = () => {};
export let sendPrompt: Function;
export let continueResponse: Function;
export let regenerateResponse: Function;
@ -426,6 +428,7 @@
messageId={message.id}
idx={messageIdx}
{user}
{setInputText}
{gotoMessage}
{showPreviousMessage}
{showNextMessage}

View File

@ -20,6 +20,7 @@
export let history;
export let selectedModels = [];
export let done = true;
export let model = null;
export let sources = null;
@ -133,6 +134,7 @@
{model}
{save}
{preview}
{done}
sourceIds={(sources ?? []).reduce((acc, s) => {
let ids = [];
s.document.forEach((document, index) => {

View File

@ -10,6 +10,7 @@
export let id = '';
export let content;
export let done = true;
export let model = null;
export let save = false;
export let preview = false;
@ -47,6 +48,7 @@
<MarkdownTokens
{tokens}
{id}
{done}
{save}
{preview}
{onTaskClick}

View File

@ -14,8 +14,11 @@
import KatexRenderer from './KatexRenderer.svelte';
import Source from './Source.svelte';
import HtmlToken from './HTMLToken.svelte';
import TextToken from './MarkdownInlineTokens/TextToken.svelte';
import CodespanToken from './MarkdownInlineTokens/CodespanToken.svelte';
export let id: string;
export let done = true;
export let tokens: Token[];
export let onSourceClick: Function = () => {};
</script>
@ -28,7 +31,7 @@
{:else if token.type === 'link'}
{#if token.tokens}
<a href={token.href} target="_blank" rel="nofollow" title={token.title}>
<svelte:self id={`${id}-a`} tokens={token.tokens} {onSourceClick} />
<svelte:self id={`${id}-a`} tokens={token.tokens} {onSourceClick} {done} />
</a>
{:else}
<a href={token.href} target="_blank" rel="nofollow" title={token.title}>{token.text}</a>
@ -40,15 +43,7 @@
{:else if token.type === 'em'}
<em><svelte:self id={`${id}-em`} tokens={token.tokens} {onSourceClick} /></em>
{:else if token.type === 'codespan'}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<code
class="codespan cursor-pointer"
on:click={() => {
copyToClipboard(unescapeHtml(token.text));
toast.success($i18n.t('Copied to clipboard'));
}}>{unescapeHtml(token.text)}</code
>
<CodespanToken {token} {done} />
{:else if token.type === 'br'}
<br />
{:else if token.type === 'del'}
@ -66,6 +61,6 @@
onload="this.style.height=(this.contentWindow.document.body.scrollHeight+20)+'px';"
></iframe>
{:else if token.type === 'text'}
{token.raw}
<TextToken {token} {done} />
{/if}
{/each}

View File

@ -0,0 +1,33 @@
<script lang="ts">
import { copyToClipboard, unescapeHtml } from '$lib/utils';
import { toast } from 'svelte-sonner';
import { fade } from 'svelte/transition';
import { getContext } from 'svelte';
const i18n = getContext('i18n');
export let token;
export let done = true;
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
{#if done}
<code
class="codespan cursor-pointer"
on:click={() => {
copyToClipboard(unescapeHtml(token.text));
toast.success($i18n.t('Copied to clipboard'));
}}>{unescapeHtml(token.text)}</code
>
{:else}
<code
transition:fade={{ duration: 100 }}
class="codespan cursor-pointer"
on:click={() => {
copyToClipboard(unescapeHtml(token.text));
toast.success($i18n.t('Copied to clipboard'));
}}>{unescapeHtml(token.text)}</code
>
{/if}

View File

@ -0,0 +1,19 @@
<script lang="ts">
import { fade } from 'svelte/transition';
export let token;
export let done = true;
let texts = [];
$: texts = (token?.raw ?? '').split(' ');
</script>
{#if done}
{token?.raw}
{:else}
{#each texts as text}
<span class="" transition:fade={{ duration: 100 }}>
{text}
</span>
{/each}
{/if}

View File

@ -28,6 +28,8 @@
export let top = true;
export let attributes = {};
export let done = true;
export let save = false;
export let preview = false;
@ -85,7 +87,12 @@
<hr class=" border-gray-100 dark:border-gray-850" />
{:else if token.type === 'heading'}
<svelte:element this={headerComponent(token.depth)} dir="auto">
<MarkdownInlineTokens id={`${id}-${tokenIdx}-h`} tokens={token.tokens} {onSourceClick} />
<MarkdownInlineTokens
id={`${id}-${tokenIdx}-h`}
tokens={token.tokens}
{done}
{onSourceClick}
/>
</svelte:element>
{:else if token.type === 'code'}
{#if token.raw.includes('```')}
@ -132,6 +139,7 @@
<MarkdownInlineTokens
id={`${id}-${tokenIdx}-header-${headerIdx}`}
tokens={header.tokens}
{done}
{onSourceClick}
/>
</div>
@ -152,6 +160,7 @@
<MarkdownInlineTokens
id={`${id}-${tokenIdx}-row-${rowIdx}-${cellIdx}`}
tokens={cell.tokens}
{done}
{onSourceClick}
/>
</div>
@ -183,7 +192,13 @@
<AlertRenderer {token} {alert} />
{:else}
<blockquote dir="auto">
<svelte:self id={`${id}-${tokenIdx}`} tokens={token.tokens} {onTaskClick} {onSourceClick} />
<svelte:self
id={`${id}-${tokenIdx}`}
tokens={token.tokens}
{done}
{onTaskClick}
{onSourceClick}
/>
</blockquote>
{/if}
{:else if token.type === 'list'}
@ -213,6 +228,7 @@
id={`${id}-${tokenIdx}-${itemIdx}`}
tokens={item.tokens}
top={token.loose}
{done}
{onTaskClick}
{onSourceClick}
/>
@ -245,6 +261,7 @@
id={`${id}-${tokenIdx}-${itemIdx}`}
tokens={item.tokens}
top={token.loose}
{done}
{onTaskClick}
{onSourceClick}
/>
@ -254,6 +271,7 @@
id={`${id}-${tokenIdx}-${itemIdx}`}
tokens={item.tokens}
top={token.loose}
{done}
{onTaskClick}
{onSourceClick}
/>
@ -275,6 +293,7 @@
id={`${id}-${tokenIdx}-d`}
tokens={marked.lexer(token.text)}
attributes={token?.attributes}
{done}
{onTaskClick}
{onSourceClick}
/>
@ -295,6 +314,7 @@
<MarkdownInlineTokens
id={`${id}-${tokenIdx}-p`}
tokens={token.tokens ?? []}
{done}
{onSourceClick}
/>
</p>
@ -302,7 +322,12 @@
{#if top}
<p>
{#if token.tokens}
<MarkdownInlineTokens id={`${id}-${tokenIdx}-t`} tokens={token.tokens} {onSourceClick} />
<MarkdownInlineTokens
id={`${id}-${tokenIdx}-t`}
tokens={token.tokens}
{done}
{onSourceClick}
/>
{:else}
{unescapeHtml(token.text)}
{/if}
@ -311,6 +336,7 @@
<MarkdownInlineTokens
id={`${id}-${tokenIdx}-p`}
tokens={token.tokens ?? []}
{done}
{onSourceClick}
/>
{:else}

View File

@ -21,6 +21,7 @@
export let user;
export let setInputText: Function = () => {};
export let gotoMessage;
export let showPreviousMessage;
export let showNextMessage;
@ -74,6 +75,7 @@
{selectedModels}
isLastMessage={messageId === history.currentId}
siblings={history.messages[history.messages[messageId].parentId]?.childrenIds ?? []}
{setInputText}
{gotoMessage}
{showPreviousMessage}
{showNextMessage}
@ -96,6 +98,7 @@
{messageId}
{selectedModels}
isLastMessage={messageId === history?.currentId}
{setInputText}
{updateChat}
{editMessage}
{saveMessage}

View File

@ -28,6 +28,7 @@
export let isLastMessage;
export let readOnly = false;
export let setInputText: Function = () => {};
export let updateChat: Function;
export let editMessage: Function;
export let saveMessage: Function;
@ -259,6 +260,7 @@
gotoMessage={(message, messageIdx) => gotoMessage(modelIdx, messageIdx)}
showPreviousMessage={() => showPreviousMessage(modelIdx)}
showNextMessage={() => showNextMessage(modelIdx)}
{setInputText}
{updateChat}
{editMessage}
{saveMessage}

View File

@ -117,6 +117,7 @@
export let siblings;
export let setInputText: Function = () => {};
export let gotoMessage: Function = () => {};
export let showPreviousMessage: Function;
export let showNextMessage: Function;
@ -165,7 +166,7 @@
text = `${text}\n\n${$config?.ui?.response_watermark}`;
}
const res = await _copyToClipboard(text, $settings?.copyFormatted ?? false);
const res = await _copyToClipboard(text, null, $settings?.copyFormatted ?? false);
if (res) {
toast.success($i18n.t('Copying to clipboard was successful!'));
}
@ -804,6 +805,9 @@
floatingButtons={message?.done && !readOnly}
save={!readOnly}
preview={!readOnly}
done={($settings?.chatFadeStreamingText ?? true)
? (message?.done ?? false)
: true}
{model}
onTaskClick={async (e) => {
console.log(e);
@ -1461,12 +1465,18 @@
/>
{/if}
{#if isLastMessage && message.done && !readOnly && (message?.followUps ?? []).length > 0}
{#if (isLastMessage || ($settings?.keepFollowUpPrompts ?? false)) && message.done && !readOnly && (message?.followUps ?? []).length > 0}
<div class="mt-2.5" in:fade={{ duration: 100 }}>
<FollowUps
followUps={message?.followUps}
onClick={(prompt) => {
submitMessage(message?.id, prompt);
if ($settings?.insertFollowUpPrompt ?? false) {
// Insert the follow-up prompt into the input box
setInputText(prompt);
} else {
// Submit the follow-up prompt directly
submitMessage(message?.id, prompt);
}
}}
/>
</div>

View File

@ -12,7 +12,9 @@
user,
models as _models,
temporaryChatEnabled,
selectedFolder
selectedFolder,
chats,
currentChatPage
} from '$lib/stores';
import { sanitizeResponseContent, extractCurlyBraceWords } from '$lib/utils';
import { WEBUI_BASE_URL } from '$lib/constants';
@ -21,9 +23,9 @@
import Tooltip from '$lib/components/common/Tooltip.svelte';
import EyeSlash from '$lib/components/icons/EyeSlash.svelte';
import MessageInput from './MessageInput.svelte';
import FolderOpen from '../icons/FolderOpen.svelte';
import XMark from '../icons/XMark.svelte';
import Folder from '../icons/Folder.svelte';
import FolderPlaceholder from './Placeholder/FolderPlaceholder.svelte';
import FolderTitle from './Placeholder/FolderTitle.svelte';
import { getChatList } from '$lib/apis/chats';
const i18n = getContext('i18n');
@ -87,29 +89,21 @@
>
<div class="w-full flex flex-col justify-center items-center">
{#if $selectedFolder}
<div class="mb-3 px-4 justify-center w-fit flex relative group">
<div class="text-center flex gap-3.5 items-center">
<div class=" rounded-full bg-gray-50 dark:bg-gray-800 p-3 w-fit">
<Folder className="size-4.5" strokeWidth="2" />
</div>
<FolderTitle
folder={$selectedFolder}
onUpdate={async (folder) => {
selectedFolder.set(folder);
<div class="text-3xl">
{$selectedFolder?.name}
</div>
</div>
await chats.set(await getChatList(localStorage.token, $currentChatPage));
currentChatPage.set(1);
}}
onDelete={async () => {
await chats.set(await getChatList(localStorage.token, $currentChatPage));
currentChatPage.set(1);
<div class="absolute -right-3">
<button
class="group-hover:visible invisible rounded-md"
type="button"
on:click={() => {
selectedFolder.set(null);
}}
>
<XMark className="size-4" />
</button>
</div>
</div>
selectedFolder.set(null);
}}
/>
{:else}
<div class="flex flex-row justify-center gap-3 @sm:gap-3.5 w-fit px-5 max-w-xl">
<div class="flex shrink-0 justify-center">
@ -249,16 +243,26 @@
</div>
</div>
</div>
<div class="mx-auto max-w-2xl font-primary mt-2" in:fade={{ duration: 200, delay: 200 }}>
<div class="mx-5">
<Suggestions
suggestionPrompts={atSelectedModel?.info?.meta?.suggestion_prompts ??
models[selectedModelIdx]?.info?.meta?.suggestion_prompts ??
$config?.default_prompt_suggestions ??
[]}
inputValue={prompt}
{onSelect}
/>
{#if $selectedFolder}
<div
class="mx-auto px-4 md:max-w-3xl md:px-6 font-primary min-h-62"
in:fade={{ duration: 200, delay: 200 }}
>
<FolderPlaceholder folder={$selectedFolder} />
</div>
</div>
{:else}
<div class="mx-auto max-w-2xl font-primary mt-2" in:fade={{ duration: 200, delay: 200 }}>
<div class="mx-5">
<Suggestions
suggestionPrompts={atSelectedModel?.info?.meta?.suggestion_prompts ??
models[selectedModelIdx]?.info?.meta?.suggestion_prompts ??
$config?.default_prompt_suggestions ??
[]}
inputValue={prompt}
{onSelect}
/>
</div>
</div>
{/if}
</div>

View File

@ -0,0 +1,103 @@
<script lang="ts">
import { getContext, onMount } from 'svelte';
const i18n = getContext('i18n');
import dayjs from 'dayjs';
import localizedFormat from 'dayjs/plugin/localizedFormat';
import { getTimeRange } from '$lib/utils';
dayjs.extend(localizedFormat);
export let chats = [];
let chatList = null;
const init = async () => {
if (chats.length === 0) {
chatList = [];
} else {
chatList = chats.map((chat) => ({
...chat,
time_range: getTimeRange(chat.updated_at)
}));
}
};
$: if (chats) {
init();
}
</script>
{#if chatList}
<div class="text-left text-sm w-full mb-3">
{#if chatList.length === 0}
<div
class="text-xs text-gray-500 dark:text-gray-400 text-center px-5 min-h-20 w-full h-full flex justify-center items-center"
>
{$i18n.t('No chats found')}
</div>
{/if}
{#each chatList as chat, idx (chat.id)}
{#if (idx === 0 || (idx > 0 && chat.time_range !== chatList[idx - 1].time_range)) && chat?.time_range}
<div
class="w-full text-xs text-gray-500 dark:text-gray-500 font-medium {idx === 0
? ''
: 'pt-5'} pb-2 px-2"
>
{$i18n.t(chat.time_range)}
<!-- localisation keys for time_range to be recognized from the i18next parser (so they don't get automatically removed):
{$i18n.t('Today')}
{$i18n.t('Yesterday')}
{$i18n.t('Previous 7 days')}
{$i18n.t('Previous 30 days')}
{$i18n.t('January')}
{$i18n.t('February')}
{$i18n.t('March')}
{$i18n.t('April')}
{$i18n.t('May')}
{$i18n.t('June')}
{$i18n.t('July')}
{$i18n.t('August')}
{$i18n.t('September')}
{$i18n.t('October')}
{$i18n.t('November')}
{$i18n.t('December')}
-->
</div>
{/if}
<a
class=" w-full flex justify-between items-center rounded-lg text-sm py-2 px-3 hover:bg-gray-50 dark:hover:bg-gray-850"
draggable="false"
href={`/c/${chat.id}`}
on:click={() => (show = false)}
>
<div class="text-ellipsis line-clamp-1 w-full sm:basis-3/5">
{chat?.title}
</div>
<div class="hidden sm:flex sm:basis-2/5 items-center justify-end">
<div class=" text-gray-500 dark:text-gray-400 text-xs">
{dayjs(chat?.updated_at * 1000).calendar()}
</div>
</div>
</a>
{/each}
<!-- {#if !allChatsLoaded && loadHandler}
<Loader
on:visible={(e) => {
if (!chatListLoading) {
loadHandler();
}
}}
>
<div class="w-full flex justify-center py-1 text-xs animate-pulse items-center gap-2">
<Spinner className=" size-4" />
<div class=" ">Loading...</div>
</div>
</Loader>
{/if} -->
</div>
{/if}

View File

@ -0,0 +1,51 @@
<script>
import { getContext } from 'svelte';
const i18n = getContext('i18n');
import { fade } from 'svelte/transition';
import ChatList from './ChatList.svelte';
import FolderKnowledge from './FolderKnowledge.svelte';
export let folder = null;
let selectedTab = 'chats';
</script>
<div>
<!-- <div class="mb-1">
<div
class="flex gap-1 scrollbar-none overflow-x-auto w-fit text-center text-sm font-medium rounded-full bg-transparent py-1 touch-auto pointer-events-auto"
>
<button
class="min-w-fit p-1.5 {selectedTab === 'knowledge'
? ''
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition"
type="button"
on:click={() => {
selectedTab = 'knowledge';
}}>{$i18n.t('Knowledge')}</button
>
<button
class="min-w-fit p-1.5 {selectedTab === 'chats'
? ''
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition"
type="button"
on:click={() => {
selectedTab = 'chats';
}}
>
{$i18n.t('Chats')}
</button>
</div>
</div> -->
<div class="">
{#if selectedTab === 'knowledge'}
<FolderKnowledge />
{:else if selectedTab === 'chats'}
<ChatList chats={folder?.items?.chats ?? []} />
{/if}
</div>
</div>

View File

@ -0,0 +1,147 @@
<script lang="ts">
import { getContext } from 'svelte';
const i18n = getContext('i18n');
import DOMPurify from 'dompurify';
import fileSaver from 'file-saver';
const { saveAs } = fileSaver;
import { toast } from 'svelte-sonner';
import { selectedFolder } from '$lib/stores';
import { deleteFolderById, updateFolderById } from '$lib/apis/folders';
import { getChatsByFolderId } from '$lib/apis/chats';
import FolderModal from '$lib/components/layout/Sidebar/Folders/FolderModal.svelte';
import Folder from '$lib/components/icons/Folder.svelte';
import XMark from '$lib/components/icons/XMark.svelte';
import FolderMenu from '$lib/components/layout/Sidebar/Folders/FolderMenu.svelte';
import EllipsisHorizontal from '$lib/components/icons/EllipsisHorizontal.svelte';
import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
export let folder = null;
export let onUpdate: Function = (folderId) => {};
export let onDelete: Function = (folderId) => {};
let showFolderModal = false;
let showDeleteConfirm = false;
const updateHandler = async ({ name, data }) => {
if (name === '') {
toast.error($i18n.t('Folder name cannot be empty.'));
return;
}
const currentName = folder.name;
name = name.trim();
folder.name = name;
const res = await updateFolderById(localStorage.token, folder.id, {
name,
...(data ? { data } : {})
}).catch((error) => {
toast.error(`${error}`);
folder.name = currentName;
return null;
});
if (res) {
folder.name = name;
if (data) {
folder.data = data;
}
toast.success($i18n.t('Folder updated successfully'));
selectedFolder.set(folder);
onUpdate(folder);
}
};
const deleteHandler = async () => {
const res = await deleteFolderById(localStorage.token, folder.id).catch((error) => {
toast.error(`${error}`);
return null;
});
if (res) {
toast.success($i18n.t('Folder deleted successfully'));
onDelete(folder);
}
};
const exportHandler = async () => {
const chats = await getChatsByFolderId(localStorage.token, folder.id).catch((error) => {
toast.error(`${error}`);
return null;
});
if (!chats) {
return;
}
const blob = new Blob([JSON.stringify(chats)], {
type: 'application/json'
});
saveAs(blob, `folder-${folder.name}-export-${Date.now()}.json`);
};
</script>
{#if folder}
<FolderModal bind:show={showFolderModal} edit={true} {folder} onSubmit={updateHandler} />
<DeleteConfirmDialog
bind:show={showDeleteConfirm}
title={$i18n.t('Delete folder?')}
on:confirm={() => {
deleteHandler();
}}
>
<div class=" text-sm text-gray-700 dark:text-gray-300 flex-1 line-clamp-3">
{@html DOMPurify.sanitize(
$i18n.t(
'This will delete <strong>{{NAME}}</strong> and <strong>all its contents</strong>.',
{
NAME: folder.name
}
)
)}
</div>
</DeleteConfirmDialog>
<div class="mb-3 px-6 @md:max-w-3xl justify-between w-full flex relative group items-center">
<div class="text-center flex gap-3.5 items-center">
<div class=" rounded-full bg-gray-50 dark:bg-gray-800 p-3 w-fit">
<Folder className="size-4.5" strokeWidth="2" />
</div>
<div class="text-3xl">
{folder.name}
</div>
</div>
<div class="flex items-center translate-x-2.5">
<FolderMenu
align="end"
onEdit={() => {
showFolderModal = true;
}}
onDelete={() => {
showDeleteConfirm = true;
}}
onExport={() => {
exportHandler();
}}
>
<button class="p-1.5 dark:hover:bg-gray-850 rounded-full touch-auto" on:click={(e) => {}}>
<EllipsisHorizontal className="size-4" strokeWidth="2.5" />
</button>
</FolderMenu>
</div>
</div>
{/if}

View File

@ -43,12 +43,16 @@
let largeTextAsFile = false;
let keepFollowUpPrompts = false;
let insertFollowUpPrompt = false;
let landingPageMode = '';
let chatBubble = true;
let chatDirection: 'LTR' | 'RTL' | 'auto' = 'auto';
let ctrlEnterToSend = false;
let copyFormatted = false;
let chatFadeStreamingText = true;
let collapseCodeBlocks = false;
let expandDetails = false;
@ -159,6 +163,11 @@
saveSettings({ imageCompression });
};
const toggleChatFadeStreamingText = async () => {
chatFadeStreamingText = !chatFadeStreamingText;
saveSettings({ chatFadeStreamingText: chatFadeStreamingText });
};
const toggleHapticFeedback = async () => {
hapticFeedback = !hapticFeedback;
saveSettings({ hapticFeedback: hapticFeedback });
@ -224,6 +233,16 @@
saveSettings({ insertPromptAsRichText });
};
const toggleKeepFollowUpPrompts = async () => {
keepFollowUpPrompts = !keepFollowUpPrompts;
saveSettings({ keepFollowUpPrompts });
};
const toggleInsertFollowUpPrompt = async () => {
insertFollowUpPrompt = !insertFollowUpPrompt;
saveSettings({ insertFollowUpPrompt });
};
const toggleLargeTextAsFile = async () => {
largeTextAsFile = !largeTextAsFile;
saveSettings({ largeTextAsFile });
@ -313,10 +332,15 @@
showEmojiInCall = $settings?.showEmojiInCall ?? false;
voiceInterruption = $settings?.voiceInterruption ?? false;
chatFadeStreamingText = $settings?.chatFadeStreamingText ?? true;
richTextInput = $settings?.richTextInput ?? true;
insertPromptAsRichText = $settings?.insertPromptAsRichText ?? false;
promptAutocomplete = $settings?.promptAutocomplete ?? false;
keepFollowUpPrompts = $settings?.keepFollowUpPrompts ?? false;
insertFollowUpPrompt = $settings?.insertFollowUpPrompt ?? false;
largeTextAsFile = $settings?.largeTextAsFile ?? false;
copyFormatted = $settings?.copyFormatted ?? false;
@ -746,6 +770,75 @@
</div>
</div>
<div>
<div class=" py-0.5 flex w-full justify-between">
<div id="fade-streaming-label" class=" self-center text-xs">
{$i18n.t('Fade Effect for Streaming Text')}
</div>
<button
aria-labelledby="fade-streaming-label"
class="p-1 px-3 text-xs flex rounded-sm transition"
on:click={() => {
toggleChatFadeStreamingText();
}}
type="button"
>
{#if chatFadeStreamingText === true}
<span class="ml-2 self-center">{$i18n.t('On')}</span>
{:else}
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
{/if}
</button>
</div>
</div>
<div>
<div class=" py-0.5 flex w-full justify-between">
<div id="keep-followup-prompts-label" class=" self-center text-xs">
{$i18n.t('Keep Follow-Up Prompts in Chat')}
</div>
<button
aria-labelledby="keep-followup-prompts-label"
class="p-1 px-3 text-xs flex rounded-sm transition"
on:click={() => {
toggleKeepFollowUpPrompts();
}}
type="button"
>
{#if keepFollowUpPrompts === true}
<span class="ml-2 self-center">{$i18n.t('On')}</span>
{:else}
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
{/if}
</button>
</div>
</div>
<div>
<div class=" py-0.5 flex w-full justify-between">
<div id="insert-followup-prompt-label" class=" self-center text-xs">
{$i18n.t('Insert Follow-Up Prompt to Input')}
</div>
<button
aria-labelledby="insert-followup-prompt-label"
class="p-1 px-3 text-xs flex rounded-sm transition"
on:click={() => {
toggleInsertFollowUpPrompt();
}}
type="button"
>
{#if insertFollowUpPrompt === true}
<span class="ml-2 self-center">{$i18n.t('On')}</span>
{:else}
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
{/if}
</button>
</div>
</div>
<div>
<div class=" py-0.5 flex w-full justify-between">
<div id="rich-input-label" class=" self-center text-xs">

View File

@ -50,7 +50,7 @@
}
</script>
<Modal size="xl" bind:show>
<Modal size="lg" bind:show>
<div>
<div class=" flex justify-between dark:text-gray-300 px-5 pt-4 pb-1">
<div class=" text-lg font-medium self-center">{$i18n.t('Memory')}</div>

View File

@ -537,7 +537,7 @@
}
</script>
<Modal size="xl" bind:show>
<Modal size="lg" bind:show>
<div class="text-gray-700 dark:text-gray-100">
<div class=" flex justify-between dark:text-gray-300 px-5 pt-4 pb-1">
<div class=" text-lg font-medium self-center">{$i18n.t('Settings')}</div>

View File

@ -133,9 +133,9 @@
>
<div class="flex items-center gap-1.5 text-xs">
{#if enableFullContent}
Using Entire Document
{$i18n.t('Using Entire Document')}
{:else}
Using Focused Retrieval
{$i18n.t('Using Focused Retrieval')}
{/if}
<Switch
bind:state={enableFullContent}

View File

@ -26,6 +26,14 @@
return 'w-[30rem]';
} else if (size === 'md') {
return 'w-[42rem]';
} else if (size === 'lg') {
return 'w-[56rem]';
} else if (size === 'xl') {
return 'w-[70rem]';
} else if (size === '2xl') {
return 'w-[84rem]';
} else if (size === '3xl') {
return 'w-[100rem]';
} else {
return 'w-[56rem]';
}

View File

@ -56,6 +56,7 @@
import { Fragment, DOMParser } from 'prosemirror-model';
import { EditorState, Plugin, PluginKey, TextSelection, Selection } from 'prosemirror-state';
import { Decoration, DecorationSet } from 'prosemirror-view';
import { Editor, Extension } from '@tiptap/core';
// Yjs imports
@ -72,32 +73,32 @@
import { keymap } from 'prosemirror-keymap';
import { AIAutocompletion } from './RichTextInput/AutoCompletion.js';
import Table from '@tiptap/extension-table';
import TableRow from '@tiptap/extension-table-row';
import TableHeader from '@tiptap/extension-table-header';
import TableCell from '@tiptap/extension-table-cell';
import Link from '@tiptap/extension-link';
import Underline from '@tiptap/extension-underline';
import TaskItem from '@tiptap/extension-task-item';
import TaskList from '@tiptap/extension-task-list';
import CharacterCount from '@tiptap/extension-character-count';
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight';
import Placeholder from '@tiptap/extension-placeholder';
import StarterKit from '@tiptap/starter-kit';
import Highlight from '@tiptap/extension-highlight';
import Typography from '@tiptap/extension-typography';
// Bubble and Floating menus are currently fixed to v2 due to styling issues in v3
// TODO: Update to v3 when styling issues are resolved
import BubbleMenu from '@tiptap/extension-bubble-menu';
import FloatingMenu from '@tiptap/extension-floating-menu';
import { TableKit } from '@tiptap/extension-table';
import { ListKit } from '@tiptap/extension-list';
import { Placeholder, CharacterCount } from '@tiptap/extensions';
import Image from './RichTextInput/Image/index.js';
// import TiptapImage from '@tiptap/extension-image';
import FileHandler from '@tiptap/extension-file-handler';
import Typography from '@tiptap/extension-typography';
import Highlight from '@tiptap/extension-highlight';
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight';
import { all, createLowlight } from 'lowlight';
import { PASTED_TEXT_CHARACTER_LIMIT } from '$lib/constants';
import FormattingButtons from './RichTextInput/FormattingButtons.svelte';
import { duration } from 'dayjs';
export let oncompositionstart = (e) => {};
export let oncompositionend = (e) => {};
@ -110,11 +111,64 @@
export let socket = null;
export let user = null;
export let files = [];
export let documentId = '';
export let className = 'input-prose';
export let placeholder = 'Type here...';
export let link = false;
export let image = false;
export let fileHandler = false;
export let onFileDrop = (currentEditor, files, pos) => {
files.forEach((file) => {
const fileReader = new FileReader();
fileReader.readAsDataURL(file);
fileReader.onload = () => {
currentEditor
.chain()
.insertContentAt(pos, {
type: 'image',
attrs: {
src: fileReader.result
}
})
.focus()
.run();
};
});
};
export let onFilePaste = (currentEditor, files, htmlContent) => {
files.forEach((file) => {
if (htmlContent) {
// if there is htmlContent, stop manual insertion & let other extensions handle insertion via inputRule
// you could extract the pasted file from this url string and upload it to a server for example
console.log(htmlContent); // eslint-disable-line no-console
return false;
}
const fileReader = new FileReader();
fileReader.readAsDataURL(file);
fileReader.onload = () => {
currentEditor
.chain()
.insertContentAt(currentEditor.state.selection.anchor, {
type: 'image',
attrs: {
src: fileReader.result
}
})
.focus()
.run();
};
});
};
export let onSelectionUpdate = (e) => {};
export let id = '';
export let value = '';
@ -141,11 +195,21 @@
let jsonValue = '';
let mdValue = '';
let lastSelectionBookmark = null;
// Yjs setup
let ydoc = null;
let yXmlFragment = null;
let awareness = null;
const getEditorInstance = async () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(editor);
}, 0);
});
};
// Custom Yjs Socket.IO provider
class SocketIOProvider {
constructor(doc, documentId, socket, user) {
@ -217,11 +281,27 @@
if (state.length === 2 && state[0] === 0 && state[1] === 0) {
// Empty state, check if we have content to initialize
// check if editor empty as well
// const editor = await getEditorInstance();
const isEmptyEditor = !editor || editor.getText().trim() === '';
if (content && isEmptyEditor && (data?.sessions ?? ['']).length === 1) {
const editorYdoc = prosemirrorJSONToYDoc(editor.schema, content);
if (editorYdoc) {
Y.applyUpdate(this.doc, Y.encodeStateAsUpdate(editorYdoc));
if (isEmptyEditor) {
if (content && (data?.sessions ?? ['']).length === 1) {
const editorYdoc = prosemirrorJSONToYDoc(editor.schema, content);
if (editorYdoc) {
Y.applyUpdate(this.doc, Y.encodeStateAsUpdate(editorYdoc));
}
}
} else {
// If the editor already has content, we don't need to send an empty state
if (this.doc.getXmlFragment('prosemirror').length > 0) {
this.socket.emit('ydoc:document:update', {
document_id: this.documentId,
user_id: this.user?.id,
socket_id: this.socket.id,
update: Y.encodeStateAsUpdate(this.doc)
});
} else {
console.warn('Yjs document is empty, not sending state.');
}
}
} else {
@ -580,6 +660,10 @@
export const setText = (text: string) => {
if (!editor) return;
text = text.replaceAll('\n\n', '\n');
// reset the editor content
editor.commands.clearContent();
const { state, view } = editor;
const { schema, tr } = state;
@ -748,6 +832,33 @@
}
};
const SelectionDecoration = Extension.create({
name: 'selectionDecoration',
addProseMirrorPlugins() {
return [
new Plugin({
key: new PluginKey('selection'),
props: {
decorations: (state) => {
const { selection } = state;
const { focused } = this.editor;
if (focused || selection.empty) {
return null;
}
return DecorationSet.create(state.doc, [
Decoration.inline(selection.from, selection.to, {
class: 'editor-selection'
})
]);
}
}
})
];
}
});
onMount(async () => {
content = value;
@ -794,35 +905,42 @@
initializeCollaboration();
}
console.log(bubbleMenuElement, floatingMenuElement);
editor = new Editor({
element: element,
extensions: [
StarterKit,
StarterKit.configure({
link: link
}),
Placeholder.configure({ placeholder }),
SelectionDecoration,
CodeBlockLowlight.configure({
lowlight
}),
Highlight,
Typography,
Underline,
Placeholder.configure({ placeholder }),
Table.configure({ resizable: true }),
TableRow,
TableHeader,
TableCell,
TaskList,
TaskItem.configure({
nested: true
TableKit.configure({
table: { resizable: true }
}),
ListKit.configure({
taskItem: {
nested: true
}
}),
CharacterCount.configure({}),
...(link
...(image ? [Image] : []),
...(fileHandler
? [
Link.configure({
openOnClick: true,
linkOnPaste: true
FileHandler.configure({
onDrop: onFileDrop,
onPaste: onFilePaste
})
]
: []),
...(autocomplete
? [
AIAutocompletion.configure({
@ -873,6 +991,7 @@
onTransaction: () => {
// force re-render so `editor.isActive` works as expected
editor = editor;
if (!editor) return;
htmlValue = editor.getHTML();
jsonValue = editor.getJSON();
@ -1063,7 +1182,10 @@
const hasImageItem = Array.from(event.clipboardData.items).some((item) =>
item.type.startsWith('image/')
);
if (hasImageFile || hasImageItem) {
const hasFile = Array.from(event.clipboardData.files).length > 0;
if (hasImageFile || hasImageItem || hasFile) {
eventDispatch('paste', { event });
event.preventDefault();
return true;
@ -1074,7 +1196,13 @@
return false;
}
}
}
},
onBeforeCreate: ({ editor }) => {
if (files) {
editor.storage.files = files;
}
},
onSelectionUpdate: onSelectionUpdate
});
if (messageInput) {
@ -1146,11 +1274,11 @@
</script>
{#if showFormattingButtons}
<div bind:this={bubbleMenuElement} class="p-0">
<div bind:this={bubbleMenuElement} id="bubble-menu" class="p-0">
<FormattingButtons {editor} />
</div>
<div bind:this={floatingMenuElement} class="p-0">
<div bind:this={floatingMenuElement} id="floating-menu" class="p-0">
<FormattingButtons {editor} />
</div>
{/if}

View File

@ -17,6 +17,8 @@
import Tooltip from '../Tooltip.svelte';
import CheckBox from '$lib/components/icons/CheckBox.svelte';
import ArrowLeftTag from '$lib/components/icons/ArrowLeftTag.svelte';
import ArrowRightTag from '$lib/components/icons/ArrowRightTag.svelte';
</script>
<div
@ -58,6 +60,31 @@
</button>
</Tooltip>
{#if editor?.isActive('bulletList') || editor?.isActive('orderedList') || editor?.isActive('taskList')}
<Tooltip placement="top" content={$i18n.t('Lift List')}>
<button
on:click={() => {
editor?.commands.liftListItem(editor?.isActive('taskList') ? 'taskItem' : 'listItem');
}}
class="hover:bg-gray-50 dark:hover:bg-gray-700 rounded-lg p-1.5 transition-all"
type="button"
>
<ArrowLeftTag />
</button>
</Tooltip>
<Tooltip placement="top" content={$i18n.t('Sink List')}>
<button
on:click={() =>
editor?.commands.sinkListItem(editor?.isActive('taskList') ? 'taskItem' : 'listItem')}
class="hover:bg-gray-50 dark:hover:bg-gray-700 rounded-lg p-1.5 transition-all"
type="button"
>
<ArrowRightTag />
</button>
</Tooltip>
{/if}
<Tooltip placement="top" content={$i18n.t('Bullet List')}>
<button
on:click={() => editor?.chain().focus().toggleBulletList().run()}

View File

@ -0,0 +1,197 @@
import { mergeAttributes, Node, nodeInputRule } from '@tiptap/core';
export interface ImageOptions {
/**
* Controls if the image node should be inline or not.
* @default false
* @example true
*/
inline: boolean;
/**
* Controls if base64 images are allowed. Enable this if you want to allow
* base64 image urls in the `src` attribute.
* @default false
* @example true
*/
allowBase64: boolean;
/**
* HTML attributes to add to the image element.
* @default {}
* @example { class: 'foo' }
*/
HTMLAttributes: Record<string, any>;
}
export interface SetImageOptions {
src: string;
alt?: string;
title?: string;
width?: number;
height?: number;
}
declare module '@tiptap/core' {
interface Commands<ReturnType> {
image: {
/**
* Add an image
* @param options The image attributes
* @example
* editor
* .commands
* .setImage({ src: 'https://tiptap.dev/logo.png', alt: 'tiptap', title: 'tiptap logo' })
*/
setImage: (options: SetImageOptions) => ReturnType;
};
}
}
/**
* Matches an image to a ![image](src "title") on input.
*/
export const inputRegex = /(?:^|\s)(!\[(.+|:?)]\((\S+)(?:(?:\s+)["'](\S+)["'])?\))$/;
/**
* This extension allows you to insert images.
* @see https://www.tiptap.dev/api/nodes/image
*/
export const Image = Node.create<ImageOptions>({
name: 'image',
addOptions() {
return {
inline: false,
allowBase64: false,
HTMLAttributes: {}
};
},
inline() {
return this.options.inline;
},
group() {
return this.options.inline ? 'inline' : 'block';
},
draggable: true,
addAttributes() {
return {
file: {
default: null
},
src: {
default: null
},
alt: {
default: null
},
title: {
default: null
},
width: {
default: null
},
height: {
default: null
}
};
},
parseHTML() {
return [
{
tag: this.options.allowBase64 ? 'img[src]' : 'img[src]:not([src^="data:"])'
}
];
},
renderHTML({ HTMLAttributes }) {
if (HTMLAttributes.file) {
delete HTMLAttributes.file;
}
return ['img', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)];
},
addNodeView() {
return ({ node, editor }) => {
const domImg = document.createElement('img');
domImg.setAttribute('src', node.attrs.src || '');
domImg.setAttribute('alt', node.attrs.alt || '');
domImg.setAttribute('title', node.attrs.title || '');
const container = document.createElement('div');
const img = document.createElement('img');
const fileId = node.attrs.src.replace('data://', '');
img.setAttribute('id', `image:${fileId}`);
img.classList.add('rounded-md', 'max-h-72', 'w-fit', 'object-contain');
const editorFiles = editor.storage?.files || [];
if (editorFiles && node.attrs.src.startsWith('data://')) {
const file = editorFiles.find((f) => f.id === fileId);
if (file) {
img.setAttribute('src', file.url || '');
} else {
img.setAttribute('src', '/image-placeholder.png');
}
} else {
img.setAttribute('src', node.attrs.src || '');
}
img.setAttribute('alt', node.attrs.alt || '');
img.setAttribute('title', node.attrs.title || '');
img.addEventListener('data', (e) => {
const files = e?.files || [];
if (files && node.attrs.src.startsWith('data://')) {
const file = editorFiles.find((f) => f.id === fileId);
if (file) {
img.setAttribute('src', file.url || '');
} else {
img.setAttribute('src', '/image-placeholder.png');
}
}
});
container.append(img);
return {
dom: img,
contentDOM: domImg
};
};
},
addCommands() {
return {
setImage:
(options) =>
({ commands }) => {
return commands.insertContent({
type: this.name,
attrs: options
});
}
};
},
addInputRules() {
return [
nodeInputRule({
find: inputRegex,
type: this.type,
getAttributes: (match) => {
const [, , alt, src, title] = match;
return { src, alt, title };
}
})
];
}
});

View File

@ -0,0 +1,5 @@
import { Image } from './image.js';
export * from './image.js';
export default Image;

View File

@ -0,0 +1,19 @@
<script lang="ts">
export let className = 'w-4 h-4';
export let strokeWidth = '1.5';
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width={strokeWidth}
stroke="currentColor"
class={className}
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M10.5 6h9.75M10.5 6a1.5 1.5 0 1 1-3 0m3 0a1.5 1.5 0 1 0-3 0M3.75 6H7.5m3 12h9.75m-9.75 0a1.5 1.5 0 0 1-3 0m3 0a1.5 1.5 0 0 0-3 0m-3.75 0H7.5m9-6h3.75m-3.75 0a1.5 1.5 0 0 1-3 0m3 0a1.5 1.5 0 0 0-3 0m-9.75 0h9.75"
/>
</svg>

View File

@ -0,0 +1,20 @@
<script lang="ts">
export let className = 'size-4';
export let strokeWidth = '1.5';
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width={strokeWidth}
stroke="currentColor"
class={className}
><path
d="M16.75 12H6.75M6.75 12L9.5 14.75M6.75 12L9.5 9.25"
stroke-linecap="round"
stroke-linejoin="round"
></path><path
d="M2 15V9C2 6.79086 3.79086 5 6 5H18C20.2091 5 22 6.79086 22 9V15C22 17.2091 20.2091 19 18 19H6C3.79086 19 2 17.2091 2 15Z"
></path></svg
>

View File

@ -0,0 +1,20 @@
<script lang="ts">
export let className = 'size-4';
export let strokeWidth = '1.5';
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width={strokeWidth}
stroke="currentColor"
class={className}
><path
d="M6.75 12H16.75M16.75 12L14 14.75M16.75 12L14 9.25"
stroke-linecap="round"
stroke-linejoin="round"
></path><path
d="M2 15V9C2 6.79086 3.79086 5 6 5H18C20.2091 5 22 6.79086 22 9V15C22 17.2091 20.2091 19 18 19H6C3.79086 19 2 17.2091 2 15Z"
></path></svg
>

View File

@ -1,16 +1,19 @@
<script lang="ts">
import { toast } from 'svelte-sonner';
import { getContext, onMount } from 'svelte';
import { getContext, onDestroy, onMount, tick } from 'svelte';
const i18n = getContext('i18n');
import Modal from '$lib/components/common/Modal.svelte';
import SearchInput from './Sidebar/SearchInput.svelte';
import { getChatList, getChatListBySearchText } from '$lib/apis/chats';
import { getChatById, getChatList, getChatListBySearchText } from '$lib/apis/chats';
import Spinner from '../common/Spinner.svelte';
import dayjs from '$lib/dayjs';
import calendar from 'dayjs/plugin/calendar';
import Loader from '../common/Loader.svelte';
import { createMessagesList } from '$lib/utils';
import { user } from '$lib/stores';
import Messages from '../chat/Messages.svelte';
dayjs.extend(calendar);
export let show = false;
@ -28,6 +31,60 @@
let selectedIdx = 0;
let selectedChat = null;
let selectedModels = [''];
let history = null;
let messages = null;
$: if (!chatListLoading && chatList) {
loadChatPreview(selectedIdx);
}
const loadChatPreview = async (selectedIdx) => {
if (!chatList || chatList.length === 0) {
selectedChat = null;
messages = null;
history = null;
selectedModels = [''];
return;
}
const chatId = chatList[selectedIdx].id;
const chat = await getChatById(localStorage.token, chatId).catch(async (error) => {
return null;
});
if (chat) {
if (chat?.chat?.history) {
selectedModels =
(chat?.chat?.models ?? undefined) !== undefined
? chat?.chat?.models
: [chat?.chat?.models ?? ''];
history = chat?.chat?.history;
messages = createMessagesList(chat?.chat?.history, chat?.chat?.history?.currentId);
// scroll to the bottom of the messages container
await tick();
const messagesContainerElement = document.getElementById('chat-preview');
if (messagesContainerElement) {
messagesContainerElement.scrollTop = messagesContainerElement.scrollHeight;
}
} else {
messages = [];
}
} else {
toast.error($i18n.t('Failed to load chat preview'));
selectedChat = null;
messages = null;
history = null;
selectedModels = [''];
return;
}
};
const searchHandler = async () => {
if (searchDebounceTimeout) {
clearTimeout(searchDebounceTimeout);
@ -43,6 +100,11 @@
}, 500);
}
selectedChat = null;
messages = null;
history = null;
selectedModels = [''];
if ((chatList ?? []).length === 0) {
allChatsLoaded = true;
} else {
@ -76,12 +138,67 @@
searchHandler();
};
const onKeyDown = (e) => {
if (e.code === 'Escape') {
show = false;
onClose();
} else if (e.code === 'Enter' && (chatList ?? []).length > 0) {
const item = document.querySelector(`[data-arrow-selected="true"]`);
if (item) {
item?.click();
}
show = false;
return;
} else if (e.code === 'ArrowDown') {
const searchInput = document.getElementById('search-input');
if (searchInput) {
// check if focused on the search input
if (document.activeElement === searchInput) {
searchInput.blur();
selectedIdx = 0;
return;
}
}
selectedIdx = Math.min(selectedIdx + 1, (chatList ?? []).length - 1);
} else if (e.code === 'ArrowUp') {
if (selectedIdx === 0) {
const searchInput = document.getElementById('search-input');
if (searchInput) {
// check if focused on the search input
if (document.activeElement !== searchInput) {
searchInput.focus();
selectedIdx = 0;
return;
}
}
}
selectedIdx = Math.max(selectedIdx - 1, 0);
}
const item = document.querySelector(`[data-arrow-selected="true"]`);
item?.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'instant' });
};
onMount(() => {
init();
document.addEventListener('keydown', onKeyDown);
});
onDestroy(() => {
if (searchDebounceTimeout) {
clearTimeout(searchDebounceTimeout);
}
document.removeEventListener('keydown', onKeyDown);
});
</script>
<Modal size="md" bind:show>
<Modal size="xl" bind:show>
<div class="py-2.5 dark:text-gray-300 text-gray-700">
<div class="px-3.5 pb-1.5">
<SearchInput
@ -116,23 +233,26 @@
<!-- <hr class="border-gray-100 dark:border-gray-850 my-1" /> -->
<div class="flex flex-col overflow-y-auto h-80 scrollbar-hidden px-3 pb-1">
{#if chatList}
{#if chatList.length === 0}
<div class="text-xs text-gray-500 dark:text-gray-400 text-center px-5">
{$i18n.t('No results found')}
</div>
{/if}
<div class="flex px-3 pb-1">
<div
class="flex flex-col overflow-y-auto h-96 md:h-[40rem] max-h-full scrollbar-hidden w-full flex-1"
>
{#if chatList}
{#if chatList.length === 0}
<div class="text-xs text-gray-500 dark:text-gray-400 text-center px-5">
{$i18n.t('No results found')}
</div>
{/if}
{#each chatList as chat, idx (chat.id)}
{#if idx === 0 || (idx > 0 && chat.time_range !== chatList[idx - 1].time_range)}
<div
class="w-full text-xs text-gray-500 dark:text-gray-500 font-medium {idx === 0
? ''
: 'pt-5'} pb-2 px-2"
>
{$i18n.t(chat.time_range)}
<!-- localisation keys for time_range to be recognized from the i18next parser (so they don't get automatically removed):
{#each chatList as chat, idx (chat.id)}
{#if idx === 0 || (idx > 0 && chat.time_range !== chatList[idx - 1].time_range)}
<div
class="w-full text-xs text-gray-500 dark:text-gray-500 font-medium {idx === 0
? ''
: 'pt-5'} pb-2 px-2"
>
{$i18n.t(chat.time_range)}
<!-- localisation keys for time_range to be recognized from the i18next parser (so they don't get automatically removed):
{$i18n.t('Today')}
{$i18n.t('Yesterday')}
{$i18n.t('Previous 7 days')}
@ -150,56 +270,84 @@
{$i18n.t('November')}
{$i18n.t('December')}
-->
</div>
{/if}
<a
class=" w-full flex justify-between items-center rounded-lg text-sm py-2 px-3 hover:bg-gray-50 dark:hover:bg-gray-850 {selectedIdx ===
idx
? 'bg-gray-50 dark:bg-gray-850'
: ''}"
href="/c/{chat.id}"
draggable="false"
data-arrow-selected={selectedIdx === idx ? 'true' : undefined}
on:mouseenter={() => {
selectedIdx = idx;
}}
on:click={() => {
show = false;
onClose();
}}
>
<div class=" flex-1">
<div class="text-ellipsis line-clamp-1 w-full">
{chat?.title}
</div>
</div>
{/if}
<div class=" pl-3 shrink-0 text-gray-500 dark:text-gray-400 text-xs">
{dayjs(chat?.updated_at * 1000).calendar()}
</div>
</a>
{/each}
<a
class=" w-full flex justify-between items-center rounded-lg text-sm py-2 px-3 hover:bg-gray-50 dark:hover:bg-gray-850 {selectedIdx ===
idx
? 'bg-gray-50 dark:bg-gray-850'
: ''}"
href="/c/{chat.id}"
draggable="false"
data-arrow-selected={selectedIdx === idx ? 'true' : undefined}
on:mouseenter={() => {
selectedIdx = idx;
}}
on:click={() => {
show = false;
onClose();
}}
>
<div class=" flex-1">
<div class="text-ellipsis line-clamp-1 w-full">
{chat?.title}
</div>
</div>
{#if !allChatsLoaded}
<Loader
on:visible={(e) => {
if (!chatListLoading) {
loadMoreChats();
}
}}
>
<div class="w-full flex justify-center py-1 text-xs animate-pulse items-center gap-2">
<Spinner className=" size-4" />
<div class=" ">Loading...</div>
</div>
</Loader>
<div class=" pl-3 shrink-0 text-gray-500 dark:text-gray-400 text-xs">
{dayjs(chat?.updated_at * 1000).calendar()}
</div>
</a>
{/each}
{#if !allChatsLoaded}
<Loader
on:visible={(e) => {
if (!chatListLoading) {
loadMoreChats();
}
}}
>
<div class="w-full flex justify-center py-1 text-xs animate-pulse items-center gap-2">
<Spinner className=" size-4" />
<div class=" ">Loading...</div>
</div>
</Loader>
{/if}
{:else}
<div class="w-full h-full flex justify-center items-center">
<Spinner className="size-5" />
</div>
{/if}
{:else}
<div class="w-full h-full flex justify-center items-center">
<Spinner className="size-5" />
</div>
{/if}
</div>
<div
id="chat-preview"
class="hidden md:flex md:flex-1 w-full overflow-y-auto h-96 md:h-[40rem] scrollbar-hidden"
>
{#if messages === null}
<div
class="w-full h-full flex justify-center items-center text-gray-500 dark:text-gray-400 text-sm"
>
{$i18n.t('Select a conversation to preview')}
</div>
{:else}
<div class="w-full h-full flex flex-col">
<Messages
className="h-full flex pt-4 pb-8 w-full"
user={$user}
readOnly={true}
{selectedModels}
bind:history
bind:messages
autoScroll={true}
sendPrompt={() => {}}
continueResponse={() => {}}
regenerateResponse={() => {}}
/>
</div>
{/if}
</div>
</div>
</div>
</Modal>

View File

@ -58,6 +58,7 @@
import Home from '../icons/Home.svelte';
import Search from '../icons/Search.svelte';
import SearchModal from './SearchModal.svelte';
import FolderModal from './Sidebar/Folders/FolderModal.svelte';
const BREAKPOINT = 768;
@ -74,6 +75,7 @@
let chatListLoading = false;
let allChatsLoaded = false;
let showCreateFolderModal = false;
let folders = {};
let newFolderId = null;
@ -117,7 +119,7 @@
}
};
const createFolder = async (name = 'Untitled') => {
const createFolder = async ({ name, data }) => {
if (name === '') {
toast.error($i18n.t('Folder name cannot be empty.'));
return;
@ -148,13 +150,16 @@
}
};
const res = await createNewFolder(localStorage.token, name).catch((error) => {
const res = await createNewFolder(localStorage.token, {
name,
data
}).catch((error) => {
toast.error(`${error}`);
return null;
});
if (res) {
newFolderId = res.id;
// newFolderId = res.id;
await initFolders();
}
};
@ -363,9 +368,7 @@
});
chats.subscribe((value) => {
if ($selectedFolder) {
initFolders();
}
initFolders();
});
await initChannels();
@ -431,6 +434,14 @@
}}
/>
<FolderModal
bind:show={showCreateFolderModal}
onSubmit={async (folder) => {
await createFolder(folder);
showCreateFolderModal = false;
}}
/>
<!-- svelte-ignore a11y-no-static-element-interactions -->
{#if $showSidebar}
@ -734,11 +745,12 @@
className="px-2 mt-0.5"
name={$i18n.t('Chats')}
onAdd={() => {
createFolder();
showCreateFolderModal = true;
}}
onAddLabel={$i18n.t('New Folder')}
on:change={(e) => {
on:change={async (e) => {
selectedFolder.set(null);
await goto('/');
}}
on:import={(e) => {
importChatHandler(e.detail);

View File

@ -12,6 +12,7 @@
import Tooltip from '$lib/components/common/Tooltip.svelte';
import Download from '$lib/components/icons/Download.svelte';
export let align: 'start' | 'end' = 'start';
export let onEdit = () => {};
export let onExport = () => {};
export let onDelete = () => {};
@ -36,7 +37,7 @@
class="w-full max-w-[170px] rounded-lg px-1 py-1.5 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg"
sideOffset={-2}
side="bottom"
align="start"
{align}
transition={flyAndScale}
>
<DropdownMenu.Item

View File

@ -10,11 +10,14 @@
import { goto } from '$app/navigation';
import Textarea from '$lib/components/common/Textarea.svelte';
import Knowledge from '$lib/components/workspace/Models/Knowledge.svelte';
import { user } from '$lib/stores';
const i18n = getContext('i18n');
export let show = false;
export let onSubmit: Function = (e) => {};
export let edit = false;
export let folder = null;
let name = '';
@ -46,13 +49,25 @@
$: if (folder) {
init();
}
$: if (!show && !edit) {
name = '';
data = {
system_prompt: '',
files: []
};
}
</script>
<Modal size="md" bind:show>
<div>
<div class=" flex justify-between dark:text-gray-300 px-5 pt-4 pb-1">
<div class=" text-lg font-medium self-center">
{$i18n.t('Edit Folder')}
{#if edit}
{$i18n.t('Edit Folder')}
{:else}
{$i18n.t('Create Folder')}
{/if}
</div>
<button
class="self-center"
@ -88,17 +103,19 @@
<hr class=" border-gray-50 dark:border-gray-850 my-2.5 w-full" />
<div class="my-1">
<div class="mb-2 text-xs text-gray-500">{$i18n.t('System Prompt')}</div>
<div>
<Textarea
className=" text-sm w-full bg-transparent outline-hidden "
placeholder={`Write your model system prompt content here\ne.g.) You are Mario from Super Mario Bros, acting as an assistant.`}
maxSize={200}
bind:value={data.system_prompt}
/>
{#if $user?.role === 'admin' || ($user?.permissions.chat?.system_prompt ?? true)}
<div class="my-1">
<div class="mb-2 text-xs text-gray-500">{$i18n.t('System Prompt')}</div>
<div>
<Textarea
className=" text-sm w-full bg-transparent outline-hidden "
placeholder={`Write your model system prompt content here\ne.g.) You are Mario from Super Mario Bros, acting as an assistant.`}
maxSize={200}
bind:value={data.system_prompt}
/>
</div>
</div>
</div>
{/if}
<div class="my-2">
<Knowledge bind:selectedItems={data.files}>

View File

@ -36,7 +36,7 @@
import ChatItem from './ChatItem.svelte';
import FolderMenu from './Folders/FolderMenu.svelte';
import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
import EditFolderModal from './Folders/EditFolderModal.svelte';
import FolderModal from './Folders/FolderModal.svelte';
import { goto } from '$app/navigation';
export let open = false;
@ -53,7 +53,7 @@
let folderElement;
let showEditFolderModal = false;
let showFolderModal = false;
let edit = false;
let draggedOver = false;
@ -378,8 +378,9 @@
</div>
</DeleteConfirmDialog>
<EditFolderModal
bind:show={showEditFolderModal}
<FolderModal
bind:show={showFolderModal}
edit={true}
folder={folders[folderId]}
onSubmit={updateHandler}
/>
@ -426,10 +427,9 @@
renameHandler();
}}
on:click={async (e) => {
await goto('/');
selectedFolder.set(folders[folderId]);
if ($chatId) {
await goto('/');
}
}}
>
<div class="text-gray-300 dark:text-gray-600">
@ -483,7 +483,7 @@
>
<FolderMenu
onEdit={() => {
showEditFolderModal = true;
showFolderModal = true;
}}
onDelete={() => {
showDeleteConfirm = true;

View File

@ -96,6 +96,7 @@
</div>
<input
id="search-input"
class="w-full rounded-r-xl py-1.5 pl-2.5 text-sm bg-transparent dark:text-gray-300 outline-hidden"
placeholder={placeholder ? placeholder : $i18n.t('Search')}
bind:value
@ -106,6 +107,9 @@
focused = true;
initTags();
}}
on:blur={() => {
focused = false;
}}
on:keydown={(e) => {
if (e.key === 'Enter') {
if (filteredTags.length > 0) {

View File

@ -9,7 +9,7 @@
import { getUsage } from '$lib/apis';
import { userSignOut } from '$lib/apis/auths';
import { showSettings, mobile, showSidebar, user } from '$lib/stores';
import { showSettings, mobile, showSidebar, showShortcuts, user } from '$lib/stores';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import ArchiveBox from '$lib/components/icons/ArchiveBox.svelte';
@ -29,8 +29,6 @@
export let help = false;
export let className = 'max-w-[240px]';
let showShortcuts = false;
const dispatch = createEventDispatcher();
let usage = null;
@ -51,7 +49,7 @@
}
</script>
<ShortcutsModal bind:show={showShortcuts} />
<ShortcutsModal bind:show={$showShortcuts} />
<!-- svelte-ignore a11y-no-static-element-interactions -->
<DropdownMenu.Root
@ -173,7 +171,7 @@
class="flex gap-2 items-center py-1.5 px-3 text-sm select-none w-full cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md transition"
id="chat-share-button"
on:click={() => {
showShortcuts = !showShortcuts;
showShortcuts.set(!$showShortcuts);
show = false;
}}
>

View File

@ -34,10 +34,10 @@
import { config, models, settings, showSidebar, socket, user, WEBUI_NAME } from '$lib/stores';
import NotePanel from '$lib/components/notes/NotePanel.svelte';
import MenuLines from '../icons/MenuLines.svelte';
import ChatBubbleOval from '../icons/ChatBubbleOval.svelte';
import Settings from './NoteEditor/Settings.svelte';
import Controls from './NoteEditor/Controls.svelte';
import Chat from './NoteEditor/Chat.svelte';
import AccessControlModal from '$lib/components/workspace/common/AccessControlModal.svelte';
async function loadLocale(locales) {
@ -61,6 +61,8 @@
import MicSolid from '../icons/MicSolid.svelte';
import VoiceRecording from '../chat/MessageInput/VoiceRecording.svelte';
import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
import MenuLines from '../icons/MenuLines.svelte';
import ChatBubbleOval from '../icons/ChatBubbleOval.svelte';
import Calendar from '../icons/Calendar.svelte';
import Users from '../icons/Users.svelte';
@ -81,6 +83,7 @@
import ArrowRight from '../icons/ArrowRight.svelte';
import Cog6 from '../icons/Cog6.svelte';
import AiMenu from './AIMenu.svelte';
import AdjustmentsHorizontalOutline from '../icons/AdjustmentsHorizontalOutline.svelte';
export let id: null | string = null;
@ -118,6 +121,8 @@
let showPanel = false;
let selectedPanel = 'chat';
let selectedContent = null;
let showDeleteConfirm = false;
let showAccessControlModal = false;
@ -382,6 +387,13 @@ ${content}
files = [...files, fileItem];
// open the settings panel if it is not open
selectedPanel = 'settings';
if (!showPanel) {
showPanel = true;
}
try {
// If the file is an audio file, provide the language for STT.
let metadata = null;
@ -432,95 +444,115 @@ ${content}
note.data.files = null;
}
editor.storage.files = files;
changeDebounceHandler();
return fileItem;
};
const inputFilesHandler = async (inputFiles) => {
console.log('Input files handler called with:', inputFiles);
inputFiles.forEach(async (file) => {
console.log('Processing file:', {
name: file.name,
type: file.type,
size: file.size,
extension: file.name.split('.').at(-1)
const compressImageHandler = async (imageUrl, settings = {}, config = {}) => {
// Quick shortcut so we dont do unnecessary work.
const settingsCompression = settings?.imageCompression ?? false;
const configWidth = config?.file?.image_compression?.width ?? null;
const configHeight = config?.file?.image_compression?.height ?? null;
// If neither settings nor config wants compression, return original URL.
if (!settingsCompression && !configWidth && !configHeight) {
return imageUrl;
}
// Default to null (no compression unless set)
let width = null;
let height = null;
// If user/settings want compression, pick their preferred size.
if (settingsCompression) {
width = settings?.imageCompressionSize?.width ?? null;
height = settings?.imageCompressionSize?.height ?? null;
}
// Apply config limits as an upper bound if any
if (configWidth && (width === null || width > configWidth)) {
width = configWidth;
}
if (configHeight && (height === null || height > configHeight)) {
height = configHeight;
}
// Do the compression if required
if (width || height) {
return await compressImage(imageUrl, width, height);
}
return imageUrl;
};
const inputFileHandler = async (file) => {
console.log('Processing file:', {
name: file.name,
type: file.type,
size: file.size,
extension: file.name.split('.').at(-1)
});
if (
($config?.file?.max_size ?? null) !== null &&
file.size > ($config?.file?.max_size ?? 0) * 1024 * 1024
) {
console.log('File exceeds max size limit:', {
fileSize: file.size,
maxSize: ($config?.file?.max_size ?? 0) * 1024 * 1024
});
toast.error(
$i18n.t(`File size should not exceed {{maxSize}} MB.`, {
maxSize: $config?.file?.max_size
})
);
return;
}
if (
($config?.file?.max_size ?? null) !== null &&
file.size > ($config?.file?.max_size ?? 0) * 1024 * 1024
) {
console.log('File exceeds max size limit:', {
fileSize: file.size,
maxSize: ($config?.file?.max_size ?? 0) * 1024 * 1024
});
toast.error(
$i18n.t(`File size should not exceed {{maxSize}} MB.`, {
maxSize: $config?.file?.max_size
})
);
return;
}
if (file['type'].startsWith('image/')) {
const compressImageHandler = async (imageUrl, settings = {}, config = {}) => {
// Quick shortcut so we dont do unnecessary work.
const settingsCompression = settings?.imageCompression ?? false;
const configWidth = config?.file?.image_compression?.width ?? null;
const configHeight = config?.file?.image_compression?.height ?? null;
// If neither settings nor config wants compression, return original URL.
if (!settingsCompression && !configWidth && !configHeight) {
return imageUrl;
}
// Default to null (no compression unless set)
let width = null;
let height = null;
// If user/settings want compression, pick their preferred size.
if (settingsCompression) {
width = settings?.imageCompressionSize?.width ?? null;
height = settings?.imageCompressionSize?.height ?? null;
}
// Apply config limits as an upper bound if any
if (configWidth && (width === null || width > configWidth)) {
width = configWidth;
}
if (configHeight && (height === null || height > configHeight)) {
height = configHeight;
}
// Do the compression if required
if (width || height) {
return await compressImage(imageUrl, width, height);
}
return imageUrl;
};
if (file['type'].startsWith('image/')) {
const uploadImagePromise = new Promise(async (resolve, reject) => {
let reader = new FileReader();
reader.onload = async (event) => {
let imageUrl = event.target.result;
try {
let imageUrl = event.target.result;
imageUrl = await compressImageHandler(imageUrl, $settings, $config);
imageUrl = await compressImageHandler(imageUrl, $settings, $config);
files = [
...files,
{
const fileId = uuidv4();
const fileItem = {
id: fileId,
type: 'image',
url: `${imageUrl}`
}
];
note.data.files = files;
};
files = [...files, fileItem];
note.data.files = files;
editor.storage.files = files;
changeDebounceHandler();
resolve(fileItem);
} catch (err) {
reject(err);
}
};
reader.readAsDataURL(
file['type'] === 'image/heic'
? await heic2any({ blob: file, toType: 'image/jpeg' })
: file
);
} else {
uploadFileHandler(file);
}
});
return await uploadImagePromise;
} else {
return await uploadFileHandler(file);
}
};
const inputFilesHandler = async (inputFiles) => {
console.log('Input files handler called with:', inputFiles);
inputFiles.forEach(async (file) => {
await inputFileHandler(file);
});
};
@ -725,9 +757,25 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
const onDragOver = (e) => {
e.preventDefault();
// Check if a file is being dragged.
if (e.dataTransfer?.types?.includes('Files')) {
dragged = true;
if (
e.dataTransfer?.types?.includes('text/plain') ||
e.dataTransfer?.types?.includes('text/html')
) {
dragged = false;
return;
}
// Check if the dragged item is a file or image
if (e.dataTransfer?.types?.includes('Files') && e.dataTransfer?.items) {
const items = Array.from(e.dataTransfer.items);
const hasFiles = items.some((item) => item.kind === 'file');
const hasImages = items.some((item) => item.type.startsWith('image/'));
if (hasFiles && !hasImages) {
dragged = true;
} else {
dragged = false;
}
} else {
dragged = false;
}
@ -757,8 +805,47 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
inputElement?.insertContent(content);
};
const noteEventHandler = async (_note) => {
console.log('noteEventHandler', _note);
if (_note.id !== id) return;
if (_note.access_control && _note.access_control !== note.access_control) {
note.access_control = _note.access_control;
}
if (_note.data && _note.data.files) {
files = _note.data.files;
note.data.files = files;
}
if (_note.title && _note.title) {
note.title = _note.title;
}
editor.storage.files = files;
await tick();
for (const file of files) {
if (file.type === 'image') {
const e = new CustomEvent('data', { files: files });
const img = document.getElementById(`image:${file.id}`);
if (img) {
img.dispatchEvent(e);
}
}
}
};
onMount(async () => {
await tick();
$socket?.emit('join-note', {
note_id: id,
auth: {
token: localStorage.token
}
});
$socket?.on('note-events', noteEventHandler);
if ($settings?.models) {
selectedModelId = $settings?.models[0];
@ -784,19 +871,21 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
const dropzoneElement = document.getElementById('note-editor');
dropzoneElement?.addEventListener('dragover', onDragOver);
dropzoneElement?.addEventListener('drop', onDrop);
dropzoneElement?.addEventListener('dragleave', onDragLeave);
// dropzoneElement?.addEventListener('dragover', onDragOver);
// dropzoneElement?.addEventListener('drop', onDrop);
// dropzoneElement?.addEventListener('dragleave', onDragLeave);
});
onDestroy(() => {
console.log('destroy');
$socket?.off('note-events', noteEventHandler);
const dropzoneElement = document.getElementById('note-editor');
if (dropzoneElement) {
dropzoneElement?.removeEventListener('dragover', onDragOver);
dropzoneElement?.removeEventListener('drop', onDrop);
dropzoneElement?.removeEventListener('dragleave', onDragLeave);
// dropzoneElement?.removeEventListener('dragover', onDragOver);
// dropzoneElement?.removeEventListener('drop', onDrop);
// dropzoneElement?.removeEventListener('dragleave', onDragLeave);
}
});
</script>
@ -960,7 +1049,7 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
</button>
</Tooltip>
<Tooltip placement="top" content={$i18n.t('Settings')} className="cursor-pointer">
<Tooltip placement="top" content={$i18n.t('Controls')} className="cursor-pointer">
<button
class="p-1.5 bg-transparent hover:bg-white/5 transition rounded-lg"
on:click={() => {
@ -974,7 +1063,7 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
}
}}
>
<Cog6 />
<AdjustmentsHorizontalOutline />
</button>
</Tooltip>
@ -993,7 +1082,11 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
}
}}
onCopyToClipboard={async () => {
const res = await copyToClipboard(note.data.content.md).catch((error) => {
const res = await copyToClipboard(
note.data.content.md,
note.data.content.html,
true
).catch((error) => {
toast.error(`${error}`);
return null;
});
@ -1095,43 +1188,10 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
></div>
{/if}
{#if files && files.length > 0}
<div class="mb-2.5 w-full flex gap-1 flex-wrap z-40">
{#each files as file, fileIdx}
<div class="w-fit">
{#if file.type === 'image'}
<Image
src={file.url}
imageClassName=" max-h-96 rounded-lg"
dismissible={true}
onDismiss={() => {
files = files.filter((item, idx) => idx !== fileIdx);
note.data.files = files.length > 0 ? files : null;
}}
/>
{:else}
<FileItem
item={file}
dismissible={true}
url={file.url}
name={file.name}
type={file.type}
size={file?.size}
loading={file.status === 'uploading'}
on:dismiss={() => {
files = files.filter((item) => item?.id !== file.id);
note.data.files = files.length > 0 ? files : null;
}}
/>
{/if}
</div>
{/each}
</div>
{/if}
<RichTextInput
bind:this={inputElement}
bind:editor
id={`note-${note.id}`}
className="input-prose-sm px-0.5"
json={true}
bind:value={note.data.content.json}
@ -1141,8 +1201,24 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
socket={$socket}
user={$user}
link={true}
image={true}
{files}
placeholder={$i18n.t('Write something...')}
editable={versionIdx === null && !editing}
onSelectionUpdate={({ editor }) => {
const { from, to } = editor.state.selection;
const selectedText = editor.state.doc.textBetween(from, to, ' ');
if (selectedText.length === 0) {
selectedContent = null;
} else {
selectedContent = {
text: selectedText,
from: from,
to: to
};
}
}}
onChange={(content) => {
note.data.content.html = content.html;
note.data.content.md = content.md;
@ -1152,6 +1228,62 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
charCount = editor.storage.characterCount.characters();
}
}}
fileHandler={true}
onFileDrop={(currentEditor, files, pos) => {
files.forEach(async (file) => {
const fileItem = await inputFileHandler(file).catch((error) => {
return null;
});
if (fileItem.type === 'image') {
// If the file is an image, insert it directly
currentEditor
.chain()
.insertContentAt(pos, {
type: 'image',
attrs: {
src: `data://${fileItem.id}`
}
})
.focus()
.run();
}
});
}}
onFilePaste={() => {}}
on:paste={async (e) => {
e = e.detail.event || e;
const clipboardData = e.clipboardData || window.clipboardData;
console.log('Clipboard data:', clipboardData);
if (clipboardData && clipboardData.items) {
console.log('Clipboard data items:', clipboardData.items);
for (const item of clipboardData.items) {
console.log('Clipboard item:', item);
if (item.type.indexOf('image') !== -1) {
const blob = item.getAsFile();
const fileItem = await inputFileHandler(blob);
if (editor) {
editor
?.chain()
.insertContentAt(editor.state.selection.$anchor.pos, {
type: 'image',
attrs: {
src: `data://${fileItem.id}` // Use data URI for the image
}
})
.focus()
.run();
}
} else if (item?.kind === 'file') {
const file = item.getAsFile();
await inputFileHandler(file);
e.preventDefault();
}
}
}
}}
/>
</div>
</div>
@ -1286,6 +1418,9 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
bind:editing
bind:streaming
bind:stopResponseFlag
{editor}
{inputElement}
{selectedContent}
{files}
onInsert={insertHandler}
onStop={stopResponseHandler}
@ -1296,7 +1431,14 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
scrollToBottomHandler={scrollToBottom}
/>
{:else if selectedPanel === 'settings'}
<Settings bind:show={showPanel} bind:selectedModelId />
<Controls
bind:show={showPanel}
bind:selectedModelId
bind:files
onUpdate={() => {
changeDebounceHandler();
}}
/>
{/if}
</NotePanel>
</PaneGroup>

View File

@ -56,11 +56,14 @@
const i18n = getContext('i18n');
export let editor = null;
export let editing = false;
export let streaming = false;
export let stopResponseFlag = false;
export let note = null;
export let selectedContent = null;
export let files = [];
export let messages = [];
@ -79,20 +82,23 @@
let messagesContainerElement: HTMLDivElement;
let system = '';
let editorEnabled = false;
let editEnabled = false;
let chatInputElement = null;
const DEFAULT_DOCUMENT_EDITOR_PROMPT = `You are an expert document editor.
## Task
Based on the user's instruction, update and enhance the existing notes by incorporating relevant and accurate information from the provided context in the content's primary language. Ensure all edits strictly follow the users intent.
Based on the user's instruction, update and enhance the existing notes or selection by incorporating relevant and accurate information from the provided context in the content's primary language. Ensure all edits strictly follow the users intent.
## Input Structure
- Existing notes: Enclosed within <notes></notes> XML tags.
- Additional context: Enclosed within <context></context> XML tags.
- Current note selection: Enclosed within <selection></selection> XML tags.
- Editing instruction: Provided in the user message.
## Output Instructions
- If a selection is provided, edit **only** the content within <selection></selection>. Leave unselected parts unchanged.
- If no selection is provided, edit the entire notes.
- Deliver a single, rewritten version of the notes in markdown format.
- Integrate information from the context only if it directly supports the user's instruction.
- Use clear, organized markdown elements: headings, lists, task lists ([ ]) where tasks or checklists are strongly implied, bold and italic text as appropriate.
@ -155,7 +161,7 @@ Based on the user's instruction, update and enhance the existing notes by incorp
system = '';
if (editorEnabled) {
if (editEnabled) {
system = `${DEFAULT_DOCUMENT_EDITOR_PROMPT}\n\n`;
} else {
system = `You are a helpful assistant. Please answer the user's questions based on the context provided.\n\n`;
@ -165,7 +171,8 @@ Based on the user's instruction, update and enhance the existing notes by incorp
`<notes>${note?.data?.content?.md ?? ''}</notes>` +
(files && files.length > 0
? `\n<context>${files.map((file) => `${file.name}: ${file?.file?.data?.content ?? 'Could not extract content'}\n`).join('')}</context>`
: '');
: '') +
(selectedContent ? `\n<selection>${selectedContent?.text}</selection>` : '');
const chatMessages = JSON.parse(
JSON.stringify([
@ -206,7 +213,7 @@ Based on the user's instruction, update and enhance the existing notes by incorp
controller.abort('User: Stop Response');
}
if (editorEnabled) {
if (editEnabled) {
editing = false;
streaming = false;
onEdited();
@ -222,8 +229,20 @@ Based on the user's instruction, update and enhance the existing notes by incorp
if (line !== '') {
console.log(line);
if (line === 'data: [DONE]') {
if (editorEnabled) {
if (editEnabled) {
responseMessage.content = `<status title="${$i18n.t('Edited')}" done="true" />`;
if (selectedContent && selectedContent?.text && editor) {
editor.commands.insertContentAt(
{
from: selectedContent.from,
to: selectedContent.to
},
enhancedContent.html || enhancedContent.md || ''
);
selectedContent = null;
}
}
responseMessage.done = true;
@ -236,20 +255,22 @@ Based on the user's instruction, update and enhance the existing notes by incorp
if (responseMessage.content == '' && deltaContent == '\n') {
continue;
} else {
if (editorEnabled) {
if (editEnabled) {
editing = true;
streaming = true;
enhancedContent.md += deltaContent;
enhancedContent.html = marked.parse(enhancedContent.md);
note.data.content.md = enhancedContent.md;
note.data.content.html = enhancedContent.html;
note.data.content.json = null;
responseMessage.content = `<status title="${$i18n.t('Editing')}" done="false" />`;
if (!selectedContent || !selectedContent?.text) {
note.data.content.md = enhancedContent.md;
note.data.content.html = enhancedContent.html;
note.data.content.json = null;
}
scrollToBottomHandler();
responseMessage.content = `<status title="${$i18n.t('Editing')}" done="false" />`;
messages = messages;
} else {
messageContent += deltaContent;
@ -297,11 +318,7 @@ Based on the user's instruction, update and enhance the existing notes by incorp
};
onMount(async () => {
if ($user?.role !== 'admin') {
await goto('/');
}
editorEnabled = localStorage.getItem('noteEditorEnabled') === 'true';
editEnabled = localStorage.getItem('noteEditEnabled') === 'true';
loaded = true;
@ -359,6 +376,16 @@ Based on the user's instruction, update and enhance the existing notes by incorp
</div>
<div class=" pb-2">
{#if selectedContent}
<div class="text-xs rounded-xl px-3.5 py-3 w-full markdown-prose-xs">
<blockquote>
<div class=" line-clamp-3">
{selectedContent?.text}
</div>
</blockquote>
</div>
{/if}
<MessageInput
bind:chatInputElement
acceptFiles={false}
@ -372,14 +399,15 @@ Based on the user's instruction, update and enhance the existing notes by incorp
<Tooltip content={$i18n.t('Edit')} placement="top">
<button
on:click|preventDefault={() => {
editorEnabled = !editorEnabled;
editEnabled = !editEnabled;
localStorage.setItem('noteEditorEnabled', editorEnabled ? 'true' : 'false');
localStorage.setItem('noteEditEnabled', editEnabled ? 'true' : 'false');
}}
disabled={streaming || loading}
type="button"
class="px-2 @xl:px-2.5 py-2 flex gap-1.5 items-center text-sm rounded-full transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden hover:bg-gray-50 dark:hover:bg-gray-800 {editorEnabled
class="px-2 @xl:px-2.5 py-2 flex gap-1.5 items-center text-sm rounded-full transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden hover:bg-gray-50 dark:hover:bg-gray-800 {editEnabled
? ' text-sky-500 dark:text-sky-300 bg-sky-50 dark:bg-sky-200/5'
: 'bg-transparent text-gray-600 dark:text-gray-300 '}"
: 'bg-transparent text-gray-600 dark:text-gray-300 '} disabled:opacity-50 disabled:pointer-events-none"
>
<PencilSquare className="size-4" strokeWidth="1.75" />
<span

View File

@ -0,0 +1,103 @@
<script lang="ts">
import { getContext } from 'svelte';
const i18n = getContext('i18n');
import XMark from '$lib/components/icons/XMark.svelte';
import { models } from '$lib/stores';
import Collapsible from '$lib/components/common/Collapsible.svelte';
import FileItem from '$lib/components/common/FileItem.svelte';
import Image from '$lib/components/common/Image.svelte';
export let show = false;
export let selectedModelId = '';
export let files = [];
export let onUpdate = (files: any[]) => {
// Default no-op function
};
</script>
<div class="flex items-center mb-1.5 pt-1.5">
<div class=" -translate-x-1.5 flex items-center">
<button
class="p-0.5 bg-transparent transition rounded-lg"
on:click={() => {
show = !show;
}}
>
<XMark className="size-5" strokeWidth="2.5" />
</button>
</div>
<div class=" font-medium text-base flex items-center gap-1">
<div>
{$i18n.t('Controls')}
</div>
</div>
</div>
<div class="mt-1">
<div class="pb-10">
{#if files.length > 0}
<div class=" text-xs font-medium pb-1">Files</div>
<div class="flex flex-col gap-1">
{#each files.filter((file) => file.type !== 'image') as file, fileIdx}
<FileItem
className="w-full"
item={file}
small={true}
edit={true}
dismissible={true}
url={file.url}
name={file.name}
type={file.type}
size={file?.size}
loading={file.status === 'uploading'}
on:dismiss={() => {
// Remove the file from the files array
files = files.filter((item) => item.id !== file.id);
files = files;
onUpdate(files);
}}
on:click={() => {
console.log(file);
}}
/>
{/each}
<div class="flex items-center flex-wrap gap-2 mt-1.5">
{#each files.filter((file) => file.type === 'image') as file, fileIdx}
<Image
src={file.url}
imageClassName=" size-14 rounded-xl object-cover"
dismissible={true}
onDismiss={() => {
files = files.filter((item) => item.id !== file.id);
files = files;
onUpdate(files);
}}
/>
{/each}
</div>
</div>
<hr class="my-2 border-gray-50 dark:border-gray-700/10" />
{/if}
<div class=" text-xs font-medium mb-1">Model</div>
<div class="w-full">
<select class="w-full bg-transparent text-sm outline-hidden" bind:value={selectedModelId}>
<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>
</div>
</div>
</div>

View File

@ -1,46 +0,0 @@
<script lang="ts">
import { getContext } from 'svelte';
const i18n = getContext('i18n');
import XMark from '$lib/components/icons/XMark.svelte';
import { models } from '$lib/stores';
export let show = false;
export let selectedModelId = '';
</script>
<div class="flex items-center mb-1.5 pt-1.5">
<div class=" -translate-x-1.5 flex items-center">
<button
class="p-0.5 bg-transparent transition rounded-lg"
on:click={() => {
show = !show;
}}
>
<XMark className="size-5" strokeWidth="2.5" />
</button>
</div>
<div class=" font-medium text-base flex items-center gap-1">
<div>
{$i18n.t('Settings')}
</div>
</div>
</div>
<div class="mt-1">
<div>
<div class=" text-xs font-medium mb-1">Model</div>
<div class="w-full">
<select class="w-full bg-transparent text-sm outline-hidden" bind:value={selectedModelId}>
<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>
</div>
</div>
</div>

View File

@ -44,7 +44,7 @@
class="w-full {className} text-sm rounded-xl px-1 py-1.5 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg font-primary"
sideOffset={6}
side="bottom"
align="start"
align="end"
transition={(e) => fade(e, { duration: 100 })}
>
<DropdownMenu.Sub>
@ -59,6 +59,7 @@
class="w-full rounded-xl px-1 py-1.5 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg"
transition={flyAndScale}
sideOffset={8}
align="end"
>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
@ -102,6 +103,7 @@
class="w-full rounded-xl px-1 py-1.5 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg"
transition={flyAndScale}
sideOffset={8}
align="end"
>
{#if onCopyLink}
<DropdownMenu.Item

View File

@ -1,16 +1,144 @@
<script lang="ts">
import { getContext, onMount } from 'svelte';
import { knowledge } from '$lib/stores';
import { config, knowledge, settings, user } from '$lib/stores';
import Selector from './Knowledge/Selector.svelte';
import FileItem from '$lib/components/common/FileItem.svelte';
import { getKnowledgeBases } from '$lib/apis/knowledge';
import { uploadFile } from '$lib/apis/files';
import { toast } from 'svelte-sonner';
import { v4 as uuidv4 } from 'uuid';
import { WEBUI_API_BASE_URL } from '$lib/constants';
export let selectedItems = [];
const i18n = getContext('i18n');
let loaded = false;
let filesInputElement = null;
let inputFiles = [];
const uploadFileHandler = async (file, fullContext: boolean = false) => {
if ($user?.role !== 'admin' && !($user?.permissions?.chat?.file_upload ?? true)) {
toast.error($i18n.t('You do not have permission to upload files.'));
return null;
}
const tempItemId = uuidv4();
const fileItem = {
type: 'file',
file: '',
id: null,
url: '',
name: file.name,
collection_name: '',
status: 'uploading',
size: file.size,
error: '',
itemId: tempItemId,
...(fullContext ? { context: 'full' } : {})
};
if (fileItem.size == 0) {
toast.error($i18n.t('You cannot upload an empty file.'));
return null;
}
selectedItems = [...selectedItems, fileItem];
try {
// If the file is an audio file, provide the language for STT.
let metadata = null;
if (
(file.type.startsWith('audio/') || file.type.startsWith('video/')) &&
$settings?.audio?.stt?.language
) {
metadata = {
language: $settings?.audio?.stt?.language
};
}
// During the file upload, file content is automatically extracted.
const uploadedFile = await uploadFile(localStorage.token, file, metadata);
if (uploadedFile) {
console.log('File upload completed:', {
id: uploadedFile.id,
name: fileItem.name,
collection: uploadedFile?.meta?.collection_name
});
if (uploadedFile.error) {
console.warn('File upload warning:', uploadedFile.error);
toast.warning(uploadedFile.error);
}
fileItem.status = 'uploaded';
fileItem.file = uploadedFile;
fileItem.id = uploadedFile.id;
fileItem.collection_name =
uploadedFile?.meta?.collection_name || uploadedFile?.collection_name;
fileItem.url = `${WEBUI_API_BASE_URL}/files/${uploadedFile.id}`;
selectedItems = selectedItems;
} else {
selectedItems = selectedItems.filter((item) => item?.itemId !== tempItemId);
}
} catch (e) {
toast.error(`${e}`);
selectedItems = selectedItems.filter((item) => item?.itemId !== tempItemId);
}
};
const inputFilesHandler = async (inputFiles) => {
console.log('Input files handler called with:', inputFiles);
if (
($config?.file?.max_count ?? null) !== null &&
files.length + inputFiles.length > $config?.file?.max_count
) {
toast.error(
$i18n.t(`You can only chat with a maximum of {{maxCount}} file(s) at a time.`, {
maxCount: $config?.file?.max_count
})
);
return;
}
inputFiles.forEach(async (file) => {
console.log('Processing file:', {
name: file.name,
type: file.type,
size: file.size,
extension: file.name.split('.').at(-1)
});
if (
($config?.file?.max_size ?? null) !== null &&
file.size > ($config?.file?.max_size ?? 0) * 1024 * 1024
) {
console.log('File exceeds max size limit:', {
fileSize: file.size,
maxSize: ($config?.file?.max_size ?? 0) * 1024 * 1024
});
toast.error(
$i18n.t(`File size should not exceed {{maxSize}} MB.`, {
maxSize: $config?.file?.max_size
})
);
return;
}
if (!file['type'].startsWith('image/')) {
uploadFileHandler(file);
} else {
toast.error($i18n.t(`Unsupported file type.`));
}
});
};
onMount(async () => {
if (!$knowledge) {
knowledge.set(await getKnowledgeBases(localStorage.token));
@ -19,6 +147,24 @@
});
</script>
<input
bind:this={filesInputElement}
bind:files={inputFiles}
type="file"
hidden
multiple
on:change={async () => {
if (inputFiles && inputFiles.length > 0) {
const _inputFiles = Array.from(inputFiles);
inputFilesHandler(_inputFiles);
} else {
toast.error($i18n.t(`File not found.`));
}
filesInputElement.value = '';
}}
/>
<div>
<slot name="label">
<div class="mb-2">
@ -57,7 +203,7 @@
{/if}
{#if loaded}
<div class="flex flex-wrap text-sm font-medium gap-1.5">
<div class="flex flex-wrap flex-row text-sm gap-1">
<Selector
knowledgeItems={$knowledge || []}
on:select={(e) => {
@ -73,11 +219,22 @@
}
}}
>
<div
class=" px-3.5 py-1.5 font-medium hover:bg-black/5 dark:hover:bg-white/5 outline outline-1 outline-gray-100 dark:outline-gray-850 rounded-3xl"
>
{$i18n.t('Select Knowledge')}
</div>
</Selector>
{#if $user?.role === 'admin' || $user?.permissions?.chat?.file_upload}
<button
class=" px-3.5 py-1.5 font-medium hover:bg-black/5 dark:hover:bg-white/5 outline outline-1 outline-gray-100 dark:outline-gray-850 rounded-3xl"
type="button">{$i18n.t('Select Knowledge')}</button
type="button"
on:click={() => {
filesInputElement.click();
}}>{$i18n.t('Upload Files')}</button
>
</Selector>
{/if}
</div>
{/if}
<!-- {knowledge} -->

View File

@ -226,7 +226,7 @@
filterIds = model?.meta?.filterIds ?? [];
actionIds = model?.meta?.actionIds ?? [];
knowledge = (model?.meta?.knowledge ?? []).map((item) => {
if (item?.collection_name) {
if (item?.collection_name && item?.type !== 'file') {
return {
id: item.collection_name,
name: item.name,

View File

@ -171,11 +171,10 @@
<span class=" text-gray-600 dark:text-gray-300 font-medium">{'}}'}</span>.
</div>
<div class="text-xs text-gray-400 dark:text-gray-500">
{$i18n.t('Utilize')}<span class=" text-gray-600 dark:text-gray-300 font-medium">
{` {{CLIPBOARD}}`}</span
>
{$i18n.t('variable to have them replaced with clipboard content.')}
<div class="text-xs text-gray-400 dark:text-gray-500 underline">
<a href="https://docs.openwebui.com/features/workspace/prompts" target="_blank">
{$i18n.t('To learn more about powerful prompt variables, click here')}
</a>
</div>
</div>
</div>

View File

@ -290,6 +290,7 @@
"Create Account": "إنشاء حساب",
"Create Admin Account": "",
"Create Channel": "",
"Create Folder": "",
"Create Group": "",
"Create Knowledge": "",
"Create new key": "عمل مفتاح جديد",
@ -413,7 +414,7 @@
"e.g. pdf, docx, txt": "",
"e.g. Tools for performing various operations": "",
"e.g., 3, 4, 5 (leave blank for default)": "",
"e.g., audio/wav,audio/mpeg,video/* (leave blank for defaults, * for all)": "",
"e.g., audio/wav,audio/mpeg,video/* (leave blank for defaults)": "",
"e.g., en-US,ja-JP (leave blank for auto-detect)": "",
"e.g., westus (leave blank for eastus)": "",
"e.g.) en,fr,de": "",
@ -601,6 +602,7 @@
"External Web Loader URL": "",
"External Web Search API Key": "",
"External Web Search URL": "",
"Fade Effect for Streaming Text": "",
"Failed to add file.": "",
"Failed to connect to {{URL}} OpenAPI tool server": "",
"Failed to copy link": "",
@ -610,6 +612,7 @@
"Failed to extract content from the file.": "",
"Failed to fetch models": "",
"Failed to generate title": "",
"Failed to load chat preview": "",
"Failed to load file content.": "",
"Failed to read clipboard contents": "فشل في قراءة محتويات الحافظة",
"Failed to save connections": "",
@ -755,6 +758,7 @@
"Input commands": "إدخال الأوامر",
"Input Variables": "",
"Insert": "",
"Insert Follow-Up Prompt to Input": "",
"Insert Prompt as Rich Text": "",
"Install from Github URL": "التثبيت من عنوان URL لجيثب",
"Instant Auto-Send After Voice Transcription": "",
@ -778,6 +782,7 @@
"JWT Expiration": "JWT تجريبي",
"JWT Token": "JWT Token",
"Kagi Search API Key": "",
"Keep Follow-Up Prompts in Chat": "",
"Keep in Sidebar": "",
"Key": "",
"Keyboard shortcuts": "اختصارات لوحة المفاتيح",
@ -812,6 +817,7 @@
"Leave empty to use the default prompt, or enter a custom prompt": "",
"Leave model field empty to use the default model.": "",
"License": "",
"Lift List": "",
"Light": "فاتح",
"Listening...": "",
"Llama.cpp": "",
@ -902,6 +908,7 @@
"New Tool": "",
"new-channel": "",
"Next message": "",
"No chats found": "",
"No chats found for this user.": "",
"No chats found.": "",
"No content": "",
@ -1140,6 +1147,7 @@
"See what's new": "ما الجديد",
"Seed": "Seed",
"Select a base model": "حدد نموذجا أساسيا",
"Select a conversation to preview": "",
"Select a engine": "",
"Select a function": "",
"Select a group": "",
@ -1210,6 +1218,7 @@
"Sign up to {{WEBUI_NAME}}": "",
"Significantly improves accuracy by using an LLM to enhance tables, forms, inline math, and layout detection. Will increase latency. Defaults to True.": "",
"Signing in to {{WEBUI_NAME}}": "",
"Sink List": "",
"sk-1234": "",
"Skip Cache": "",
"Skip the cache and re-run the inference. Defaults to False.": "",
@ -1315,6 +1324,7 @@
"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "",
"To attach knowledge base here, add them to the \"Knowledge\" workspace first.": "",
"To learn more about available endpoints, visit our documentation.": "",
"To learn more about powerful prompt variables, click here": "",
"To protect your privacy, only ratings, model IDs, tags, and metadata are shared from your feedback—your chat logs remain private and are not included.": "",
"To select actions here, add them to the \"Functions\" workspace first.": "",
"To select filters here, add them to the \"Functions\" workspace first.": "",
@ -1361,6 +1371,7 @@
"Unlock mysteries": "",
"Unpin": "",
"Unravel secrets": "",
"Unsupported file type.": "",
"Untagged": "",
"Untitled": "",
"Update": "",
@ -1396,14 +1407,14 @@
"User Webhooks": "",
"Username": "",
"Users": "المستخدمين",
"Using Entire Document": "",
"Using Focused Retrieval": "",
"Using the default arena model with all models. Click the plus button to add custom models.": "",
"Utilize": "يستخدم",
"Valid time units:": "وحدات زمنية صالحة:",
"Valves": "",
"Valves updated": "",
"Valves updated successfully": "",
"variable": "المتغير",
"variable to have them replaced with clipboard content.": "متغير لاستبدالها بمحتوى الحافظة.",
"Verify Connection": "",
"Verify SSL Certificate": "",
"Version": "إصدار",

View File

@ -290,6 +290,7 @@
"Create Account": "إنشاء حساب",
"Create Admin Account": "إنشاء حساب مسؤول",
"Create Channel": "إنشاء قناة",
"Create Folder": "",
"Create Group": "إنشاء مجموعة",
"Create Knowledge": "إنشاء معرفة",
"Create new key": "إنشاء مفتاح جديد",
@ -413,7 +414,7 @@
"e.g. pdf, docx, txt": "",
"e.g. Tools for performing various operations": "مثال: أدوات لتنفيذ عمليات متنوعة",
"e.g., 3, 4, 5 (leave blank for default)": "",
"e.g., audio/wav,audio/mpeg,video/* (leave blank for defaults, * for all)": "",
"e.g., audio/wav,audio/mpeg,video/* (leave blank for defaults)": "",
"e.g., en-US,ja-JP (leave blank for auto-detect)": "",
"e.g., westus (leave blank for eastus)": "",
"e.g.) en,fr,de": "",
@ -601,6 +602,7 @@
"External Web Loader URL": "",
"External Web Search API Key": "",
"External Web Search URL": "",
"Fade Effect for Streaming Text": "",
"Failed to add file.": "فشل في إضافة الملف.",
"Failed to connect to {{URL}} OpenAPI tool server": "",
"Failed to copy link": "",
@ -610,6 +612,7 @@
"Failed to extract content from the file.": "",
"Failed to fetch models": "فشل في جلب النماذج",
"Failed to generate title": "",
"Failed to load chat preview": "",
"Failed to load file content.": "",
"Failed to read clipboard contents": "فشل في قراءة محتويات الحافظة",
"Failed to save connections": "",
@ -755,6 +758,7 @@
"Input commands": "إدخال الأوامر",
"Input Variables": "",
"Insert": "",
"Insert Follow-Up Prompt to Input": "",
"Insert Prompt as Rich Text": "",
"Install from Github URL": "التثبيت من عنوان URL لجيثب",
"Instant Auto-Send After Voice Transcription": "إرسال تلقائي فوري بعد تحويل الصوت إلى نص",
@ -778,6 +782,7 @@
"JWT Expiration": "JWT تجريبي",
"JWT Token": "JWT Token",
"Kagi Search API Key": "مفتاح API لـ Kagi Search",
"Keep Follow-Up Prompts in Chat": "",
"Keep in Sidebar": "",
"Key": "المفتاح",
"Keyboard shortcuts": "اختصارات لوحة المفاتيح",
@ -812,6 +817,7 @@
"Leave empty to use the default prompt, or enter a custom prompt": "اتركه فارغًا لاستخدام التوجيه الافتراضي، أو أدخل توجيهًا مخصصًا",
"Leave model field empty to use the default model.": "اترك حقل النموذج فارغًا لاستخدام النموذج الافتراضي.",
"License": "الترخيص",
"Lift List": "",
"Light": "فاتح",
"Listening...": "جارٍ الاستماع...",
"Llama.cpp": "Llama.cpp",
@ -902,6 +908,7 @@
"New Tool": "",
"new-channel": "قناة جديدة",
"Next message": "",
"No chats found": "",
"No chats found for this user.": "",
"No chats found.": "",
"No content": "",
@ -1140,6 +1147,7 @@
"See what's new": "ما الجديد",
"Seed": "Seed",
"Select a base model": "حدد نموذجا أساسيا",
"Select a conversation to preview": "",
"Select a engine": "اختر محركًا",
"Select a function": "اختر وظيفة",
"Select a group": "اختر مجموعة",
@ -1210,6 +1218,7 @@
"Sign up to {{WEBUI_NAME}}": "سجّل في {{WEBUI_NAME}}",
"Significantly improves accuracy by using an LLM to enhance tables, forms, inline math, and layout detection. Will increase latency. Defaults to True.": "",
"Signing in to {{WEBUI_NAME}}": "جارٍ تسجيل الدخول إلى {{WEBUI_NAME}}",
"Sink List": "",
"sk-1234": "sk-1234",
"Skip Cache": "",
"Skip the cache and re-run the inference. Defaults to False.": "",
@ -1315,6 +1324,7 @@
"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "للوصول إلى واجهة WebUI، يُرجى التواصل مع المسؤول. يمكن للمسؤولين إدارة حالة المستخدمين من لوحة الإدارة.",
"To attach knowledge base here, add them to the \"Knowledge\" workspace first.": "لإرفاق قاعدة المعرفة هنا، أضفها أولاً إلى مساحة العمل \"المعرفة\".",
"To learn more about available endpoints, visit our documentation.": "لمعرفة المزيد حول نقاط النهاية المتاحة، قم بزيارة الوثائق الخاصة بنا.",
"To learn more about powerful prompt variables, click here": "",
"To protect your privacy, only ratings, model IDs, tags, and metadata are shared from your feedback—your chat logs remain private and are not included.": "لحماية خصوصيتك، يتم مشاركة التقييمات ومعرّفات النماذج والوسوم والبيانات الوصفية فقط من ملاحظاتك—سجلات الدردشة تظل خاصة ولا تُدرج.",
"To select actions here, add them to the \"Functions\" workspace first.": "لاختيار الإجراءات هنا، أضفها أولاً إلى مساحة العمل \"الوظائف\".",
"To select filters here, add them to the \"Functions\" workspace first.": "لاختيار الفلاتر هنا، أضفها أولاً إلى مساحة العمل \"الوظائف\".",
@ -1361,6 +1371,7 @@
"Unlock mysteries": "اكشف الأسرار",
"Unpin": "إزالة التثبيت",
"Unravel secrets": "فكّ الأسرار",
"Unsupported file type.": "",
"Untagged": "بدون وسوم",
"Untitled": "",
"Update": "تحديث",
@ -1396,14 +1407,14 @@
"User Webhooks": "",
"Username": "اسم المستخدم",
"Users": "المستخدمين",
"Using Entire Document": "",
"Using Focused Retrieval": "",
"Using the default arena model with all models. Click the plus button to add custom models.": "يتم استخدام نموذج الساحة الافتراضي مع جميع النماذج. اضغط على زر + لإضافة نماذج مخصصة.",
"Utilize": "يستخدم",
"Valid time units:": "وحدات زمنية صالحة:",
"Valves": "الصمامات",
"Valves updated": "تم تحديث الصمامات",
"Valves updated successfully": "تم تحديث الصمامات بنجاح",
"variable": "المتغير",
"variable to have them replaced with clipboard content.": "متغير لاستبدالها بمحتوى الحافظة.",
"Verify Connection": "",
"Verify SSL Certificate": "",
"Version": "إصدار",

View File

@ -290,6 +290,7 @@
"Create Account": "Създаване на Акаунт",
"Create Admin Account": "Създаване на администраторски акаунт",
"Create Channel": "Създаване на канал",
"Create Folder": "",
"Create Group": "Създаване на група",
"Create Knowledge": "Създаване на знания",
"Create new key": "Създаване на нов ключ",
@ -413,7 +414,7 @@
"e.g. pdf, docx, txt": "",
"e.g. Tools for performing various operations": "напр. Инструменти за извършване на различни операции",
"e.g., 3, 4, 5 (leave blank for default)": "",
"e.g., audio/wav,audio/mpeg,video/* (leave blank for defaults, * for all)": "",
"e.g., audio/wav,audio/mpeg,video/* (leave blank for defaults)": "",
"e.g., en-US,ja-JP (leave blank for auto-detect)": "",
"e.g., westus (leave blank for eastus)": "",
"e.g.) en,fr,de": "",
@ -601,6 +602,7 @@
"External Web Loader URL": "",
"External Web Search API Key": "",
"External Web Search URL": "",
"Fade Effect for Streaming Text": "",
"Failed to add file.": "Неуспешно добавяне на файл.",
"Failed to connect to {{URL}} OpenAPI tool server": "",
"Failed to copy link": "",
@ -610,6 +612,7 @@
"Failed to extract content from the file.": "",
"Failed to fetch models": "Неуспешно извличане на модели",
"Failed to generate title": "",
"Failed to load chat preview": "",
"Failed to load file content.": "",
"Failed to read clipboard contents": "Грешка при четене на съдържанието от клипборда",
"Failed to save connections": "",
@ -755,6 +758,7 @@
"Input commands": "Въведете команди",
"Input Variables": "",
"Insert": "",
"Insert Follow-Up Prompt to Input": "",
"Insert Prompt as Rich Text": "",
"Install from Github URL": "Инсталиране от URL адреса на Github",
"Instant Auto-Send After Voice Transcription": "Незабавно автоматично изпращане след гласова транскрипция",
@ -778,6 +782,7 @@
"JWT Expiration": "JWT изтичане",
"JWT Token": "JWT токен",
"Kagi Search API Key": "API ключ за Kagi Search",
"Keep Follow-Up Prompts in Chat": "",
"Keep in Sidebar": "",
"Key": "Ключ",
"Keyboard shortcuts": "Клавиши за бърз достъп",
@ -812,6 +817,7 @@
"Leave empty to use the default prompt, or enter a custom prompt": "Оставете празно, за да използвате промпта по подразбиране, или въведете персонализиран промпт",
"Leave model field empty to use the default model.": "Оставете полето за модел празно, за да използвате модела по подразбиране.",
"License": "Лиценз",
"Lift List": "",
"Light": "Светъл",
"Listening...": "Слушане...",
"Llama.cpp": "Llama.cpp",
@ -902,6 +908,7 @@
"New Tool": "",
"new-channel": "нов-канал",
"Next message": "",
"No chats found": "",
"No chats found for this user.": "",
"No chats found.": "",
"No content": "Без съдържание",
@ -1140,6 +1147,7 @@
"See what's new": "Виж какво е новото",
"Seed": "Начално число",
"Select a base model": "Изберете базов модел",
"Select a conversation to preview": "",
"Select a engine": "Изберете двигател",
"Select a function": "Изберете функция",
"Select a group": "Изберете група",
@ -1210,6 +1218,7 @@
"Sign up to {{WEBUI_NAME}}": "Регистрирайте се в {{WEBUI_NAME}}",
"Significantly improves accuracy by using an LLM to enhance tables, forms, inline math, and layout detection. Will increase latency. Defaults to True.": "",
"Signing in to {{WEBUI_NAME}}": "Вписване в {{WEBUI_NAME}}",
"Sink List": "",
"sk-1234": "sk-1234",
"Skip Cache": "",
"Skip the cache and re-run the inference. Defaults to False.": "",
@ -1315,6 +1324,7 @@
"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "За достъп до уеб интерфейса, моля, свържете се с администратора. Администраторите могат да управляват статусите на потребителите от Административния панел.",
"To attach knowledge base here, add them to the \"Knowledge\" workspace first.": "За да прикачите база знания тук, първо ги добавете към работното пространство \"Знания\".",
"To learn more about available endpoints, visit our documentation.": "За да научите повече за наличните крайни точки, посетете нашата документация.",
"To learn more about powerful prompt variables, click here": "",
"To protect your privacy, only ratings, model IDs, tags, and metadata are shared from your feedback—your chat logs remain private and are not included.": "За да защитим вашата поверителност, от вашата обратна връзка се споделят само оценки, идентификатори на модели, тагове и метаданни-вашите чат логове остават лични и не са включени.",
"To select actions here, add them to the \"Functions\" workspace first.": "За да изберете действия тук, първо ги добавете към работното пространство \"Функции\".",
"To select filters here, add them to the \"Functions\" workspace first.": "За да изберете филтри тук, първо ги добавете към работното пространство \"Функции\".",
@ -1361,6 +1371,7 @@
"Unlock mysteries": "Разкрий мистерии",
"Unpin": "Откачи",
"Unravel secrets": "Разгадай тайни",
"Unsupported file type.": "",
"Untagged": "Без етикет",
"Untitled": "Неозаглавен",
"Update": "Актуализиране",
@ -1396,14 +1407,14 @@
"User Webhooks": "",
"Username": "Потребителско име",
"Users": "Потребители",
"Using Entire Document": "",
"Using Focused Retrieval": "",
"Using the default arena model with all models. Click the plus button to add custom models.": "Използване на стандартния арена модел с всички модели. Кликнете бутона плюс, за да добавите персонализирани модели.",
"Utilize": "Използване на",
"Valid time units:": "Валидни единици за време:",
"Valves": "Клапани",
"Valves updated": "Клапаните са актуализирани",
"Valves updated successfully": "Клапаните са актуализирани успешно",
"variable": "променлива",
"variable to have them replaced with clipboard content.": "променлива, за да бъдат заменени със съдържание от клипборда.",
"Verify Connection": "",
"Verify SSL Certificate": "",
"Version": "Версия",

View File

@ -290,6 +290,7 @@
"Create Account": "একাউন্ট তৈরি করুন",
"Create Admin Account": "",
"Create Channel": "",
"Create Folder": "",
"Create Group": "",
"Create Knowledge": "",
"Create new key": "একটি নতুন কী তৈরি করুন",
@ -413,7 +414,7 @@
"e.g. pdf, docx, txt": "",
"e.g. Tools for performing various operations": "",
"e.g., 3, 4, 5 (leave blank for default)": "",
"e.g., audio/wav,audio/mpeg,video/* (leave blank for defaults, * for all)": "",
"e.g., audio/wav,audio/mpeg,video/* (leave blank for defaults)": "",
"e.g., en-US,ja-JP (leave blank for auto-detect)": "",
"e.g., westus (leave blank for eastus)": "",
"e.g.) en,fr,de": "",
@ -601,6 +602,7 @@
"External Web Loader URL": "",
"External Web Search API Key": "",
"External Web Search URL": "",
"Fade Effect for Streaming Text": "",
"Failed to add file.": "",
"Failed to connect to {{URL}} OpenAPI tool server": "",
"Failed to copy link": "",
@ -610,6 +612,7 @@
"Failed to extract content from the file.": "",
"Failed to fetch models": "",
"Failed to generate title": "",
"Failed to load chat preview": "",
"Failed to load file content.": "",
"Failed to read clipboard contents": "ক্লিপবোর্ডের বিষয়বস্তু পড়া সম্ভব হয়নি",
"Failed to save connections": "",
@ -755,6 +758,7 @@
"Input commands": "ইনপুট কমান্ডস",
"Input Variables": "",
"Insert": "",
"Insert Follow-Up Prompt to Input": "",
"Insert Prompt as Rich Text": "",
"Install from Github URL": "Github URL থেকে ইনস্টল করুন",
"Instant Auto-Send After Voice Transcription": "",
@ -778,6 +782,7 @@
"JWT Expiration": "JWT-র মেয়াদ",
"JWT Token": "JWT টোকেন",
"Kagi Search API Key": "",
"Keep Follow-Up Prompts in Chat": "",
"Keep in Sidebar": "",
"Key": "",
"Keyboard shortcuts": "কিবোর্ড শর্টকাটসমূহ",
@ -812,6 +817,7 @@
"Leave empty to use the default prompt, or enter a custom prompt": "",
"Leave model field empty to use the default model.": "",
"License": "",
"Lift List": "",
"Light": "লাইট",
"Listening...": "",
"Llama.cpp": "",
@ -902,6 +908,7 @@
"New Tool": "",
"new-channel": "",
"Next message": "",
"No chats found": "",
"No chats found for this user.": "",
"No chats found.": "",
"No content": "",
@ -1140,6 +1147,7 @@
"See what's new": "নতুন কী আছে দেখুন",
"Seed": "সীড",
"Select a base model": "একটি বেস মডেল নির্বাচন করুন",
"Select a conversation to preview": "",
"Select a engine": "",
"Select a function": "",
"Select a group": "",
@ -1210,6 +1218,7 @@
"Sign up to {{WEBUI_NAME}}": "",
"Significantly improves accuracy by using an LLM to enhance tables, forms, inline math, and layout detection. Will increase latency. Defaults to True.": "",
"Signing in to {{WEBUI_NAME}}": "",
"Sink List": "",
"sk-1234": "",
"Skip Cache": "",
"Skip the cache and re-run the inference. Defaults to False.": "",
@ -1315,6 +1324,7 @@
"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "",
"To attach knowledge base here, add them to the \"Knowledge\" workspace first.": "",
"To learn more about available endpoints, visit our documentation.": "",
"To learn more about powerful prompt variables, click here": "",
"To protect your privacy, only ratings, model IDs, tags, and metadata are shared from your feedback—your chat logs remain private and are not included.": "",
"To select actions here, add them to the \"Functions\" workspace first.": "",
"To select filters here, add them to the \"Functions\" workspace first.": "",
@ -1361,6 +1371,7 @@
"Unlock mysteries": "",
"Unpin": "",
"Unravel secrets": "",
"Unsupported file type.": "",
"Untagged": "",
"Untitled": "",
"Update": "",
@ -1396,14 +1407,14 @@
"User Webhooks": "",
"Username": "",
"Users": "ব্যাবহারকারীগণ",
"Using Entire Document": "",
"Using Focused Retrieval": "",
"Using the default arena model with all models. Click the plus button to add custom models.": "",
"Utilize": "ইউটিলাইজ",
"Valid time units:": "সময়ের গ্রহণযোগ্য এককসমূহ:",
"Valves": "",
"Valves updated": "",
"Valves updated successfully": "",
"variable": "ভেরিয়েবল",
"variable to have them replaced with clipboard content.": "ক্লিপবোর্ডের কন্টেন্ট দিয়ে যেই ভেরিয়েবল রিপ্লেস করা যাবে।",
"Verify Connection": "",
"Verify SSL Certificate": "",
"Version": "ভার্সন",

View File

@ -290,6 +290,7 @@
"Create Account": "རྩིས་ཁྲ་གསར་བཟོ།",
"Create Admin Account": "དོ་དམ་པའི་རྩིས་ཁྲ་གསར་བཟོ།",
"Create Channel": "བགྲོ་གླེང་གསར་བཟོ།",
"Create Folder": "",
"Create Group": "ཚོགས་པ་གསར་བཟོ།",
"Create Knowledge": "ཤེས་བྱ་གསར་བཟོ།",
"Create new key": "ལྡེ་མིག་གསར་པ་བཟོ་བ།",
@ -413,7 +414,7 @@
"e.g. pdf, docx, txt": "",
"e.g. Tools for performing various operations": "དཔེར་ན། ལས་ཀ་སྣ་ཚོགས་སྒྲུབ་བྱེད་ཀྱི་ལག་ཆ།",
"e.g., 3, 4, 5 (leave blank for default)": "",
"e.g., audio/wav,audio/mpeg,video/* (leave blank for defaults, * for all)": "",
"e.g., audio/wav,audio/mpeg,video/* (leave blank for defaults)": "",
"e.g., en-US,ja-JP (leave blank for auto-detect)": "",
"e.g., westus (leave blank for eastus)": "",
"e.g.) en,fr,de": "",
@ -601,6 +602,7 @@
"External Web Loader URL": "",
"External Web Search API Key": "",
"External Web Search URL": "",
"Fade Effect for Streaming Text": "",
"Failed to add file.": "ཡིག་ཆ་སྣོན་པར་མ་ཐུབ།",
"Failed to connect to {{URL}} OpenAPI tool server": "{{URL}} OpenAPI ལག་ཆའི་སར་བར་ལ་སྦྲེལ་མཐུད་བྱེད་མ་ཐུབ།",
"Failed to copy link": "",
@ -610,6 +612,7 @@
"Failed to extract content from the file.": "",
"Failed to fetch models": "དཔེ་དབྱིབས་ལེན་པར་མ་ཐུབ།",
"Failed to generate title": "",
"Failed to load chat preview": "",
"Failed to load file content.": "",
"Failed to read clipboard contents": "སྦྱར་སྡེར་གྱི་ནང་དོན་ཀློག་མ་ཐུབ།",
"Failed to save connections": "",
@ -755,6 +758,7 @@
"Input commands": "ནང་འཇུག་བཀའ་བརྡ།",
"Input Variables": "",
"Insert": "",
"Insert Follow-Up Prompt to Input": "",
"Insert Prompt as Rich Text": "",
"Install from Github URL": "Github URL ནས་སྒྲིག་སྦྱོར་བྱེད་པ།",
"Instant Auto-Send After Voice Transcription": "སྐད་ཆ་ཡིག་འབེབས་བྱས་རྗེས་ལམ་སང་རང་འགུལ་གཏོང་བ།",
@ -778,6 +782,7 @@
"JWT Expiration": "JWT དུས་ཚོད་རྫོགས་པ།",
"JWT Token": "JWT Token",
"Kagi Search API Key": "Kagi Search API ལྡེ་མིག",
"Keep Follow-Up Prompts in Chat": "",
"Keep in Sidebar": "",
"Key": "ལྡེ་མིག",
"Keyboard shortcuts": "མཐེབ་གནོན་མྱུར་ལམ།",
@ -812,6 +817,7 @@
"Leave empty to use the default prompt, or enter a custom prompt": "སྔོན་སྒྲིག་འགུལ་སློང་བེད་སྤྱོད་གཏོང་བར་སྟོང་པ་བཞག་པའམ། ཡང་ན་སྲོལ་བཟོས་འགུལ་སློང་འཇུག་པ།",
"Leave model field empty to use the default model.": "སྔོན་སྒྲིག་དཔེ་དབྱིབས་བེད་སྤྱོད་གཏོང་བར་དཔེ་དབྱིབས་ཀྱི་ཁོངས་སྟོང་པ་བཞག་པ།",
"License": "ཆོག་མཆན།",
"Lift List": "",
"Light": "དཀར་པོ།",
"Listening...": "ཉན་བཞིན་པ།...",
"Llama.cpp": "Llama.cpp",
@ -902,6 +908,7 @@
"New Tool": "",
"new-channel": "བགྲོ་གླེང་གསར་པ།",
"Next message": "",
"No chats found": "",
"No chats found for this user.": "",
"No chats found.": "",
"No content": "",
@ -1140,6 +1147,7 @@
"See what's new": "གསར་པ་ཅི་ཡོད་ལྟ་བ།",
"Seed": "Seed",
"Select a base model": "གཞི་རྩའི་དཔེ་དབྱིབས་ཤིག་གདམ་པ།",
"Select a conversation to preview": "",
"Select a engine": "འཕྲུལ་འཁོར་ཞིག་གདམ་པ།",
"Select a function": "ལས་འགན་ཞིག་གདམ་པ།",
"Select a group": "ཚོགས་པ་ཞིག་གདམ་པ།",
@ -1210,6 +1218,7 @@
"Sign up to {{WEBUI_NAME}}": "{{WEBUI_NAME}} ལ་ཐོ་འགོད།",
"Significantly improves accuracy by using an LLM to enhance tables, forms, inline math, and layout detection. Will increase latency. Defaults to True.": "",
"Signing in to {{WEBUI_NAME}}": "{{WEBUI_NAME}} ལ་ནང་འཛུལ་བྱེད་བཞིན་པ།",
"Sink List": "",
"sk-1234": "sk-༡༢༣༤",
"Skip Cache": "",
"Skip the cache and re-run the inference. Defaults to False.": "",
@ -1315,6 +1324,7 @@
"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "WebUI ལ་འཛུལ་སྤྱོད་བྱེད་པར། དོ་དམ་པ་དང་འབྲེལ་གཏུག་བྱེད་རོགས། དོ་དམ་པས་དོ་དམ་པའི་ལྟ་སྟེགས་ནས་བེད་སྤྱོད་མཁན་གྱི་གནས་སྟངས་དོ་དམ་བྱེད་ཐུབ།",
"To attach knowledge base here, add them to the \"Knowledge\" workspace first.": "ཤེས་བྱའི་རྟེན་གཞི་འདིར་འཇོག་པར། ཐོག་མར་དེ་དག་ \"ཤེས་བྱའི་\" ལས་ཡུལ་དུ་སྣོན་པ།",
"To learn more about available endpoints, visit our documentation.": "ཡོད་པའི་མཇུག་མཐུད་སྐོར་མང་ཙམ་ཤེས་པར། ང་ཚོའི་ཡིག་ཆ་ལ་ལྟ་བ།",
"To learn more about powerful prompt variables, click here": "",
"To protect your privacy, only ratings, model IDs, tags, and metadata are shared from your feedback—your chat logs remain private and are not included.": "ཁྱེད་ཀྱི་སྒེར་དོན་སྲུང་སྐྱོབ་བྱེད་པར། ཁྱེད་ཀྱི་བསམ་འཆར་ནས་སྐར་མ། དཔེ་དབྱིབས་ཀྱི་ IDs། རྟགས། དང་ metadata ཁོ་ན་མཉམ་སྤྱོད་བྱེད།—ཁྱེད་ཀྱི་ཁ་བརྡའི་ཟིན་ཐོ་སྒེར་དོན་དུ་གནས་པ་དང་ཚུད་མེད།",
"To select actions here, add them to the \"Functions\" workspace first.": "བྱ་སྤྱོད་འདིར་གདམ་ག་བྱེད་པར། ཐོག་མར་དེ་དག་ \"ལས་འགན་གྱི་\" ལས་ཡུལ་དུ་སྣོན་པ།",
"To select filters here, add them to the \"Functions\" workspace first.": "འཚག་མ་འདིར་གདམ་ག་བྱེད་པར། ཐོག་མར་དེ་དག་ \"ལས་འགན་གྱི་\" ལས་ཡུལ་དུ་སྣོན་པ།",
@ -1361,6 +1371,7 @@
"Unlock mysteries": "གསང་བ་གྲོལ་བ།",
"Unpin": "ཕྱིར་འདོན།",
"Unravel secrets": "གསང་བ་གྲོལ་བ།",
"Unsupported file type.": "",
"Untagged": "རྟགས་མེད།",
"Untitled": "",
"Update": "གསར་སྒྱུར།",
@ -1396,14 +1407,14 @@
"User Webhooks": "བེད་སྤྱོད་མཁན་གྱི་ Webhooks",
"Username": "བེད་སྤྱོད་མིང་།",
"Users": "བེད་སྤྱོད་མཁན།",
"Using Entire Document": "",
"Using Focused Retrieval": "",
"Using the default arena model with all models. Click the plus button to add custom models.": "དཔེ་དབྱིབས་ཡོངས་རྫོགས་དང་མཉམ་དུ་སྔོན་སྒྲིག་ arena དཔེ་དབྱིབས་བེད་སྤྱོད་གཏོང་བཞིན་པ། སྲོལ་བཟོས་དཔེ་དབྱིབས་སྣོན་པར་བསྣན་རྟགས་མཐེབ་གནོན་ལ་མནན་པ།",
"Utilize": "བེད་སྤྱོད།",
"Valid time units:": "ནུས་ལྡན་དུས་ཚོད་ཀྱི་ཚན་པ།:",
"Valves": "Valves",
"Valves updated": "Valves གསར་སྒྱུར་བྱས།",
"Valves updated successfully": "Valves ལེགས་པར་གསར་སྒྱུར་བྱས།",
"variable": "འགྱུར་ཚད།",
"variable to have them replaced with clipboard content.": "འགྱུར་ཚད་དེ་དག་སྦྱར་སྡེར་གྱི་ནང་དོན་གྱིས་ཚབ་བྱེད་པར་ཡོད་པ།",
"Verify Connection": "སྦྲེལ་མཐུད་ར་སྤྲོད།",
"Verify SSL Certificate": "",
"Version": "པར་གཞི།",

View File

@ -290,6 +290,7 @@
"Create Account": "Crear un compte",
"Create Admin Account": "Crear un compte d'Administrador",
"Create Channel": "Crear un canal",
"Create Folder": "",
"Create Group": "Crear grup",
"Create Knowledge": "Crear Coneixement",
"Create new key": "Crear una nova clau",
@ -413,7 +414,7 @@
"e.g. pdf, docx, txt": "p. ex. pdf, docx, txt",
"e.g. Tools for performing various operations": "p. ex. Eines per dur a terme operacions",
"e.g., 3, 4, 5 (leave blank for default)": "p. ex. 3, 4, 5 (deixa-ho en blanc per utilitzar el per defecte)",
"e.g., audio/wav,audio/mpeg,video/* (leave blank for defaults, * for all)": "",
"e.g., audio/wav,audio/mpeg,video/* (leave blank for defaults)": "",
"e.g., en-US,ja-JP (leave blank for auto-detect)": "p. ex. en-US, ja-JP, ca-ES (deixa-ho en blanc per detecció automàtica)",
"e.g., westus (leave blank for eastus)": "p. ex. westus (deixa-ho en blanc per a eastus)",
"e.g.) en,fr,de": "p. ex. en,fr,de",
@ -601,6 +602,7 @@
"External Web Loader URL": "URL d'External Web Loader",
"External Web Search API Key": "Clau API d'External Web Search",
"External Web Search URL": "URL d'External Web Search",
"Fade Effect for Streaming Text": "",
"Failed to add file.": "No s'ha pogut afegir l'arxiu.",
"Failed to connect to {{URL}} OpenAPI tool server": "No s'ha pogut connecta al servidor d'eines OpenAPI {{URL}}",
"Failed to copy link": "No s'ha pogut copiar l'enllaç",
@ -610,6 +612,7 @@
"Failed to extract content from the file.": "",
"Failed to fetch models": "No s'han pogut obtenir els models",
"Failed to generate title": "",
"Failed to load chat preview": "",
"Failed to load file content.": "No s'ha pogut carregar el contingut del fitxer",
"Failed to read clipboard contents": "No s'ha pogut llegir el contingut del porta-retalls",
"Failed to save connections": "No s'han pogut desar les connexions",
@ -755,6 +758,7 @@
"Input commands": "Entra comandes",
"Input Variables": "",
"Insert": "",
"Insert Follow-Up Prompt to Input": "",
"Insert Prompt as Rich Text": "",
"Install from Github URL": "Instal·lar des de l'URL de Github",
"Instant Auto-Send After Voice Transcription": "Enviament automàtic després de la transcripció de veu",
@ -778,6 +782,7 @@
"JWT Expiration": "Caducitat del JWT",
"JWT Token": "Token JWT",
"Kagi Search API Key": "Clau API de Kagi Search",
"Keep Follow-Up Prompts in Chat": "",
"Keep in Sidebar": "Mantenir a la barra lateral",
"Key": "Clau",
"Keyboard shortcuts": "Dreceres de teclat",
@ -812,6 +817,7 @@
"Leave empty to use the default prompt, or enter a custom prompt": "Deixa-ho en blanc per utilitzar la indicació predeterminada o introdueix una indicació personalitzada",
"Leave model field empty to use the default model.": "Deixa el camp de model buit per utilitzar el model per defecte.",
"License": "Llicència",
"Lift List": "",
"Light": "Clar",
"Listening...": "Escoltant...",
"Llama.cpp": "Llama.cpp",
@ -902,6 +908,7 @@
"New Tool": "Nova eina",
"new-channel": "nou-canal",
"Next message": "Missatge següent",
"No chats found": "",
"No chats found for this user.": "No s'han trobat xats per a aquest usuari.",
"No chats found.": "No s'ha trobat xats.",
"No content": "No hi ha contingut",
@ -1140,6 +1147,7 @@
"See what's new": "Veure què hi ha de nou",
"Seed": "Llavor",
"Select a base model": "Seleccionar un model base",
"Select a conversation to preview": "",
"Select a engine": "Seleccionar un motor",
"Select a function": "Seleccionar una funció",
"Select a group": "Seleccionar un grup",
@ -1210,6 +1218,7 @@
"Sign up to {{WEBUI_NAME}}": "Registrar-se a {{WEBUI_NAME}}",
"Significantly improves accuracy by using an LLM to enhance tables, forms, inline math, and layout detection. Will increase latency. Defaults to True.": "Millora significativament la precisió utilitzant un LLM per millorar taules, formularis, matemàtiques en línia i detecció de layout. Augmentarà la latència. Per defecte és Verdader.",
"Signing in to {{WEBUI_NAME}}": "Iniciant sessió a {{WEBUI_NAME}}",
"Sink List": "",
"sk-1234": "sk-1234",
"Skip Cache": "Ometre la memòria cau",
"Skip the cache and re-run the inference. Defaults to False.": "Omet la memòria cai i torna a executar la inferència. Per defecte és Fals.",
@ -1315,6 +1324,7 @@
"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "Per accedir a la WebUI, poseu-vos en contacte amb l'administrador. Els administradors poden gestionar els estats dels usuaris des del tauler d'administració.",
"To attach knowledge base here, add them to the \"Knowledge\" workspace first.": "Per adjuntar la base de coneixement aquí, afegiu-la primer a l'espai de treball \"Coneixement\".",
"To learn more about available endpoints, visit our documentation.": "Per obtenir més informació sobre els punts d'accés disponibles, visiteu la nostra documentació.",
"To learn more about powerful prompt variables, click here": "",
"To protect your privacy, only ratings, model IDs, tags, and metadata are shared from your feedback—your chat logs remain private and are not included.": "Per protegir la privadesa, només es comparteixen puntuacions, identificadors de models, etiquetes i metadades dels comentaris; els registres de xat romanen privats i no s'inclouen.",
"To select actions here, add them to the \"Functions\" workspace first.": "Per seleccionar accions aquí, afegeix-les primer a l'espai de treball \"Funcions\".",
"To select filters here, add them to the \"Functions\" workspace first.": "Per seleccionar filtres aquí, afegeix-los primer a l'espai de treball \"Funcions\".",
@ -1361,6 +1371,7 @@
"Unlock mysteries": "Desbloqueja els misteris",
"Unpin": "Alliberar",
"Unravel secrets": "Descobreix els secrets",
"Unsupported file type.": "",
"Untagged": "Sense etiquetes",
"Untitled": "Sense títol",
"Update": "Actualitzar",
@ -1396,14 +1407,14 @@
"User Webhooks": "Webhooks d'usuari",
"Username": "Nom d'usuari",
"Users": "Usuaris",
"Using Entire Document": "",
"Using Focused Retrieval": "",
"Using the default arena model with all models. Click the plus button to add custom models.": "S'utilitza el model d'Arena predeterminat amb tots els models. Clica el botó més per afegir models personalitzats.",
"Utilize": "Utilitzar",
"Valid time units:": "Unitats de temps vàlides:",
"Valves": "Valves",
"Valves updated": "Valves actualitzades",
"Valves updated successfully": "Valves actualitzades correctament",
"variable": "variable",
"variable to have them replaced with clipboard content.": "variable per tenir-les reemplaçades amb el contingut del porta-retalls.",
"Verify Connection": "Verificar la connexió",
"Verify SSL Certificate": "Verificar el certificat SSL",
"Version": "Versió",

View File

@ -290,6 +290,7 @@
"Create Account": "Paghimo og account",
"Create Admin Account": "",
"Create Channel": "",
"Create Folder": "",
"Create Group": "",
"Create Knowledge": "",
"Create new key": "",
@ -413,7 +414,7 @@
"e.g. pdf, docx, txt": "",
"e.g. Tools for performing various operations": "",
"e.g., 3, 4, 5 (leave blank for default)": "",
"e.g., audio/wav,audio/mpeg,video/* (leave blank for defaults, * for all)": "",
"e.g., audio/wav,audio/mpeg,video/* (leave blank for defaults)": "",
"e.g., en-US,ja-JP (leave blank for auto-detect)": "",
"e.g., westus (leave blank for eastus)": "",
"e.g.) en,fr,de": "",
@ -601,6 +602,7 @@
"External Web Loader URL": "",
"External Web Search API Key": "",
"External Web Search URL": "",
"Fade Effect for Streaming Text": "",
"Failed to add file.": "",
"Failed to connect to {{URL}} OpenAPI tool server": "",
"Failed to copy link": "",
@ -610,6 +612,7 @@
"Failed to extract content from the file.": "",
"Failed to fetch models": "",
"Failed to generate title": "",
"Failed to load chat preview": "",
"Failed to load file content.": "",
"Failed to read clipboard contents": "Napakyas sa pagbasa sa sulod sa clipboard",
"Failed to save connections": "",
@ -755,6 +758,7 @@
"Input commands": "Pagsulod sa input commands",
"Input Variables": "",
"Insert": "",
"Insert Follow-Up Prompt to Input": "",
"Insert Prompt as Rich Text": "",
"Install from Github URL": "",
"Instant Auto-Send After Voice Transcription": "",
@ -778,6 +782,7 @@
"JWT Expiration": "Pag-expire sa JWT",
"JWT Token": "JWT token",
"Kagi Search API Key": "",
"Keep Follow-Up Prompts in Chat": "",
"Keep in Sidebar": "",
"Key": "",
"Keyboard shortcuts": "Mga shortcut sa keyboard",
@ -812,6 +817,7 @@
"Leave empty to use the default prompt, or enter a custom prompt": "",
"Leave model field empty to use the default model.": "",
"License": "",
"Lift List": "",
"Light": "Kahayag",
"Listening...": "",
"Llama.cpp": "",
@ -902,6 +908,7 @@
"New Tool": "",
"new-channel": "",
"Next message": "",
"No chats found": "",
"No chats found for this user.": "",
"No chats found.": "",
"No content": "",
@ -1140,6 +1147,7 @@
"See what's new": "Tan-awa unsay bag-o",
"Seed": "Binhi",
"Select a base model": "",
"Select a conversation to preview": "",
"Select a engine": "",
"Select a function": "",
"Select a group": "",
@ -1210,6 +1218,7 @@
"Sign up to {{WEBUI_NAME}}": "",
"Significantly improves accuracy by using an LLM to enhance tables, forms, inline math, and layout detection. Will increase latency. Defaults to True.": "",
"Signing in to {{WEBUI_NAME}}": "",
"Sink List": "",
"sk-1234": "",
"Skip Cache": "",
"Skip the cache and re-run the inference. Defaults to False.": "",
@ -1315,6 +1324,7 @@
"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "",
"To attach knowledge base here, add them to the \"Knowledge\" workspace first.": "",
"To learn more about available endpoints, visit our documentation.": "",
"To learn more about powerful prompt variables, click here": "",
"To protect your privacy, only ratings, model IDs, tags, and metadata are shared from your feedback—your chat logs remain private and are not included.": "",
"To select actions here, add them to the \"Functions\" workspace first.": "",
"To select filters here, add them to the \"Functions\" workspace first.": "",
@ -1361,6 +1371,7 @@
"Unlock mysteries": "",
"Unpin": "",
"Unravel secrets": "",
"Unsupported file type.": "",
"Untagged": "",
"Untitled": "",
"Update": "",
@ -1396,14 +1407,14 @@
"User Webhooks": "",
"Username": "",
"Users": "Mga tiggamit",
"Using Entire Document": "",
"Using Focused Retrieval": "",
"Using the default arena model with all models. Click the plus button to add custom models.": "",
"Utilize": "Sa paggamit",
"Valid time units:": "Balido nga mga yunit sa oras:",
"Valves": "",
"Valves updated": "",
"Valves updated successfully": "",
"variable": "variable",
"variable to have them replaced with clipboard content.": "variable aron pulihan kini sa mga sulud sa clipboard.",
"Verify Connection": "",
"Verify SSL Certificate": "",
"Version": "Bersyon",

View File

@ -290,6 +290,7 @@
"Create Account": "Vytvořit účet",
"Create Admin Account": "Vytvořit admin účet",
"Create Channel": "",
"Create Folder": "",
"Create Group": "Vytvořit skupinu",
"Create Knowledge": "Vytvořit knowledge",
"Create new key": "Vytvořit nový klíč",
@ -413,7 +414,7 @@
"e.g. pdf, docx, txt": "",
"e.g. Tools for performing various operations": "",
"e.g., 3, 4, 5 (leave blank for default)": "",
"e.g., audio/wav,audio/mpeg,video/* (leave blank for defaults, * for all)": "",
"e.g., audio/wav,audio/mpeg,video/* (leave blank for defaults)": "",
"e.g., en-US,ja-JP (leave blank for auto-detect)": "",
"e.g., westus (leave blank for eastus)": "",
"e.g.) en,fr,de": "",
@ -601,6 +602,7 @@
"External Web Loader URL": "",
"External Web Search API Key": "",
"External Web Search URL": "",
"Fade Effect for Streaming Text": "",
"Failed to add file.": "Nepodařilo se přidat soubor.",
"Failed to connect to {{URL}} OpenAPI tool server": "",
"Failed to copy link": "",
@ -610,6 +612,7 @@
"Failed to extract content from the file.": "",
"Failed to fetch models": "",
"Failed to generate title": "",
"Failed to load chat preview": "",
"Failed to load file content.": "",
"Failed to read clipboard contents": "Nepodařilo se přečíst obsah schránky",
"Failed to save connections": "",
@ -755,6 +758,7 @@
"Input commands": "Vstupní příkazy",
"Input Variables": "",
"Insert": "",
"Insert Follow-Up Prompt to Input": "",
"Insert Prompt as Rich Text": "",
"Install from Github URL": "Instalace z URL adresy Githubu",
"Instant Auto-Send After Voice Transcription": "Okamžité automatické odeslání po přepisu hlasu",
@ -778,6 +782,7 @@
"JWT Expiration": "Vypršení JWT",
"JWT Token": "JWT Token (JSON Web Token)",
"Kagi Search API Key": "",
"Keep Follow-Up Prompts in Chat": "",
"Keep in Sidebar": "",
"Key": "",
"Keyboard shortcuts": "Klávesové zkratky",
@ -812,6 +817,7 @@
"Leave empty to use the default prompt, or enter a custom prompt": "Nechte prázdné pro použití výchozího podnětu, nebo zadejte vlastní podnět.",
"Leave model field empty to use the default model.": "",
"License": "",
"Lift List": "",
"Light": "Světlo",
"Listening...": "Poslouchání...",
"Llama.cpp": "",
@ -902,6 +908,7 @@
"New Tool": "",
"new-channel": "",
"Next message": "",
"No chats found": "",
"No chats found for this user.": "",
"No chats found.": "",
"No content": "",
@ -1140,6 +1147,7 @@
"See what's new": "Podívejte se, co je nového",
"Seed": "Semínko",
"Select a base model": "Vyberte základní model",
"Select a conversation to preview": "",
"Select a engine": "Vyberte engine",
"Select a function": "Vyberte funkci",
"Select a group": "",
@ -1210,6 +1218,7 @@
"Sign up to {{WEBUI_NAME}}": "Zaregistrujte se na {{WEBUI_NAME}}",
"Significantly improves accuracy by using an LLM to enhance tables, forms, inline math, and layout detection. Will increase latency. Defaults to True.": "",
"Signing in to {{WEBUI_NAME}}": "Přihlašování do {{WEBUI_NAME}}",
"Sink List": "",
"sk-1234": "",
"Skip Cache": "",
"Skip the cache and re-run the inference. Defaults to False.": "",
@ -1315,6 +1324,7 @@
"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "Pro přístup k WebUI se prosím obraťte na administrátora. Administrátoři mohou spravovat stavy uživatelů z Admin Panelu.",
"To attach knowledge base here, add them to the \"Knowledge\" workspace first.": "Chcete-li zde připojit znalostní databázi, nejprve ji přidejte do workspace \"Knowledge\".",
"To learn more about available endpoints, visit our documentation.": "",
"To learn more about powerful prompt variables, click here": "",
"To protect your privacy, only ratings, model IDs, tags, and metadata are shared from your feedback—your chat logs remain private and are not included.": "Aby byla chráněna vaše soukromí, z vaší zpětné vazby jsou sdílena pouze hodnocení, ID modelů, značky a metadata vaše chatové záznamy zůstávají soukromé a nejsou zahrnuty.",
"To select actions here, add them to the \"Functions\" workspace first.": "Chcete-li zde vybrat akce, nejprve je přidejte do pracovní plochy \"Functions\".",
"To select filters here, add them to the \"Functions\" workspace first.": "Chcete-li zde vybrat filtry, nejprve je přidejte do workspace „Functions“.",
@ -1361,6 +1371,7 @@
"Unlock mysteries": "",
"Unpin": "Odepnout",
"Unravel secrets": "",
"Unsupported file type.": "",
"Untagged": "Nebyla označena",
"Untitled": "",
"Update": "Aktualizovat",
@ -1396,14 +1407,14 @@
"User Webhooks": "",
"Username": "Uživatelské jméno",
"Users": "Uživatelé",
"Using Entire Document": "",
"Using Focused Retrieval": "",
"Using the default arena model with all models. Click the plus button to add custom models.": "Použití výchozího modelu arény se všemi modely. Kliknutím na tlačítko plus přidejte vlastní modely.",
"Utilize": "Využít",
"Valid time units:": "Platné časové jednotky:",
"Valves": "Ventily",
"Valves updated": "Ventily aktualizovány",
"Valves updated successfully": "Ventily byly úspěšně aktualizovány.",
"variable": "proměnná",
"variable to have them replaced with clipboard content.": "proměnnou, aby byl jejich obsah nahrazen obsahem schránky.",
"Verify Connection": "",
"Verify SSL Certificate": "",
"Version": "Verze",

View File

@ -290,6 +290,7 @@
"Create Account": "Opret profil",
"Create Admin Account": "Opret administrator profil",
"Create Channel": "Opret kanal",
"Create Folder": "",
"Create Group": "Opret gruppe",
"Create Knowledge": "Opret Viden",
"Create new key": "Opret en ny nøgle",
@ -413,7 +414,7 @@
"e.g. pdf, docx, txt": "",
"e.g. Tools for performing various operations": "",
"e.g., 3, 4, 5 (leave blank for default)": "",
"e.g., audio/wav,audio/mpeg,video/* (leave blank for defaults, * for all)": "",
"e.g., audio/wav,audio/mpeg,video/* (leave blank for defaults)": "",
"e.g., en-US,ja-JP (leave blank for auto-detect)": "",
"e.g., westus (leave blank for eastus)": "",
"e.g.) en,fr,de": "",
@ -601,6 +602,7 @@
"External Web Loader URL": "Ekstern Web Loader URL",
"External Web Search API Key": "Ekstern Web Search API-nøgle",
"External Web Search URL": "Ekstern Web Search URL",
"Fade Effect for Streaming Text": "",
"Failed to add file.": "Kunne ikke tilføje fil.",
"Failed to connect to {{URL}} OpenAPI tool server": "Kunne ikke forbinde til {{URL}} OpenAPI tool server",
"Failed to copy link": "",
@ -610,6 +612,7 @@
"Failed to extract content from the file.": "",
"Failed to fetch models": "Kunne ikke hente modeller",
"Failed to generate title": "",
"Failed to load chat preview": "",
"Failed to load file content.": "Kunne ikke indlæse filindhold.",
"Failed to read clipboard contents": "Kunne ikke læse indholdet af udklipsholderen",
"Failed to save connections": "Kunne ikke gemme forbindelser",
@ -755,6 +758,7 @@
"Input commands": "Inputkommandoer",
"Input Variables": "",
"Insert": "",
"Insert Follow-Up Prompt to Input": "",
"Insert Prompt as Rich Text": "",
"Install from Github URL": "Installer fra Github URL",
"Instant Auto-Send After Voice Transcription": "Øjeblikkelig automatisk afsendelse efter stemmetransskription",
@ -778,6 +782,7 @@
"JWT Expiration": "JWT-udløb",
"JWT Token": "JWT-token",
"Kagi Search API Key": "",
"Keep Follow-Up Prompts in Chat": "",
"Keep in Sidebar": "",
"Key": "Nøgle",
"Keyboard shortcuts": "Tastaturgenveje",
@ -812,6 +817,7 @@
"Leave empty to use the default prompt, or enter a custom prompt": "Lad stå tomt for at bruge standardprompten, eller indtast en brugerdefineret prompt",
"Leave model field empty to use the default model.": "",
"License": "Licens",
"Lift List": "",
"Light": "Lys",
"Listening...": "Lytter...",
"Llama.cpp": "Llama.cpp",
@ -902,6 +908,7 @@
"New Tool": "Nyt værktøj",
"new-channel": "",
"Next message": "Næste besked",
"No chats found": "",
"No chats found for this user.": "Ingen besked-tråde fundet for denne bruger.",
"No chats found.": "Ingen besked-tråde fundet.",
"No content": "Intet indhold",
@ -1140,6 +1147,7 @@
"See what's new": "Se, hvad der er nyt",
"Seed": "Seed",
"Select a base model": "Vælg en basemodel",
"Select a conversation to preview": "",
"Select a engine": "Vælg en engine",
"Select a function": "Vælg en funktion",
"Select a group": "Vælg en gruppe",
@ -1210,6 +1218,7 @@
"Sign up to {{WEBUI_NAME}}": "Tilmeld dig {{WEBUI_NAME}}",
"Significantly improves accuracy by using an LLM to enhance tables, forms, inline math, and layout detection. Will increase latency. Defaults to True.": "",
"Signing in to {{WEBUI_NAME}}": "Logger ind på {{WEBUI_NAME}}",
"Sink List": "",
"sk-1234": "sk-1234",
"Skip Cache": "",
"Skip the cache and re-run the inference. Defaults to False.": "",
@ -1315,6 +1324,7 @@
"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "For at få adgang til WebUI skal du kontakte administratoren. Administratorer kan administrere brugerstatus fra administrationspanelet.",
"To attach knowledge base here, add them to the \"Knowledge\" workspace first.": "For at vedhæfte vidensbase her skal du først tilføje dem til \"Viden\"-arbejdsområdet.",
"To learn more about available endpoints, visit our documentation.": "",
"To learn more about powerful prompt variables, click here": "",
"To protect your privacy, only ratings, model IDs, tags, and metadata are shared from your feedback—your chat logs remain private and are not included.": "",
"To select actions here, add them to the \"Functions\" workspace first.": "For at vælge handlinger her skal du først tilføje dem til \"Funktioner\"-arbejdsområdet.",
"To select filters here, add them to the \"Functions\" workspace first.": "For at vælge filtre her skal du først tilføje dem til \"Funktioner\"-arbejdsområdet.",
@ -1361,6 +1371,7 @@
"Unlock mysteries": "Lås op for mysterier",
"Unpin": "Frigør",
"Unravel secrets": "Afslør hemmeligheder",
"Unsupported file type.": "",
"Untagged": "Uden mærker",
"Untitled": "Unavngivet",
"Update": "Opdater",
@ -1396,14 +1407,14 @@
"User Webhooks": "Bruger Webhooks",
"Username": "Brugernavn",
"Users": "Brugere",
"Using Entire Document": "",
"Using Focused Retrieval": "",
"Using the default arena model with all models. Click the plus button to add custom models.": "Brug den standard Arena-model med alle modeller. Klik på plusknappen for at tilføje brugerdefinerede modeller.",
"Utilize": "Anvend",
"Valid time units:": "Gyldige tidsenheder:",
"Valves": "Ventiler",
"Valves updated": "Ventiler opdateret",
"Valves updated successfully": "Ventiler opdateret.",
"variable": "variabel",
"variable to have them replaced with clipboard content.": "variabel for at få dem erstattet med indholdet af udklipsholderen.",
"Verify Connection": "Verificer forbindelse",
"Verify SSL Certificate": "Verificer SSL-certifikat",
"Version": "Version",

View File

@ -167,7 +167,7 @@
"By {{name}}": "Von {{name}}",
"Bypass Embedding and Retrieval": "Embedding und Retrieval umgehen",
"Bypass Web Loader": "",
"Cache Base Model List": "",
"Cache Base Model List": "Basis Modell-Liste cachen",
"Calendar": "Kalender",
"Call": "Anrufen",
"Call feature is not supported when using Web STT engine": "Die Anruffunktion wird nicht unterstützt, wenn die Web-STT-Engine verwendet wird.",
@ -290,6 +290,7 @@
"Create Account": "Konto erstellen",
"Create Admin Account": "Administrator-Account erstellen",
"Create Channel": "Kanal erstellen",
"Create Folder": "",
"Create Group": "Gruppe erstellen",
"Create Knowledge": "Wissen erstellen",
"Create new key": "Neuen Schlüssel erstellen",
@ -413,7 +414,7 @@
"e.g. pdf, docx, txt": "z. B. pdf, docx, txt",
"e.g. Tools for performing various operations": "z. B. Werkzeuge für verschiedene Operationen",
"e.g., 3, 4, 5 (leave blank for default)": "z. B. 3, 4, 5 (leer lassen für Standard)",
"e.g., audio/wav,audio/mpeg,video/* (leave blank for defaults, * for all)": "",
"e.g., audio/wav,audio/mpeg,video/* (leave blank for defaults)": "",
"e.g., en-US,ja-JP (leave blank for auto-detect)": "z. B. en-US,de-DE (freilassen für automatische Erkennung)",
"e.g., westus (leave blank for eastus)": "z. B. westus (leer lassen für eastus)",
"e.g.) en,fr,de": "z. B. en,fr,de",
@ -601,15 +602,17 @@
"External Web Loader URL": "Externer Web-Loader URL",
"External Web Search API Key": "Externe Websuche API-Schlüssel",
"External Web Search URL": "Externe Websuche URL",
"Fade Effect for Streaming Text": "",
"Failed to add file.": "Fehler beim Hinzufügen der Datei.",
"Failed to connect to {{URL}} OpenAPI tool server": "Verbindung zum OpenAPI-Toolserver {{URL}} fehlgeschlagen",
"Failed to copy link": "Fehler beim kopieren des Links",
"Failed to create API Key.": "Fehler beim Erstellen des API-Schlüssels.",
"Failed to delete note": "Notiz konnte nicht gelöscht werden",
"Failed to extract content from the file: {{error}}": "",
"Failed to extract content from the file.": "",
"Failed to extract content from the file: {{error}}": "Fehler beim extrahieren des Inhalts aus der Datei: {{error}}",
"Failed to extract content from the file.": "Fehler beim extrahieren des Inhalts aus der Datei.",
"Failed to fetch models": "Fehler beim Abrufen der Modelle",
"Failed to generate title": "Fehler beim generieren des Titels",
"Failed to load chat preview": "",
"Failed to load file content.": "Fehler beim Laden des Dateiinhalts.",
"Failed to read clipboard contents": "Fehler beim Lesen des Inhalts der Zwischenablage.",
"Failed to save connections": "Verbindungen konnten nicht gespeichert werden",
@ -755,6 +758,7 @@
"Input commands": "Eingabebefehle",
"Input Variables": "Eingabe Variablen",
"Insert": "Einfügen",
"Insert Follow-Up Prompt to Input": "",
"Insert Prompt as Rich Text": "Prompt als Rich Text einfügen",
"Install from Github URL": "Von GitHub-URL installieren",
"Instant Auto-Send After Voice Transcription": "Spracherkennung direkt absenden",
@ -778,12 +782,13 @@
"JWT Expiration": "JWT-Ablauf",
"JWT Token": "JWT-Token",
"Kagi Search API Key": "Kagi Search API-Schlüssel",
"Keep Follow-Up Prompts in Chat": "",
"Keep in Sidebar": "In Seitenleiste anzeigen",
"Key": "Schlüssel",
"Keyboard shortcuts": "Tastenkombinationen",
"Knowledge": "Wissen",
"Knowledge Access": "Wissenszugriff",
"Knowledge Base": "",
"Knowledge Base": "Wissensdatenbank",
"Knowledge created successfully.": "Wissen erfolgreich erstellt.",
"Knowledge deleted successfully.": "Wissen erfolgreich gelöscht.",
"Knowledge Public Sharing": "Öffentliche Freigabe von Wissen",
@ -812,6 +817,7 @@
"Leave empty to use the default prompt, or enter a custom prompt": "Leer lassen, um den Standardprompt zu verwenden, oder geben Sie einen benutzerdefinierten Prompt ein",
"Leave model field empty to use the default model.": "Leer lassen, um das Standardmodell zu verwenden.",
"License": "Lizenz",
"Lift List": "",
"Light": "Hell",
"Listening...": "Höre zu...",
"Llama.cpp": "Llama.cpp",
@ -902,6 +908,7 @@
"New Tool": "Neues Werkzeug",
"new-channel": "neuer-kanal",
"Next message": "Nächste Nachricht",
"No chats found": "",
"No chats found for this user.": "Keine Chats für diesen Nutzer gefunden.",
"No chats found.": "Keine Chats gefunden.",
"No content": "Kein Inhalt",
@ -1056,8 +1063,8 @@
"Re-rank models by topic similarity": "Modelle nach thematischer Ähnlichkeit neu ordnen",
"Read": "Lesen",
"Read Aloud": "Vorlesen",
"Reason": "",
"Reasoning Effort": "Schlussfolgerungsaufwand",
"Reason": "Nachdenken",
"Reasoning Effort": "Nachdenk-Aufwand",
"Record": "Aufzeichnen",
"Record voice": "Stimme aufnehmen",
"Redirecting you to Open WebUI Community": "Sie werden zur OpenWebUI-Community weitergeleitet",
@ -1069,7 +1076,7 @@
"Reindex": "Neu indexieren",
"Reindex Knowledge Base Vectors": "Vektoren der Wissensdatenbank neu indizieren",
"Release Notes": "Veröffentlichungshinweise",
"Releases": "",
"Releases": "Versionen",
"Relevance": "Relevanz",
"Relevance Threshold": "Relevanzschwelle",
"Remember Dismissal": "",
@ -1090,7 +1097,7 @@
"Reset Upload Directory": "Upload-Verzeichnis zurücksetzen",
"Reset Vector Storage/Knowledge": "Vektorspeicher/Wissen zurücksetzen",
"Reset view": "Ansicht zurücksetzen",
"Response": "",
"Response": "Antwort",
"Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "Benachrichtigungen können nicht aktiviert werden, da die Website-Berechtigungen abgelehnt wurden. Bitte besuchen Sie Ihre Browser-Einstellungen, um den erforderlichen Zugriff zu gewähren.",
"Response splitting": "Antwortaufteilung",
"Response Watermark": "Antwort Wasserzeichen",
@ -1140,6 +1147,7 @@
"See what's new": "Entdecken Sie die Neuigkeiten",
"Seed": "Seed",
"Select a base model": "Wählen Sie ein Basismodell",
"Select a conversation to preview": "",
"Select a engine": "Wählen Sie eine Engine",
"Select a function": "Wählen Sie eine Funktion",
"Select a group": "Wählen Sie eine Gruppe",
@ -1210,6 +1218,7 @@
"Sign up to {{WEBUI_NAME}}": "Bei {{WEBUI_NAME}} registrieren",
"Significantly improves accuracy by using an LLM to enhance tables, forms, inline math, and layout detection. Will increase latency. Defaults to True.": "",
"Signing in to {{WEBUI_NAME}}": "Wird bei {{WEBUI_NAME}} angemeldet",
"Sink List": "",
"sk-1234": "sk-1234",
"Skip Cache": "Cache überspringen",
"Skip the cache and re-run the inference. Defaults to False.": "",
@ -1315,6 +1324,7 @@
"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "Um auf das WebUI zugreifen zu können, wenden Sie sich bitte an einen Administrator. Administratoren können den Benutzerstatus über das Admin-Panel verwalten.",
"To attach knowledge base here, add them to the \"Knowledge\" workspace first.": "Um Wissensdatenbanken hier anzuhängen, fügen Sie sie zunächst dem Arbeitsbereich \"Wissen\" hinzu.",
"To learn more about available endpoints, visit our documentation.": "Um mehr über verfügbare Endpunkte zu erfahren, besuchen Sie unsere Dokumentation.",
"To learn more about powerful prompt variables, click here": "",
"To protect your privacy, only ratings, model IDs, tags, and metadata are shared from your feedback—your chat logs remain private and are not included.": "Um Ihre Privatsphäre zu schützen, werden nur Bewertungen, Modell-IDs, Tags und Metadaten aus Ihrem Feedback geteilt Ihre Chats bleiben privat und werden nicht einbezogen.",
"To select actions here, add them to the \"Functions\" workspace first.": "Um Aktionen auszuwählen, fügen Sie diese zunächst dem Arbeitsbereich „Funktionen“ hinzu.",
"To select filters here, add them to the \"Functions\" workspace first.": "Um Filter auszuwählen, fügen Sie diese zunächst dem Arbeitsbereich „Funktionen“ hinzu.",
@ -1361,6 +1371,7 @@
"Unlock mysteries": "Geheimnisse entsperren",
"Unpin": "Lösen",
"Unravel secrets": "Geheimnisse lüften",
"Unsupported file type.": "",
"Untagged": "Ungetaggt",
"Untitled": "Unbenannt",
"Update": "Aktualisieren",
@ -1396,14 +1407,14 @@
"User Webhooks": "Benutzer Webhooks",
"Username": "Benutzername",
"Users": "Benutzer",
"Using Entire Document": "",
"Using Focused Retrieval": "",
"Using the default arena model with all models. Click the plus button to add custom models.": "Verwendung des Standard-Arena-Modells mit allen Modellen. Klicken Sie auf die Plus-Schaltfläche, um benutzerdefinierte Modelle hinzuzufügen.",
"Utilize": "Verwenden",
"Valid time units:": "Gültige Zeiteinheiten:",
"Valves": "Valves",
"Valves updated": "Valves aktualisiert",
"Valves updated successfully": "Valves erfolgreich aktualisiert",
"variable": "Variable",
"variable to have them replaced with clipboard content.": "Variable, um den Inhalt der Zwischenablage beim Nutzen des Prompts zu ersetzen.",
"Verify Connection": "Verbindung verifizieren",
"Verify SSL Certificate": "SSL Zertifikat prüfen",
"Version": "Version",
@ -1414,7 +1425,7 @@
"Vision": "Bilderkennung",
"Voice": "Stimme",
"Voice Input": "Spracheingabe",
"Voice mode": "",
"Voice mode": "Sprachmodus",
"Warning": "Warnung",
"Warning:": "Warnung:",
"Warning: Enabling this will allow users to upload arbitrary code on the server.": "Warnung: Wenn Sie dies aktivieren, können Benutzer beliebigen Code auf dem Server hochladen.",

View File

@ -290,6 +290,7 @@
"Create Account": "Create Account",
"Create Admin Account": "",
"Create Channel": "",
"Create Folder": "",
"Create Group": "",
"Create Knowledge": "",
"Create new key": "",
@ -413,7 +414,7 @@
"e.g. pdf, docx, txt": "",
"e.g. Tools for performing various operations": "",
"e.g., 3, 4, 5 (leave blank for default)": "",
"e.g., audio/wav,audio/mpeg,video/* (leave blank for defaults, * for all)": "",
"e.g., audio/wav,audio/mpeg,video/* (leave blank for defaults)": "",
"e.g., en-US,ja-JP (leave blank for auto-detect)": "",
"e.g., westus (leave blank for eastus)": "",
"e.g.) en,fr,de": "",
@ -601,6 +602,7 @@
"External Web Loader URL": "",
"External Web Search API Key": "",
"External Web Search URL": "",
"Fade Effect for Streaming Text": "",
"Failed to add file.": "",
"Failed to connect to {{URL}} OpenAPI tool server": "",
"Failed to copy link": "",
@ -610,6 +612,7 @@
"Failed to extract content from the file.": "",
"Failed to fetch models": "",
"Failed to generate title": "",
"Failed to load chat preview": "",
"Failed to load file content.": "",
"Failed to read clipboard contents": "Failed to read clipboard borks",
"Failed to save connections": "",
@ -755,6 +758,7 @@
"Input commands": "Input commands",
"Input Variables": "",
"Insert": "",
"Insert Follow-Up Prompt to Input": "",
"Insert Prompt as Rich Text": "",
"Install from Github URL": "",
"Instant Auto-Send After Voice Transcription": "",
@ -778,6 +782,7 @@
"JWT Expiration": "JWT Expire",
"JWT Token": "JWT Borken",
"Kagi Search API Key": "",
"Keep Follow-Up Prompts in Chat": "",
"Keep in Sidebar": "",
"Key": "",
"Keyboard shortcuts": "Keyboard Barkcuts",
@ -812,6 +817,7 @@
"Leave empty to use the default prompt, or enter a custom prompt": "",
"Leave model field empty to use the default model.": "",
"License": "",
"Lift List": "",
"Light": "Light",
"Listening...": "",
"Llama.cpp": "",
@ -902,6 +908,7 @@
"New Tool": "",
"new-channel": "",
"Next message": "",
"No chats found": "",
"No chats found for this user.": "",
"No chats found.": "",
"No content": "",
@ -1140,6 +1147,7 @@
"See what's new": "See what's new so amaze",
"Seed": "Seed very plant",
"Select a base model": "",
"Select a conversation to preview": "",
"Select a engine": "",
"Select a function": "",
"Select a group": "",
@ -1210,6 +1218,7 @@
"Sign up to {{WEBUI_NAME}}": "",
"Significantly improves accuracy by using an LLM to enhance tables, forms, inline math, and layout detection. Will increase latency. Defaults to True.": "",
"Signing in to {{WEBUI_NAME}}": "",
"Sink List": "",
"sk-1234": "",
"Skip Cache": "",
"Skip the cache and re-run the inference. Defaults to False.": "",
@ -1315,6 +1324,7 @@
"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "",
"To attach knowledge base here, add them to the \"Knowledge\" workspace first.": "",
"To learn more about available endpoints, visit our documentation.": "",
"To learn more about powerful prompt variables, click here": "",
"To protect your privacy, only ratings, model IDs, tags, and metadata are shared from your feedback—your chat logs remain private and are not included.": "",
"To select actions here, add them to the \"Functions\" workspace first.": "",
"To select filters here, add them to the \"Functions\" workspace first.": "",
@ -1361,6 +1371,7 @@
"Unlock mysteries": "",
"Unpin": "",
"Unravel secrets": "",
"Unsupported file type.": "",
"Untagged": "",
"Untitled": "",
"Update": "",
@ -1396,14 +1407,14 @@
"User Webhooks": "",
"Username": "",
"Users": "Users much users",
"Using Entire Document": "",
"Using Focused Retrieval": "",
"Using the default arena model with all models. Click the plus button to add custom models.": "",
"Utilize": "Utilize very use",
"Valid time units:": "Valid time units: much time",
"Valves": "",
"Valves updated": "",
"Valves updated successfully": "",
"variable": "variable very variable",
"variable to have them replaced with clipboard content.": "variable to have them replaced with clipboard content. Very replace.",
"Verify Connection": "",
"Verify SSL Certificate": "",
"Version": "Version much version",

View File

@ -290,6 +290,7 @@
"Create Account": "Δημιουργία Λογαριασμού",
"Create Admin Account": "Δημιουργία Λογαριασμού Διαχειριστή",
"Create Channel": "",
"Create Folder": "",
"Create Group": "Δημιουργία Ομάδας",
"Create Knowledge": "Δημιουργία Γνώσης",
"Create new key": "Δημιουργία νέου κλειδιού",
@ -413,7 +414,7 @@
"e.g. pdf, docx, txt": "",
"e.g. Tools for performing various operations": "π.χ. Εργαλεία για την εκτέλεση διάφορων λειτουργιών",
"e.g., 3, 4, 5 (leave blank for default)": "",
"e.g., audio/wav,audio/mpeg,video/* (leave blank for defaults, * for all)": "",
"e.g., audio/wav,audio/mpeg,video/* (leave blank for defaults)": "",
"e.g., en-US,ja-JP (leave blank for auto-detect)": "",
"e.g., westus (leave blank for eastus)": "",
"e.g.) en,fr,de": "",
@ -601,6 +602,7 @@
"External Web Loader URL": "",
"External Web Search API Key": "",
"External Web Search URL": "",
"Fade Effect for Streaming Text": "",
"Failed to add file.": "Αποτυχία προσθήκης αρχείου.",
"Failed to connect to {{URL}} OpenAPI tool server": "",
"Failed to copy link": "",
@ -610,6 +612,7 @@
"Failed to extract content from the file.": "",
"Failed to fetch models": "",
"Failed to generate title": "",
"Failed to load chat preview": "",
"Failed to load file content.": "",
"Failed to read clipboard contents": "Αποτυχία ανάγνωσης περιεχομένων πρόχειρου",
"Failed to save connections": "",
@ -755,6 +758,7 @@
"Input commands": "Εισαγωγή εντολών",
"Input Variables": "",
"Insert": "",
"Insert Follow-Up Prompt to Input": "",
"Insert Prompt as Rich Text": "",
"Install from Github URL": "Εγκατάσταση από URL Github",
"Instant Auto-Send After Voice Transcription": "Άμεση Αυτόματη Αποστολή μετά τη μεταγραφή φωνής",
@ -778,6 +782,7 @@
"JWT Expiration": "Λήξη JWT",
"JWT Token": "Token JWT",
"Kagi Search API Key": "",
"Keep Follow-Up Prompts in Chat": "",
"Keep in Sidebar": "",
"Key": "Κλειδί",
"Keyboard shortcuts": "Συντομεύσεις Πληκτρολογίου",
@ -812,6 +817,7 @@
"Leave empty to use the default prompt, or enter a custom prompt": "Αφήστε κενό για να χρησιμοποιήσετε την προεπιλεγμένη προτροπή, ή εισάγετε μια προσαρμοσμένη προτροπή",
"Leave model field empty to use the default model.": "",
"License": "",
"Lift List": "",
"Light": "Φως",
"Listening...": "Ακούγεται...",
"Llama.cpp": "",
@ -902,6 +908,7 @@
"New Tool": "",
"new-channel": "",
"Next message": "",
"No chats found": "",
"No chats found for this user.": "",
"No chats found.": "",
"No content": "",
@ -1140,6 +1147,7 @@
"See what's new": "Δείτε τι νέο υπάρχει",
"Seed": "Seed",
"Select a base model": "Επιλέξτε ένα βασικό μοντέλο",
"Select a conversation to preview": "",
"Select a engine": "Επιλέξτε μια μηχανή",
"Select a function": "Επιλέξτε μια λειτουργία",
"Select a group": "Επιλέξτε μια ομάδα",
@ -1210,6 +1218,7 @@
"Sign up to {{WEBUI_NAME}}": "Εγγραφή στο {{WEBUI_NAME}}",
"Significantly improves accuracy by using an LLM to enhance tables, forms, inline math, and layout detection. Will increase latency. Defaults to True.": "",
"Signing in to {{WEBUI_NAME}}": "Σύνδεση στο {{WEBUI_NAME}}",
"Sink List": "",
"sk-1234": "",
"Skip Cache": "",
"Skip the cache and re-run the inference. Defaults to False.": "",
@ -1315,6 +1324,7 @@
"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "Για να αποκτήσετε πρόσβαση στο WebUI, παρακαλώ επικοινωνήστε με τον διαχειριστή. Οι διαχειριστές μπορούν να διαχειριστούν τις καταστάσεις των χρηστών από τον Πίνακα Διαχειριστή.",
"To attach knowledge base here, add them to the \"Knowledge\" workspace first.": "Για να επισυνάψετε τη βάση γνώσης εδώ, προσθέστε τα πρώτα στο χώρο εργασίας \"Knowledge\".",
"To learn more about available endpoints, visit our documentation.": "",
"To learn more about powerful prompt variables, click here": "",
"To protect your privacy, only ratings, model IDs, tags, and metadata are shared from your feedback—your chat logs remain private and are not included.": "Για να προστατεύσετε την ιδιωτικότητά σας, μόνο οι βαθμολογίες, τα IDs μοντέλων, οι ετικέτες και τα μεταδεδομένα μοιράζονται από την ανατροφοδότησή σας—τα αρχεία συνομιλιών σας παραμένουν ιδιωτικά και δεν περιλαμβάνονται.",
"To select actions here, add them to the \"Functions\" workspace first.": "Για να επιλέξετε ενέργειες εδώ, προσθέστε τις πρώτα στο χώρο εργασίας \"Functions\".",
"To select filters here, add them to the \"Functions\" workspace first.": "Για να επιλέξετε φίλτρα εδώ, προσθέστε τα πρώτα στο χώρο εργασίας \"Functions\".",
@ -1361,6 +1371,7 @@
"Unlock mysteries": "Ξεκλείδωμα μυστηρίων",
"Unpin": "Αφαίρεση καρφίτσματος",
"Unravel secrets": "Ξετυλίξτε μυστικά",
"Unsupported file type.": "",
"Untagged": "Χωρίς Ετικέτες",
"Untitled": "",
"Update": "Ενημέρωση",
@ -1396,14 +1407,14 @@
"User Webhooks": "",
"Username": "Όνομα Χρήστη",
"Users": "Χρήστες",
"Using Entire Document": "",
"Using Focused Retrieval": "",
"Using the default arena model with all models. Click the plus button to add custom models.": "Χρησιμοποιώντας το προεπιλεγμένο μοντέλο arena με όλα τα μοντέλα. Κάντε κλικ στο κουμπί συν για να προσθέσετε προσαρμοσμένα μοντέλα.",
"Utilize": "Αξιοποίηση",
"Valid time units:": "Έγκυρες μονάδες χρόνου:",
"Valves": "Βαλβίδες",
"Valves updated": "Οι βαλβίδες ενημερώθηκαν",
"Valves updated successfully": "Οι βαλβίδες ενημερώθηκαν με επιτυχία",
"variable": "μεταβλητή",
"variable to have them replaced with clipboard content.": "μεταβλητή να αντικατασταθούν με το περιεχόμενο του πρόχειρου.",
"Verify Connection": "",
"Verify SSL Certificate": "",
"Version": "Έκδοση",

View File

@ -290,6 +290,7 @@
"Create Account": "",
"Create Admin Account": "",
"Create Channel": "",
"Create Folder": "",
"Create Group": "",
"Create Knowledge": "",
"Create new key": "",
@ -413,7 +414,7 @@
"e.g. pdf, docx, txt": "",
"e.g. Tools for performing various operations": "",
"e.g., 3, 4, 5 (leave blank for default)": "",
"e.g., audio/wav,audio/mpeg,video/* (leave blank for defaults, * for all)": "",
"e.g., audio/wav,audio/mpeg,video/* (leave blank for defaults)": "",
"e.g., en-US,ja-JP (leave blank for auto-detect)": "",
"e.g., westus (leave blank for eastus)": "",
"e.g.) en,fr,de": "",
@ -601,6 +602,7 @@
"External Web Loader URL": "",
"External Web Search API Key": "",
"External Web Search URL": "",
"Fade Effect for Streaming Text": "",
"Failed to add file.": "",
"Failed to connect to {{URL}} OpenAPI tool server": "",
"Failed to copy link": "",
@ -610,6 +612,7 @@
"Failed to extract content from the file.": "",
"Failed to fetch models": "",
"Failed to generate title": "",
"Failed to load chat preview": "",
"Failed to load file content.": "",
"Failed to read clipboard contents": "",
"Failed to save connections": "",
@ -755,6 +758,7 @@
"Input commands": "",
"Input Variables": "",
"Insert": "",
"Insert Follow-Up Prompt to Input": "",
"Insert Prompt as Rich Text": "",
"Install from Github URL": "",
"Instant Auto-Send After Voice Transcription": "",
@ -778,6 +782,7 @@
"JWT Expiration": "",
"JWT Token": "",
"Kagi Search API Key": "",
"Keep Follow-Up Prompts in Chat": "",
"Keep in Sidebar": "",
"Key": "",
"Keyboard shortcuts": "",
@ -812,6 +817,7 @@
"Leave empty to use the default prompt, or enter a custom prompt": "",
"Leave model field empty to use the default model.": "",
"License": "",
"Lift List": "",
"Light": "",
"Listening...": "",
"Llama.cpp": "",
@ -902,6 +908,7 @@
"New Tool": "",
"new-channel": "",
"Next message": "",
"No chats found": "",
"No chats found for this user.": "",
"No chats found.": "",
"No content": "",
@ -1140,6 +1147,7 @@
"See what's new": "",
"Seed": "",
"Select a base model": "",
"Select a conversation to preview": "",
"Select a engine": "",
"Select a function": "",
"Select a group": "",
@ -1210,6 +1218,7 @@
"Sign up to {{WEBUI_NAME}}": "",
"Significantly improves accuracy by using an LLM to enhance tables, forms, inline math, and layout detection. Will increase latency. Defaults to True.": "",
"Signing in to {{WEBUI_NAME}}": "",
"Sink List": "",
"sk-1234": "",
"Skip Cache": "",
"Skip the cache and re-run the inference. Defaults to False.": "",
@ -1315,6 +1324,7 @@
"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "",
"To attach knowledge base here, add them to the \"Knowledge\" workspace first.": "",
"To learn more about available endpoints, visit our documentation.": "",
"To learn more about powerful prompt variables, click here": "",
"To protect your privacy, only ratings, model IDs, tags, and metadata are shared from your feedback—your chat logs remain private and are not included.": "",
"To select actions here, add them to the \"Functions\" workspace first.": "",
"To select filters here, add them to the \"Functions\" workspace first.": "",
@ -1361,6 +1371,7 @@
"Unlock mysteries": "",
"Unpin": "",
"Unravel secrets": "",
"Unsupported file type.": "",
"Untagged": "",
"Untitled": "",
"Update": "",
@ -1396,14 +1407,14 @@
"User Webhooks": "",
"Username": "",
"Users": "",
"Using Entire Document": "",
"Using Focused Retrieval": "",
"Using the default arena model with all models. Click the plus button to add custom models.": "",
"Utilize": "Utilise",
"Valid time units:": "",
"Valves": "",
"Valves updated": "",
"Valves updated successfully": "",
"variable": "",
"variable to have them replaced with clipboard content.": "",
"Verify Connection": "",
"Verify SSL Certificate": "",
"Version": "",

View File

@ -290,6 +290,7 @@
"Create Account": "",
"Create Admin Account": "",
"Create Channel": "",
"Create Folder": "",
"Create Group": "",
"Create Knowledge": "",
"Create new key": "",
@ -413,7 +414,7 @@
"e.g. pdf, docx, txt": "",
"e.g. Tools for performing various operations": "",
"e.g., 3, 4, 5 (leave blank for default)": "",
"e.g., audio/wav,audio/mpeg,video/* (leave blank for defaults, * for all)": "",
"e.g., audio/wav,audio/mpeg,video/* (leave blank for defaults)": "",
"e.g., en-US,ja-JP (leave blank for auto-detect)": "",
"e.g., westus (leave blank for eastus)": "",
"e.g.) en,fr,de": "",
@ -601,6 +602,7 @@
"External Web Loader URL": "",
"External Web Search API Key": "",
"External Web Search URL": "",
"Fade Effect for Streaming Text": "",
"Failed to add file.": "",
"Failed to connect to {{URL}} OpenAPI tool server": "",
"Failed to copy link": "",
@ -610,6 +612,7 @@
"Failed to extract content from the file.": "",
"Failed to fetch models": "",
"Failed to generate title": "",
"Failed to load chat preview": "",
"Failed to load file content.": "",
"Failed to read clipboard contents": "",
"Failed to save connections": "",
@ -755,6 +758,7 @@
"Input commands": "",
"Input Variables": "",
"Insert": "",
"Insert Follow-Up Prompt to Input": "",
"Insert Prompt as Rich Text": "",
"Install from Github URL": "",
"Instant Auto-Send After Voice Transcription": "",
@ -778,6 +782,7 @@
"JWT Expiration": "",
"JWT Token": "",
"Kagi Search API Key": "",
"Keep Follow-Up Prompts in Chat": "",
"Keep in Sidebar": "",
"Key": "",
"Keyboard shortcuts": "",
@ -812,6 +817,7 @@
"Leave empty to use the default prompt, or enter a custom prompt": "",
"Leave model field empty to use the default model.": "",
"License": "",
"Lift List": "",
"Light": "",
"Listening...": "",
"Llama.cpp": "",
@ -902,6 +908,7 @@
"New Tool": "",
"new-channel": "",
"Next message": "Next message",
"No chats found": "",
"No chats found for this user.": "",
"No chats found.": "",
"No content": "",
@ -1140,6 +1147,7 @@
"See what's new": "",
"Seed": "",
"Select a base model": "",
"Select a conversation to preview": "",
"Select a engine": "",
"Select a function": "",
"Select a group": "",
@ -1210,6 +1218,7 @@
"Sign up to {{WEBUI_NAME}}": "",
"Significantly improves accuracy by using an LLM to enhance tables, forms, inline math, and layout detection. Will increase latency. Defaults to True.": "",
"Signing in to {{WEBUI_NAME}}": "",
"Sink List": "",
"sk-1234": "",
"Skip Cache": "",
"Skip the cache and re-run the inference. Defaults to False.": "",
@ -1315,6 +1324,7 @@
"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "",
"To attach knowledge base here, add them to the \"Knowledge\" workspace first.": "",
"To learn more about available endpoints, visit our documentation.": "",
"To learn more about powerful prompt variables, click here": "",
"To protect your privacy, only ratings, model IDs, tags, and metadata are shared from your feedback—your chat logs remain private and are not included.": "",
"To select actions here, add them to the \"Functions\" workspace first.": "",
"To select filters here, add them to the \"Functions\" workspace first.": "",
@ -1361,6 +1371,7 @@
"Unlock mysteries": "",
"Unpin": "",
"Unravel secrets": "",
"Unsupported file type.": "",
"Untagged": "",
"Untitled": "",
"Update": "",
@ -1396,14 +1407,14 @@
"User Webhooks": "",
"Username": "",
"Users": "",
"Using Entire Document": "",
"Using Focused Retrieval": "",
"Using the default arena model with all models. Click the plus button to add custom models.": "",
"Utilize": "",
"Valid time units:": "",
"Valves": "",
"Valves updated": "",
"Valves updated successfully": "",
"variable": "",
"variable to have them replaced with clipboard content.": "",
"Verify Connection": "",
"Verify SSL Certificate": "",
"Version": "",

View File

@ -290,6 +290,7 @@
"Create Account": "Crear Cuenta",
"Create Admin Account": "Crear Cuenta Administrativa",
"Create Channel": "Crear Canal",
"Create Folder": "",
"Create Group": "Crear Grupo",
"Create Knowledge": "Crear Conocimiento",
"Create new key": "Crear Nueva Clave",
@ -413,7 +414,7 @@
"e.g. pdf, docx, txt": "p.ej. pdf, docx, txt ...",
"e.g. Tools for performing various operations": "p.ej. Herramientas para realizar diversas operaciones",
"e.g., 3, 4, 5 (leave blank for default)": "p.ej. , 3, 4, 5 ...",
"e.g., audio/wav,audio/mpeg,video/* (leave blank for defaults, * for all)": "e.g., audio/wav,audio/mpeg,video/* (dejar en blanco para predeterminados, * para todos)",
"e.g., audio/wav,audio/mpeg,video/* (leave blank for defaults)": "",
"e.g., en-US,ja-JP (leave blank for auto-detect)": "p.ej., en-US,ja-JP (dejar en blanco para detectar automáticamente)",
"e.g., westus (leave blank for eastus)": "p.ej. ,oeste (dejar vacío para este)",
"e.g.) en,fr,de": "p.ej. ) en,es,fr,de",
@ -601,6 +602,7 @@
"External Web Loader URL": "URL del Cargador Web Externo",
"External Web Search API Key": "Clave API del Buscador Web Externo",
"External Web Search URL": "URL del Buscador Web Externo",
"Fade Effect for Streaming Text": "",
"Failed to add file.": "Fallo al añadir el archivo.",
"Failed to connect to {{URL}} OpenAPI tool server": "Fallo al conectar al servidor de herramientas {{URL}}",
"Failed to copy link": "Fallo al copiar enlace",
@ -610,6 +612,7 @@
"Failed to extract content from the file.": "Fallo al extraer el contenido del fichero.",
"Failed to fetch models": "Fallo al obtener los modelos",
"Failed to generate title": "Fallo al generar el título",
"Failed to load chat preview": "",
"Failed to load file content.": "Fallo al cargar el contenido del archivo",
"Failed to read clipboard contents": "Fallo al leer el contenido del portapapeles",
"Failed to save connections": "Fallo al grabar las conexiones",
@ -755,6 +758,7 @@
"Input commands": "Ingresar comandos",
"Input Variables": "Ingresar variables",
"Insert": "Insertar",
"Insert Follow-Up Prompt to Input": "",
"Insert Prompt as Rich Text": "Insertar Indicador como Texto Enriquecido",
"Install from Github URL": "Instalar desde la URL de Github",
"Instant Auto-Send After Voice Transcription": "AutoEnvio Instantaneo tras la Transcripción de Voz",
@ -778,6 +782,7 @@
"JWT Expiration": "Expiración del JSON Web Token (JWT)",
"JWT Token": "JSON Web Token",
"Kagi Search API Key": "Clave API de Kagi Search",
"Keep Follow-Up Prompts in Chat": "",
"Keep in Sidebar": "Mantener en Barra Lateral",
"Key": "Clave",
"Keyboard shortcuts": "Atajos de teclado",
@ -812,6 +817,7 @@
"Leave empty to use the default prompt, or enter a custom prompt": "Dejar vacío para usar el indicador predeterminado, o Ingresar un indicador personalizado",
"Leave model field empty to use the default model.": "Dejar vacío el campo modelo para usar el modelo predeterminado.",
"License": "Licencia",
"Lift List": "",
"Light": "Claro",
"Listening...": "Escuchando...",
"Llama.cpp": "Llama.cpp",
@ -902,6 +908,7 @@
"New Tool": "Nueva Herramienta",
"new-channel": "nuevo-canal",
"Next message": "Siguiente mensaje",
"No chats found": "",
"No chats found for this user.": "No se encontró ningún chat de este usuario",
"No chats found.": "No se encontró ningún chat",
"No content": "Sin contenido",
@ -1140,6 +1147,7 @@
"See what's new": "Ver las novedades",
"Seed": "Semilla",
"Select a base model": "Seleccionar un modelo base",
"Select a conversation to preview": "",
"Select a engine": "Seleccionar un motor",
"Select a function": "Seleccionar una función",
"Select a group": "Seleccionar un grupo",
@ -1210,6 +1218,7 @@
"Sign up to {{WEBUI_NAME}}": "Crear una Cuenta en {{WEBUI_NAME}}",
"Significantly improves accuracy by using an LLM to enhance tables, forms, inline math, and layout detection. Will increase latency. Defaults to True.": "Mejora significativamente la precisión mediante el uso de un LLM para optimizar tablas, formularios, cálculos en línea y la detección de diseño. Incrementa la latencia. El valor predeterminado es 'Verdadero'",
"Signing in to {{WEBUI_NAME}}": "Iniciando Sesión en {{WEBUI_NAME}}",
"Sink List": "",
"sk-1234": "sk-1234",
"Skip Cache": "Evitar Caché",
"Skip the cache and re-run the inference. Defaults to False.": "Evitar caché y reiniciar la interfaz. Valor predeterminado Falso",
@ -1315,6 +1324,7 @@
"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "Para acceder a WebUI, por favor contacte con Admins. Los administradores pueden gestionar los estados de los usuarios esde el panel de administración.",
"To attach knowledge base here, add them to the \"Knowledge\" workspace first.": "Para adjuntar la base de conocimientos aquí, primero añadirla a \"Conocimiento\" en el área de trabajo.",
"To learn more about available endpoints, visit our documentation.": "Para aprender más sobre los endpoints disponibles, visite nuestra documentación.",
"To learn more about powerful prompt variables, click here": "",
"To protect your privacy, only ratings, model IDs, tags, and metadata are shared from your feedback—your chat logs remain private and are not included.": "Para proteger tu privacidad, de tus comentarios solo se comparten las calificaciones, IDs de modelo, etiquetas y metadatos; tus chat guardados permanecen privados y no se incluyen.",
"To select actions here, add them to the \"Functions\" workspace first.": "Para seleccionar acciones aquí, primero añadirlas a \"Funciones\" en el área de trabajo.",
"To select filters here, add them to the \"Functions\" workspace first.": "Para seleccionar filtros aquí, primero añadirlos a \"Funciones\" en el área de trabajo.",
@ -1361,6 +1371,7 @@
"Unlock mysteries": "Desbloquear misterios",
"Unpin": "Desfijar",
"Unravel secrets": "Desentrañar secretos",
"Unsupported file type.": "",
"Untagged": "Sin Etiqueta",
"Untitled": "Sin Título",
"Update": "Actualizar",
@ -1396,14 +1407,14 @@
"User Webhooks": "Usuario Webhooks",
"Username": "Nombre de Usuario",
"Users": "Usuarios",
"Using Entire Document": "",
"Using Focused Retrieval": "",
"Using the default arena model with all models. Click the plus button to add custom models.": "Usando el modelo de arena predeterminado con todos los modelos. Pulsar en el botón + para agregar modelos personalizados.",
"Utilize": "Utilizar",
"Valid time units:": "Unidades de tiempo válidas:",
"Valves": "Válvulas",
"Valves updated": "Válvulas actualizadas",
"Valves updated successfully": "Válvulas actualizados correctamente",
"variable": "variable",
"variable to have them replaced with clipboard content.": "hace que la variable sea reemplazada con el contenido del portapapeles.",
"Verify Connection": "Verificar Conexión",
"Verify SSL Certificate": "Verificar Certificado SSL",
"Version": "Versión",

View File

@ -290,6 +290,7 @@
"Create Account": "Loo konto",
"Create Admin Account": "Loo administraatori konto",
"Create Channel": "Loo kanal",
"Create Folder": "",
"Create Group": "Loo grupp",
"Create Knowledge": "Loo teadmised",
"Create new key": "Loo uus võti",
@ -413,7 +414,7 @@
"e.g. pdf, docx, txt": "",
"e.g. Tools for performing various operations": "nt tööriistad mitmesuguste operatsioonide teostamiseks",
"e.g., 3, 4, 5 (leave blank for default)": "",
"e.g., audio/wav,audio/mpeg,video/* (leave blank for defaults, * for all)": "",
"e.g., audio/wav,audio/mpeg,video/* (leave blank for defaults)": "",
"e.g., en-US,ja-JP (leave blank for auto-detect)": "",
"e.g., westus (leave blank for eastus)": "",
"e.g.) en,fr,de": "",
@ -601,6 +602,7 @@
"External Web Loader URL": "",
"External Web Search API Key": "",
"External Web Search URL": "",
"Fade Effect for Streaming Text": "",
"Failed to add file.": "Faili lisamine ebaõnnestus.",
"Failed to connect to {{URL}} OpenAPI tool server": "",
"Failed to copy link": "",
@ -610,6 +612,7 @@
"Failed to extract content from the file.": "",
"Failed to fetch models": "Mudelite toomine ebaõnnestus",
"Failed to generate title": "",
"Failed to load chat preview": "",
"Failed to load file content.": "",
"Failed to read clipboard contents": "Lõikelaua sisu lugemine ebaõnnestus",
"Failed to save connections": "",
@ -755,6 +758,7 @@
"Input commands": "Sisendkäsud",
"Input Variables": "",
"Insert": "",
"Insert Follow-Up Prompt to Input": "",
"Insert Prompt as Rich Text": "",
"Install from Github URL": "Installige Github URL-ilt",
"Instant Auto-Send After Voice Transcription": "Kohene automaatne saatmine pärast hääle transkriptsiooni",
@ -778,6 +782,7 @@
"JWT Expiration": "JWT aegumine",
"JWT Token": "JWT token",
"Kagi Search API Key": "Kagi Search API võti",
"Keep Follow-Up Prompts in Chat": "",
"Keep in Sidebar": "",
"Key": "Võti",
"Keyboard shortcuts": "Klaviatuuri otseteed",
@ -812,6 +817,7 @@
"Leave empty to use the default prompt, or enter a custom prompt": "Jäta tühjaks, et kasutada vaikimisi vihjet, või sisesta kohandatud vihje",
"Leave model field empty to use the default model.": "Jäta mudeli väli tühjaks, et kasutada vaikimisi mudelit.",
"License": "Litsents",
"Lift List": "",
"Light": "Hele",
"Listening...": "Kuulamine...",
"Llama.cpp": "Llama.cpp",
@ -902,6 +908,7 @@
"New Tool": "",
"new-channel": "uus-kanal",
"Next message": "",
"No chats found": "",
"No chats found for this user.": "",
"No chats found.": "",
"No content": "",
@ -1140,6 +1147,7 @@
"See what's new": "Vaata, mis on uut",
"Seed": "Seeme",
"Select a base model": "Valige baas mudel",
"Select a conversation to preview": "",
"Select a engine": "Valige mootor",
"Select a function": "Valige funktsioon",
"Select a group": "Valige grupp",
@ -1210,6 +1218,7 @@
"Sign up to {{WEBUI_NAME}}": "Registreeru {{WEBUI_NAME}}",
"Significantly improves accuracy by using an LLM to enhance tables, forms, inline math, and layout detection. Will increase latency. Defaults to True.": "",
"Signing in to {{WEBUI_NAME}}": "Sisselogimine {{WEBUI_NAME}}",
"Sink List": "",
"sk-1234": "sk-1234",
"Skip Cache": "",
"Skip the cache and re-run the inference. Defaults to False.": "",
@ -1315,6 +1324,7 @@
"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "WebUI-le juurdepääsuks võtke ühendust administraatoriga. Administraatorid saavad hallata kasutajate staatuseid administraatori paneelist.",
"To attach knowledge base here, add them to the \"Knowledge\" workspace first.": "Teadmiste baasi siia lisamiseks lisage need esmalt \"Teadmiste\" tööalale.",
"To learn more about available endpoints, visit our documentation.": "Saadaolevate lõpp-punktide kohta rohkem teada saamiseks külastage meie dokumentatsiooni.",
"To learn more about powerful prompt variables, click here": "",
"To protect your privacy, only ratings, model IDs, tags, and metadata are shared from your feedback—your chat logs remain private and are not included.": "Teie privaatsuse kaitsmiseks jagatakse teie tagasisidest ainult hinnanguid, mudeli ID-sid, silte ja metaandmeid - teie vestluslogi jääb privaatseks ja neid ei kaasata.",
"To select actions here, add them to the \"Functions\" workspace first.": "Toimingute siit valimiseks lisage need esmalt \"Funktsioonide\" tööalale.",
"To select filters here, add them to the \"Functions\" workspace first.": "Filtrite siit valimiseks lisage need esmalt \"Funktsioonide\" tööalale.",
@ -1361,6 +1371,7 @@
"Unlock mysteries": "Ava mõistatused",
"Unpin": "Võta lahti",
"Unravel secrets": "Ava saladused",
"Unsupported file type.": "",
"Untagged": "Sildistamata",
"Untitled": "",
"Update": "Uuenda",
@ -1396,14 +1407,14 @@
"User Webhooks": "",
"Username": "Kasutajanimi",
"Users": "Kasutajad",
"Using Entire Document": "",
"Using Focused Retrieval": "",
"Using the default arena model with all models. Click the plus button to add custom models.": "Kasutatakse vaikimisi areena mudelit kõigi mudelitega. Kohandatud mudelite lisamiseks klõpsake plussmärgiga nuppu.",
"Utilize": "Kasuta",
"Valid time units:": "Kehtivad ajaühikud:",
"Valves": "Klapid",
"Valves updated": "Klapid uuendatud",
"Valves updated successfully": "Klapid edukalt uuendatud",
"variable": "muutuja",
"variable to have them replaced with clipboard content.": "muutuja, et need asendataks lõikelaua sisuga.",
"Verify Connection": "",
"Verify SSL Certificate": "",
"Version": "Versioon",

View File

@ -290,6 +290,7 @@
"Create Account": "Sortu Kontua",
"Create Admin Account": "Sortu Administratzaile Kontua",
"Create Channel": "",
"Create Folder": "",
"Create Group": "Sortu Taldea",
"Create Knowledge": "Sortu Ezagutza",
"Create new key": "Sortu gako berria",
@ -413,7 +414,7 @@
"e.g. pdf, docx, txt": "",
"e.g. Tools for performing various operations": "adib. Hainbat eragiketa egiteko tresnak",
"e.g., 3, 4, 5 (leave blank for default)": "",
"e.g., audio/wav,audio/mpeg,video/* (leave blank for defaults, * for all)": "",
"e.g., audio/wav,audio/mpeg,video/* (leave blank for defaults)": "",
"e.g., en-US,ja-JP (leave blank for auto-detect)": "",
"e.g., westus (leave blank for eastus)": "",
"e.g.) en,fr,de": "",
@ -601,6 +602,7 @@
"External Web Loader URL": "",
"External Web Search API Key": "",
"External Web Search URL": "",
"Fade Effect for Streaming Text": "",
"Failed to add file.": "Huts egin du fitxategia gehitzean.",
"Failed to connect to {{URL}} OpenAPI tool server": "",
"Failed to copy link": "",
@ -610,6 +612,7 @@
"Failed to extract content from the file.": "",
"Failed to fetch models": "",
"Failed to generate title": "",
"Failed to load chat preview": "",
"Failed to load file content.": "",
"Failed to read clipboard contents": "Huts egin du arbelaren edukia irakurtzean",
"Failed to save connections": "",
@ -755,6 +758,7 @@
"Input commands": "Sartu komandoak",
"Input Variables": "",
"Insert": "",
"Insert Follow-Up Prompt to Input": "",
"Insert Prompt as Rich Text": "",
"Install from Github URL": "Instalatu Github URLtik",
"Instant Auto-Send After Voice Transcription": "Bidalketa Automatiko Berehalakoa Ahots Transkripzioaren Ondoren",
@ -778,6 +782,7 @@
"JWT Expiration": "JWT Iraungitzea",
"JWT Token": "JWT Tokena",
"Kagi Search API Key": "",
"Keep Follow-Up Prompts in Chat": "",
"Keep in Sidebar": "",
"Key": "Gakoa",
"Keyboard shortcuts": "Teklatuko lasterbideak",
@ -812,6 +817,7 @@
"Leave empty to use the default prompt, or enter a custom prompt": "Utzi hutsik prompt lehenetsia erabiltzeko, edo sartu prompt pertsonalizatu bat",
"Leave model field empty to use the default model.": "",
"License": "",
"Lift List": "",
"Light": "Argia",
"Listening...": "Entzuten...",
"Llama.cpp": "",
@ -902,6 +908,7 @@
"New Tool": "",
"new-channel": "",
"Next message": "",
"No chats found": "",
"No chats found for this user.": "",
"No chats found.": "",
"No content": "",
@ -1140,6 +1147,7 @@
"See what's new": "Ikusi berritasunak",
"Seed": "Hazia",
"Select a base model": "Hautatu oinarrizko modeloa",
"Select a conversation to preview": "",
"Select a engine": "Hautatu motor bat",
"Select a function": "Hautatu funtzio bat",
"Select a group": "Hautatu talde bat",
@ -1210,6 +1218,7 @@
"Sign up to {{WEBUI_NAME}}": "Erregistratu {{WEBUI_NAME}}-n",
"Significantly improves accuracy by using an LLM to enhance tables, forms, inline math, and layout detection. Will increase latency. Defaults to True.": "",
"Signing in to {{WEBUI_NAME}}": "{{WEBUI_NAME}}-n saioa hasten",
"Sink List": "",
"sk-1234": "",
"Skip Cache": "",
"Skip the cache and re-run the inference. Defaults to False.": "",
@ -1315,6 +1324,7 @@
"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "WebUI-a atzitzeko, mesedez jarri harremanetan administratzailearekin. Administratzaileek erabiltzaileen egoerak kudeatu ditzakete Admin Paneletik.",
"To attach knowledge base here, add them to the \"Knowledge\" workspace first.": "Ezagutza-basea hemen eransteko, gehitu ezazu lehenik \"Ezagutza\" lan-eremura.",
"To learn more about available endpoints, visit our documentation.": "",
"To learn more about powerful prompt variables, click here": "",
"To protect your privacy, only ratings, model IDs, tags, and metadata are shared from your feedback—your chat logs remain private and are not included.": "Zure pribatutasuna babesteko, zure feedbacketik bakarrik partekatzen dira balorazioak, modelo IDak, etiketak eta metadatuak—zure txat erregistroak pribatuak dira eta ez dira sartzen.",
"To select actions here, add them to the \"Functions\" workspace first.": "Ekintzak hemen hautatzeko, gehitu itzazu lehenik \"Funtzioak\" lan-eremura.",
"To select filters here, add them to the \"Functions\" workspace first.": "Iragazkiak hemen hautatzeko, gehitu itzazu lehenik \"Funtzioak\" lan-eremura.",
@ -1361,6 +1371,7 @@
"Unlock mysteries": "Askatu misterioak",
"Unpin": "Kendu aingura",
"Unravel secrets": "Askatu sekretuak",
"Unsupported file type.": "",
"Untagged": "Etiketatu gabea",
"Untitled": "",
"Update": "Eguneratu",
@ -1396,14 +1407,14 @@
"User Webhooks": "",
"Username": "Erabiltzaile-izena",
"Users": "Erabiltzaileak",
"Using Entire Document": "",
"Using Focused Retrieval": "",
"Using the default arena model with all models. Click the plus button to add custom models.": "Arena modelo lehenetsia erabiltzen modelo guztiekin. Egin klik plus botoian modelo pertsonalizatuak gehitzeko.",
"Utilize": "Erabili",
"Valid time units:": "Denbora unitate baliozkoak:",
"Valves": "Balbulak",
"Valves updated": "Balbulak eguneratuta",
"Valves updated successfully": "Balbulak ongi eguneratu dira",
"variable": "aldagaia",
"variable to have them replaced with clipboard content.": "aldagaia arbeleko edukiarekin ordezkatzeko.",
"Verify Connection": "",
"Verify SSL Certificate": "",
"Version": "Bertsioa",

View File

@ -290,6 +290,7 @@
"Create Account": "ساخت حساب کاربری",
"Create Admin Account": "ایجاد حساب مدیر",
"Create Channel": "ایجاد کانال",
"Create Folder": "",
"Create Group": "ایجاد گروه",
"Create Knowledge": "ایجاد دانش",
"Create new key": "ساخت کلید جدید",
@ -413,7 +414,7 @@
"e.g. pdf, docx, txt": "",
"e.g. Tools for performing various operations": "مثلا ابزارهایی برای انجام عملیات مختلف",
"e.g., 3, 4, 5 (leave blank for default)": "",
"e.g., audio/wav,audio/mpeg,video/* (leave blank for defaults, * for all)": "",
"e.g., audio/wav,audio/mpeg,video/* (leave blank for defaults)": "",
"e.g., en-US,ja-JP (leave blank for auto-detect)": "مثلا en-US,ja-JP (برای تشخیص خودکار خالی بگذارید)",
"e.g., westus (leave blank for eastus)": "",
"e.g.) en,fr,de": "",
@ -601,6 +602,7 @@
"External Web Loader URL": "",
"External Web Search API Key": "",
"External Web Search URL": "",
"Fade Effect for Streaming Text": "",
"Failed to add file.": "خطا در افزودن پرونده",
"Failed to connect to {{URL}} OpenAPI tool server": "خطا در اتصال به سرور ابزار OpenAPI {{URL}}",
"Failed to copy link": "",
@ -610,6 +612,7 @@
"Failed to extract content from the file.": "",
"Failed to fetch models": "خطا در دریافت مدل\u200cها",
"Failed to generate title": "",
"Failed to load chat preview": "",
"Failed to load file content.": "",
"Failed to read clipboard contents": "خواندن محتوای کلیپ بورد ناموفق بود",
"Failed to save connections": "خطا در ذخیره\u200cسازی اتصالات",
@ -755,6 +758,7 @@
"Input commands": "ورودی دستورات",
"Input Variables": "",
"Insert": "",
"Insert Follow-Up Prompt to Input": "",
"Insert Prompt as Rich Text": "",
"Install from Github URL": "نصب از ادرس Github",
"Instant Auto-Send After Voice Transcription": "ارسال خودکار فوری پس از رونویسی صوتی",
@ -778,6 +782,7 @@
"JWT Expiration": "JWT انقضای",
"JWT Token": "JWT توکن",
"Kagi Search API Key": "کلید API جستجوی کاگی",
"Keep Follow-Up Prompts in Chat": "",
"Keep in Sidebar": "",
"Key": "کلید",
"Keyboard shortcuts": "میانبرهای صفحه کلید",
@ -812,6 +817,7 @@
"Leave empty to use the default prompt, or enter a custom prompt": "برای استفاده از پرامپت پیش\u200cفرض خالی بگذارید، یا یک پرامپت سفارشی وارد کنید",
"Leave model field empty to use the default model.": "برای استفاده از مدل پیش\u200cفرض، فیلد مدل را خالی بگذارید.",
"License": "مجوز",
"Lift List": "",
"Light": "روشن",
"Listening...": "در حال گوش دادن...",
"Llama.cpp": "Llama.cpp",
@ -902,6 +908,7 @@
"New Tool": "",
"new-channel": "کانال-جدید",
"Next message": "",
"No chats found": "",
"No chats found for this user.": "",
"No chats found.": "",
"No content": "",
@ -1140,6 +1147,7 @@
"See what's new": "ببینید موارد جدید چه بوده",
"Seed": "هسته",
"Select a base model": "انتخاب یک مدل پایه",
"Select a conversation to preview": "",
"Select a engine": "انتخاب یک موتور",
"Select a function": "انتخاب یک تابع",
"Select a group": "انتخاب یک گروه",
@ -1210,6 +1218,7 @@
"Sign up to {{WEBUI_NAME}}": "ثبت نام در {{WEBUI_NAME}}",
"Significantly improves accuracy by using an LLM to enhance tables, forms, inline math, and layout detection. Will increase latency. Defaults to True.": "",
"Signing in to {{WEBUI_NAME}}": "در حال ورود به {{WEBUI_NAME}}",
"Sink List": "",
"sk-1234": "sk-1234",
"Skip Cache": "",
"Skip the cache and re-run the inference. Defaults to False.": "",
@ -1315,6 +1324,7 @@
"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "برای دسترسی به رابط کاربری وب، لطفاً با مدیر تماس بگیرید. مدیران می\u200cتوانند وضعیت کاربران را از پنل مدیریت مدیریت کنند.",
"To attach knowledge base here, add them to the \"Knowledge\" workspace first.": "برای اتصال پایگاه دانش در اینجا، ابتدا آنها را به فضای کاری \"دانش\" اضافه کنید.",
"To learn more about available endpoints, visit our documentation.": "برای کسب اطلاعات بیشتر در مورد نقاط پایانی موجود، به مستندات ما مراجعه کنید.",
"To learn more about powerful prompt variables, click here": "",
"To protect your privacy, only ratings, model IDs, tags, and metadata are shared from your feedback—your chat logs remain private and are not included.": "برای حفظ حریم خصوصی شما، فقط امتیازات، شناسه\u200cهای مدل، برچسب\u200cها و متادیتا از بازخورد شما به اشتراک گذاشته می\u200cشود - گفتگوهای شما خصوصی باقی می\u200cماند و شامل نمی\u200cشود.",
"To select actions here, add them to the \"Functions\" workspace first.": "برای انتخاب عملیات در اینجا، ابتدا آنها را به فضای کاری \"توابع\" اضافه کنید.",
"To select filters here, add them to the \"Functions\" workspace first.": "برای انتخاب فیلترها در اینجا، ابتدا آنها را به فضای کاری \"توابع\" اضافه کنید.",
@ -1361,6 +1371,7 @@
"Unlock mysteries": "رمزگشایی از اسرار",
"Unpin": "برداشتن پین",
"Unravel secrets": "کشف رازها",
"Unsupported file type.": "",
"Untagged": "بدون برچسب",
"Untitled": "",
"Update": "به\u200cروزرسانی",
@ -1396,14 +1407,14 @@
"User Webhooks": "وب\u200cهوک\u200cهای کاربر",
"Username": "نام کاربری",
"Users": "کاربران",
"Using Entire Document": "",
"Using Focused Retrieval": "",
"Using the default arena model with all models. Click the plus button to add custom models.": "در حال استفاده از مدل آرنا با همهٔ مدل\u200cهای دیگر به طور پیش\u200cفرض. برای افزودن مدل\u200cهای سفارشی، روی دکمه به\u200cعلاوه کلیک کنید.",
"Utilize": "استفاده کنید",
"Valid time units:": "واحدهای زمانی معتبر:",
"Valves": "شیرها",
"Valves updated": "شیرها به\u200cروزرسانی شدند",
"Valves updated successfully": "شیرها با موفقیت به\u200cروزرسانی شدند",
"variable": "متغیر",
"variable to have them replaced with clipboard content.": "متغیر برای جایگزینی آنها با محتوای بریده\u200cدان.",
"Verify Connection": "تأیید اتصال",
"Verify SSL Certificate": "تأیید گواهی SSL",
"Version": "نسخه",

View File

@ -290,6 +290,7 @@
"Create Account": "Luo tili",
"Create Admin Account": "Luo ylläpitäjätili",
"Create Channel": "Luo kanava",
"Create Folder": "",
"Create Group": "Luo ryhmä",
"Create Knowledge": "Luo tietoa",
"Create new key": "Luo uusi avain",
@ -413,7 +414,7 @@
"e.g. pdf, docx, txt": "esim. pdf, docx, txt",
"e.g. Tools for performing various operations": "esim. työkaluja erilaisten toimenpiteiden suorittamiseen",
"e.g., 3, 4, 5 (leave blank for default)": "esim. 3, 4, 5 (jätä tyhjäksi, jos haluat oletusarvon)",
"e.g., audio/wav,audio/mpeg,video/* (leave blank for defaults, * for all)": "",
"e.g., audio/wav,audio/mpeg,video/* (leave blank for defaults)": "",
"e.g., en-US,ja-JP (leave blank for auto-detect)": "esim. en-US,ja-JP (Tyhjäksi jättämällä, automaattinen tunnistus)",
"e.g., westus (leave blank for eastus)": "esim. westus (jätä tyhjäksi eastusta varten)",
"e.g.) en,fr,de": "esim.) en,fr,de",
@ -601,6 +602,7 @@
"External Web Loader URL": "Ulkoinen Web Loader verkko-osoite",
"External Web Search API Key": "Ulkoinen Web Search API-avain",
"External Web Search URL": "Ulkoinen Web Search verkko-osoite",
"Fade Effect for Streaming Text": "",
"Failed to add file.": "Tiedoston lisääminen epäonnistui.",
"Failed to connect to {{URL}} OpenAPI tool server": "Yhdistäminen {{URL}} OpenAPI työkalu palvelimeen epäonnistui",
"Failed to copy link": "Linkin kopioinmti epäonnistui",
@ -610,6 +612,7 @@
"Failed to extract content from the file.": "",
"Failed to fetch models": "Mallien hakeminen epäonnistui",
"Failed to generate title": "",
"Failed to load chat preview": "",
"Failed to load file content.": "Tiedoston sisällön lataaminen epäonnistui.",
"Failed to read clipboard contents": "Leikepöydän sisällön lukeminen epäonnistui",
"Failed to save connections": "Yhteyksien tallentaminen epäonnistui",
@ -755,6 +758,7 @@
"Input commands": "Syötekäskyt",
"Input Variables": "",
"Insert": "",
"Insert Follow-Up Prompt to Input": "",
"Insert Prompt as Rich Text": "",
"Install from Github URL": "Asenna Github-URL:stä",
"Instant Auto-Send After Voice Transcription": "Heti automaattinen lähetys äänitunnistuksen jälkeen",
@ -778,6 +782,7 @@
"JWT Expiration": "JWT-vanheneminen",
"JWT Token": "JWT-token",
"Kagi Search API Key": "Kagi Search API -avain",
"Keep Follow-Up Prompts in Chat": "",
"Keep in Sidebar": "Pidä sivupalkissa",
"Key": "Avain",
"Keyboard shortcuts": "Pikanäppäimet",
@ -812,6 +817,7 @@
"Leave empty to use the default prompt, or enter a custom prompt": "Jätä tyhjäksi käyttääksesi oletuskehotetta tai kirjoita mukautettu kehote",
"Leave model field empty to use the default model.": "Jätä malli kenttä tyhjäksi käyttääksesi oletus mallia.",
"License": "Lisenssi",
"Lift List": "",
"Light": "Vaalea",
"Listening...": "Kuuntelee...",
"Llama.cpp": "Llama.cpp",
@ -902,6 +908,7 @@
"New Tool": "Uusi työkalu",
"new-channel": "uusi-kanava",
"Next message": "Seuraava viesti",
"No chats found": "",
"No chats found for this user.": "Käyttäjän keskusteluja ei löytynyt.",
"No chats found.": "Keskusteluja ei löytynyt",
"No content": "Ei sisältöä",
@ -1140,6 +1147,7 @@
"See what's new": "Katso, mitä uutta",
"Seed": "Siemenluku",
"Select a base model": "Valitse perusmalli",
"Select a conversation to preview": "",
"Select a engine": "Valitse moottori",
"Select a function": "Valitse toiminto",
"Select a group": "Valitse ryhmä",
@ -1210,6 +1218,7 @@
"Sign up to {{WEBUI_NAME}}": "Rekisteröidy palveluun {{WEBUI_NAME}}",
"Significantly improves accuracy by using an LLM to enhance tables, forms, inline math, and layout detection. Will increase latency. Defaults to True.": "Parantaa merkittävästi tarkkuutta käyttämällä LLM:ää taulukoiden, lomakkeiden, matematiikan ja asettelun havaitsemisen parantamiseen. Lisää viivettä. Oletusarvo on käytössä.",
"Signing in to {{WEBUI_NAME}}": "Kirjaudutaan sisään palveluun {{WEBUI_NAME}}",
"Sink List": "",
"sk-1234": "",
"Skip Cache": "Ohita välimuisti",
"Skip the cache and re-run the inference. Defaults to False.": "Ohita välimuisti ja suorita päätelmä uudelleen. Oletusarvo ei käytössä.",
@ -1315,6 +1324,7 @@
"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "Päästäksesi käyttämään WebUI:ta, ota yhteyttä ylläpitäjään. Ylläpitäjät voivat hallita käyttäjien tiloja Ylläpitopaneelista.",
"To attach knowledge base here, add them to the \"Knowledge\" workspace first.": "Liittääksesi tietokantasi tähän, lisää ne ensin \"Tietämys\"-työtilaan.",
"To learn more about available endpoints, visit our documentation.": "Jos haluat lisätietoja käytettävissä olevista päätepisteistä, tutustu dokumentaatioomme.",
"To learn more about powerful prompt variables, click here": "",
"To protect your privacy, only ratings, model IDs, tags, and metadata are shared from your feedback—your chat logs remain private and are not included.": "Yksityisyydensuojasi vuoksi palautteestasi jaetaan vain arvostelut, mallitunnukset, tagit ja metadata - keskustelulokisi pysyvät yksityisinä eikä niitä sisällytetä.",
"To select actions here, add them to the \"Functions\" workspace first.": "Valitaksesi toimintoja tässä, lisää ne ensin \"Toiminnot\"-työtilaan.",
"To select filters here, add them to the \"Functions\" workspace first.": "Valitaksesi suodattimia tässä, lisää ne ensin \"Toiminnot\"-työtilaan.",
@ -1361,6 +1371,7 @@
"Unlock mysteries": "Selvitä arvoituksia",
"Unpin": "Irrota kiinnitys",
"Unravel secrets": "Avaa salaisuuksia",
"Unsupported file type.": "",
"Untagged": "Ei tageja",
"Untitled": "Nimetön",
"Update": "Päivitä",
@ -1396,14 +1407,14 @@
"User Webhooks": "Käyttäjän Webhook:it",
"Username": "Käyttäjätunnus",
"Users": "Käyttäjät",
"Using Entire Document": "",
"Using Focused Retrieval": "",
"Using the default arena model with all models. Click the plus button to add custom models.": "Käytetään oletusarena-mallia kaikkien mallien kanssa. Napsauta plus-painiketta lisätäksesi mukautettuja malleja.",
"Utilize": "Hyödynnä",
"Valid time units:": "Kelvolliset aikayksiköt:",
"Valves": "Venttiilit",
"Valves updated": "Venttiilit päivitetty",
"Valves updated successfully": "Venttiilit päivitetty onnistuneesti",
"variable": "muuttuja",
"variable to have them replaced with clipboard content.": "muuttuja korvataan leikepöydän sisällöllä.",
"Verify Connection": "Tarkista yhteys",
"Verify SSL Certificate": "Tarkista SSL-varmenne",
"Version": "Versio",

View File

@ -290,6 +290,7 @@
"Create Account": "Créer un compte",
"Create Admin Account": "Créer un compte administrateur",
"Create Channel": "Créer un canal",
"Create Folder": "",
"Create Group": "Créer un groupe",
"Create Knowledge": "Créer une connaissance",
"Create new key": "Créer une nouvelle clé",
@ -413,7 +414,7 @@
"e.g. pdf, docx, txt": "par ex. pdf, docx, txt",
"e.g. Tools for performing various operations": "par ex. Outils pour effectuer diverses opérations",
"e.g., 3, 4, 5 (leave blank for default)": "par ex., 3, 4, 5 (laisser vide pour par défaut)",
"e.g., audio/wav,audio/mpeg,video/* (leave blank for defaults, * for all)": "",
"e.g., audio/wav,audio/mpeg,video/* (leave blank for defaults)": "",
"e.g., en-US,ja-JP (leave blank for auto-detect)": "par ex., en-US, ja-JP (laisser vide pour détection automatique)",
"e.g., westus (leave blank for eastus)": "par ex., westus (laisser vide pour eastus)",
"e.g.) en,fr,de": "par ex., fr, en, de",
@ -601,6 +602,7 @@
"External Web Loader URL": "URL du chargeur Web externe",
"External Web Search API Key": "Clé API de la recherche Web externe",
"External Web Search URL": "URL de la recherche Web externe",
"Fade Effect for Streaming Text": "",
"Failed to add file.": "Échec de l'ajout du fichier.",
"Failed to connect to {{URL}} OpenAPI tool server": "Échec de la connexion au serveur d'outils OpenAPI {{URL}}",
"Failed to copy link": "Échec de la copie du lien",
@ -610,6 +612,7 @@
"Failed to extract content from the file.": "",
"Failed to fetch models": "Échec de la récupération des modèles",
"Failed to generate title": "",
"Failed to load chat preview": "",
"Failed to load file content.": "Échec du chargement du contenu du fichier",
"Failed to read clipboard contents": "Échec de la lecture du contenu du presse-papiers",
"Failed to save connections": "Échec de la sauvegarde des connexions",
@ -755,6 +758,7 @@
"Input commands": "Commandes d'entrée",
"Input Variables": "Variables d'entrée",
"Insert": "Insérer",
"Insert Follow-Up Prompt to Input": "",
"Insert Prompt as Rich Text": "Insérer le prompt en tant que texte enrichi",
"Install from Github URL": "Installer depuis une URL GitHub",
"Instant Auto-Send After Voice Transcription": "Envoi automatique après la transcription",
@ -778,6 +782,7 @@
"JWT Expiration": "Expiration du token JWT",
"JWT Token": "Token JWT",
"Kagi Search API Key": "Clé API Kagi Search",
"Keep Follow-Up Prompts in Chat": "",
"Keep in Sidebar": "Epingler dans la barre latérale",
"Key": "Clé",
"Keyboard shortcuts": "Raccourcis clavier",
@ -812,6 +817,7 @@
"Leave empty to use the default prompt, or enter a custom prompt": "Laissez vide pour utiliser le prompt par défaut, ou entrez un prompt personnalisé",
"Leave model field empty to use the default model.": "Laisser le champ du modèle vide pour utiliser le modèle par défaut.",
"License": "Licence",
"Lift List": "",
"Light": "Clair",
"Listening...": "Écoute en cours...",
"Llama.cpp": "Llama.cpp",
@ -902,6 +908,7 @@
"New Tool": "Nouvel outil",
"new-channel": "nouveau-canal",
"Next message": "Message suivant",
"No chats found": "",
"No chats found for this user.": "Pas de conversation trouvée pour cet utilisateur.",
"No chats found.": "Pas de conversation trouvée.",
"No content": "Pas de contenu",
@ -1140,6 +1147,7 @@
"See what's new": "Découvrez les nouvelles fonctionnalités",
"Seed": "Seed",
"Select a base model": "Sélectionnez un modèle de base",
"Select a conversation to preview": "",
"Select a engine": "Sélectionnez un moteur",
"Select a function": "Sélectionnez une fonction",
"Select a group": "Sélectionner un groupe",
@ -1210,6 +1218,7 @@
"Sign up to {{WEBUI_NAME}}": "Inscrivez-vous à {{WEBUI_NAME}}",
"Significantly improves accuracy by using an LLM to enhance tables, forms, inline math, and layout detection. Will increase latency. Defaults to True.": "Améliore considérablement la précision en utilisant un LLM pour améliorer la détection des tableaux, des formulaires, des mathématiques en ligne et de la mise en page. Augmente la latence. Par défaut à True.",
"Signing in to {{WEBUI_NAME}}": "Connexion à {{WEBUI_NAME}}",
"Sink List": "",
"sk-1234": "sk-1234",
"Skip Cache": "Ne pas utiliser le cache",
"Skip the cache and re-run the inference. Defaults to False.": "Ne pas utiliser le cache et re executer l'inférence. Par defaut à False",
@ -1315,6 +1324,7 @@
"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "Pour accéder à l'interface Web, veuillez contacter l'administrateur. Les administrateurs peuvent gérer les statuts des utilisateurs depuis le panneau d'administration.",
"To attach knowledge base here, add them to the \"Knowledge\" workspace first.": "Pour attacher une base de connaissances ici, ajoutez-les d'abord à l'espace de travail « Connaissances ».",
"To learn more about available endpoints, visit our documentation.": "Pour en savoir plus sur les points de terminaison disponibles, consultez notre documentation.",
"To learn more about powerful prompt variables, click here": "",
"To protect your privacy, only ratings, model IDs, tags, and metadata are shared from your feedback—your chat logs remain private and are not included.": "Pour protéger votre confidentialité, seules les notes, les identifiants de modèle, les tags et les métadonnées de vos commentaires sont partagés. Vos journaux de discussion restent privés et ne sont pas inclus.",
"To select actions here, add them to the \"Functions\" workspace first.": "Pour sélectionner des actions ici, ajoutez-les d'abord à l'espace de travail « Fonctions ».",
"To select filters here, add them to the \"Functions\" workspace first.": "Pour sélectionner des filtres ici, ajoutez-les d'abord à l'espace de travail « Fonctions ». ",
@ -1361,6 +1371,7 @@
"Unlock mysteries": "Déverrouiller les mystères",
"Unpin": "Désépingler",
"Unravel secrets": "Dévoiler les secrets",
"Unsupported file type.": "",
"Untagged": "Pas de tag",
"Untitled": "Sans titre",
"Update": "Mise à jour",
@ -1396,14 +1407,14 @@
"User Webhooks": "Webhooks utilisateur",
"Username": "Nom d'utilisateur",
"Users": "Utilisateurs",
"Using Entire Document": "",
"Using Focused Retrieval": "",
"Using the default arena model with all models. Click the plus button to add custom models.": "Utilisation du modèle d'arène par défaut avec tous les modèles. Cliquez sur le bouton plus pour ajouter des modèles personnalisés.",
"Utilize": "Utilisez",
"Valid time units:": "Unités de temps valides\u00a0:",
"Valves": "Vannes",
"Valves updated": "Vannes mises à jour",
"Valves updated successfully": "Les vannes ont été mises à jour avec succès",
"variable": "variable",
"variable to have them replaced with clipboard content.": "variable pour qu'elles soient remplacées par le contenu du presse-papiers.",
"Verify Connection": "Vérifier la connexion",
"Verify SSL Certificate": "Vérifier le certificat SSL",
"Version": "Version:",

View File

@ -10,10 +10,10 @@
"[Yesterday at] h:mm A": "",
"{{ models }}": "{{ models }}",
"{{COUNT}} Available Tools": "Nombre d'outils disponibles {{COUNT}}",
"{{COUNT}} characters": "",
"{{COUNT}} characters": "{{COUNT}} caractères",
"{{COUNT}} hidden lines": "Nombres de lignes cachées {{COUNT}}",
"{{COUNT}} Replies": "{{COUNT}} réponses",
"{{COUNT}} words": "",
"{{COUNT}} words": "{{COUNT}} mots",
"{{user}}'s Chats": "Conversations de {{user}}",
"{{webUIName}} Backend Required": "Backend {{webUIName}} requis",
"*Prompt node ID(s) are required for image generation": "*Les ID de noeud du prompt sont nécessaires pour la génération d'images",
@ -61,7 +61,7 @@
"Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "Les administrateurs ont accès à tous les outils en permanence ; les utilisateurs doivent se voir attribuer des outils pour chaque modèle dans l'espace de travail.",
"Advanced Parameters": "Réglages avancés",
"Advanced Params": "Réglages avancés",
"AI": "",
"AI": "IA",
"All": "Tout",
"All Documents": "Tous les documents",
"All models deleted successfully": "Tous les modèles ont été supprimés avec succès",
@ -159,11 +159,11 @@
"Bing Search V7 Endpoint": "Point de terminaison Bing Search V7",
"Bing Search V7 Subscription Key": "Clé d'abonnement Bing Search V7",
"Bocha Search API Key": "Clé API Bocha Search",
"Bold": "",
"Bold": "Gras",
"Boosting or penalizing specific tokens for constrained responses. Bias values will be clamped between -100 and 100 (inclusive). (Default: none)": "Renforcer ou pénaliser des éléments spécifiques pour les réponses contraintes. Les valeurs du biais seront comprises entre -100 et 100 (inclus). (Par défaut : aucun)",
"Both Docling OCR Engine and Language(s) must be provided or both left empty.": "Le moteur d'OCR de Docling et la (les) langue(s) doivent être fournis ou laissés vides.",
"Brave Search API Key": "Clé API Brave Search",
"Bullet List": "",
"Bullet List": "Liste à puces",
"By {{name}}": "Par {{name}}",
"Bypass Embedding and Retrieval": "Ignorer l'Embedding et le Retrieval",
"Bypass Web Loader": "Ignorer le chargeur Web",
@ -224,7 +224,7 @@
"Close Configure Connection Modal": "Fermer la fenêtre de configuration de la connexion",
"Close modal": "Fermer la fenêtre",
"Close settings modal": "Fermer la fenêtre des réglages",
"Code Block": "",
"Code Block": "Bloc de code",
"Code execution": "Exécution de code",
"Code Execution": "Exécution de code",
"Code Execution Engine": "Moteur d'execution de code",
@ -279,7 +279,7 @@
"Copy Formatted Text": "Copier le texte en gardant le format",
"Copy last code block": "Copier le dernier bloc de code",
"Copy last response": "Copier la dernière réponse",
"Copy link": "",
"Copy link": "Copier le lien",
"Copy Link": "Copier le lien",
"Copy to clipboard": "Copier dans le presse-papiers",
"Copying to clipboard was successful!": "La copie dans le presse-papiers a réussi !",
@ -290,6 +290,7 @@
"Create Account": "Créer un compte",
"Create Admin Account": "Créer un compte administrateur",
"Create Channel": "Créer un canal",
"Create Folder": "",
"Create Group": "Créer un groupe",
"Create Knowledge": "Créer une connaissance",
"Create new key": "Créer une nouvelle clé",
@ -312,7 +313,7 @@
"Database": "Base de données",
"Datalab Marker API": "API Datalab Marker",
"Datalab Marker API Key required.": "Clé API Datalab Marker requise.",
"DD/MM/YYYY": "",
"DD/MM/YYYY": "JJ/MM/AAAA",
"December": "Décembre",
"Default": "Par défaut",
"Default (Open AI)": "Par défaut (OpenAI)",
@ -413,7 +414,7 @@
"e.g. pdf, docx, txt": "par ex. pdf, docx, txt",
"e.g. Tools for performing various operations": "par ex. Outils pour effectuer diverses opérations",
"e.g., 3, 4, 5 (leave blank for default)": "par ex., 3, 4, 5 (laisser vide pour par défaut)",
"e.g., audio/wav,audio/mpeg,video/* (leave blank for defaults, * for all)": "",
"e.g., audio/wav,audio/mpeg,video/* (leave blank for defaults)": "",
"e.g., en-US,ja-JP (leave blank for auto-detect)": "par ex., en-US, ja-JP (laisser vide pour détection automatique)",
"e.g., westus (leave blank for eastus)": "par ex., westus (laisser vide pour eastus)",
"e.g.) en,fr,de": "par ex., fr, en, de",
@ -422,7 +423,7 @@
"Edit Channel": "Modifier le canal",
"Edit Connection": "Modifier la connexion",
"Edit Default Permissions": "Modifier les autorisations par défaut",
"Edit Folder": "",
"Edit Folder": "Modifier le dossier",
"Edit Memory": "Modifier la mémoire",
"Edit User": "Modifier l'utilisateur",
"Edit User Group": "Modifier le groupe d'utilisateurs",
@ -488,7 +489,7 @@
"Enter External Web Search URL": "Entrez l'URL de la recherche Web externe",
"Enter Firecrawl API Base URL": "Entrez l'URL de base de l'API Firecrawl",
"Enter Firecrawl API Key": "Entrez la clé API de Firecrawl",
"Enter folder name": "",
"Enter folder name": "Entrez le nom du dossier",
"Enter Github Raw URL": "Entrez l'URL brute de GitHub",
"Enter Google PSE API Key": "Entrez la clé API Google PSE",
"Enter Google PSE Engine Id": "Entrez l'identifiant du moteur Google PSE",
@ -601,15 +602,17 @@
"External Web Loader URL": "URL du chargeur Web externe",
"External Web Search API Key": "Clé API de la recherche Web externe",
"External Web Search URL": "URL de la recherche Web externe",
"Fade Effect for Streaming Text": "",
"Failed to add file.": "Échec de l'ajout du fichier.",
"Failed to connect to {{URL}} OpenAPI tool server": "Échec de la connexion au serveur d'outils OpenAPI {{URL}}",
"Failed to copy link": "Échec de la copie du lien",
"Failed to create API Key.": "Échec de la création de la clé API.",
"Failed to delete note": "Échec de la délétion de la note",
"Failed to extract content from the file: {{error}}": "",
"Failed to extract content from the file.": "",
"Failed to extract content from the file: {{error}}": "Échec de l'extraction du contenu du fichier : {{error}}",
"Failed to extract content from the file.": "Échec de l'extraction du contenu du fichier",
"Failed to fetch models": "Échec de la récupération des modèles",
"Failed to generate title": "",
"Failed to generate title": "Échec de la génération du titre",
"Failed to load chat preview": "",
"Failed to load file content.": "Échec du chargement du contenu du fichier",
"Failed to read clipboard contents": "Échec de la lecture du contenu du presse-papiers",
"Failed to save connections": "Échec de la sauvegarde des connexions",
@ -643,10 +646,10 @@
"Fluidly stream large external response chunks": "Streaming fluide de gros chunks de réponses externes",
"Focus chat input": "Mettre le focus sur l'entrée de la conversation",
"Folder deleted successfully": "Dossier supprimé avec succès",
"Folder Name": "",
"Folder Name": "Nom du dossier",
"Folder name cannot be empty.": "Le nom du dossier ne peut pas être vide.",
"Folder name updated successfully": "Le nom du dossier a été mis à jour avec succès",
"Folder updated successfully": "",
"Folder updated successfully": "Le dossier a été mis à jour avec succès",
"Follow up": "Suivi",
"Follow Up Generation": "Suivi de la génération",
"Follow Up Generation Prompt": "Suivi de la génération du protompt",
@ -755,6 +758,7 @@
"Input commands": "Commandes d'entrée",
"Input Variables": "Variables d'entrée",
"Insert": "Insérer",
"Insert Follow-Up Prompt to Input": "",
"Insert Prompt as Rich Text": "Insérer le prompt en tant que texte enrichi",
"Install from Github URL": "Installer depuis une URL GitHub",
"Instant Auto-Send After Voice Transcription": "Envoi automatique après la transcription",
@ -765,7 +769,7 @@
"Invalid JSON file": "Fichier JSON non valide",
"Invalid Tag": "Tag non valide",
"is typing...": "est en train d'écrire...",
"Italic": "",
"Italic": "Italique",
"January": "Janvier",
"Jina API Key": "Clé API Jina",
"join our Discord for help.": "Rejoignez notre Discord pour obtenir de l'aide.",
@ -778,6 +782,7 @@
"JWT Expiration": "Expiration du token JWT",
"JWT Token": "Token JWT",
"Kagi Search API Key": "Clé API Kagi Search",
"Keep Follow-Up Prompts in Chat": "",
"Keep in Sidebar": "Epingler dans la barre latérale",
"Key": "Clé",
"Keyboard shortcuts": "Raccourcis clavier",
@ -812,6 +817,7 @@
"Leave empty to use the default prompt, or enter a custom prompt": "Laissez vide pour utiliser le prompt par défaut, ou entrez un prompt personnalisé",
"Leave model field empty to use the default model.": "Laisser le champ du modèle vide pour utiliser le modèle par défaut.",
"License": "Licence",
"Lift List": "",
"Light": "Clair",
"Listening...": "Écoute en cours...",
"Llama.cpp": "Llama.cpp",
@ -902,6 +908,7 @@
"New Tool": "Nouvel outil",
"new-channel": "nouveau-canal",
"Next message": "Message suivant",
"No chats found": "",
"No chats found for this user.": "Pas de conversation trouvée pour cet utilisateur.",
"No chats found.": "Pas de conversation trouvée.",
"No content": "Pas de contenu",
@ -974,7 +981,7 @@
"openapi.json URL or Path": "URL ou chemin openapi.json",
"Options for running a local vision-language model in the picture description. The parameters refer to a model hosted on Hugging Face. This parameter is mutually exclusive with picture_description_api.": "Options pour exécuter un modèle de vision local dans la description d'image. Les réglages font référence à un modèle hébergé sur Hugging Face. Ce réglage est mutuellement exclusif avec picture_description_api.",
"or": "ou",
"Ordered List": "",
"Ordered List": "Liste ordonnéee",
"Organize your users": "Organisez vos utilisateurs",
"Other": "Autre",
"OUTPUT": "SORTIE",
@ -1123,7 +1130,7 @@
"Search Functions": "Rechercher des fonctions",
"Search Knowledge": "Rechercher des connaissances",
"Search Models": "Rechercher des modèles",
"Search Notes": "",
"Search Notes": "Rechercher des notes",
"Search options": "Options de recherche",
"Search Prompts": "Rechercher des prompts",
"Search Result Count": "Nombre de résultats de recherche",
@ -1140,6 +1147,7 @@
"See what's new": "Découvrez les nouvelles fonctionnalités",
"Seed": "Seed",
"Select a base model": "Sélectionnez un modèle de base",
"Select a conversation to preview": "",
"Select a engine": "Sélectionnez un moteur",
"Select a function": "Sélectionnez une fonction",
"Select a group": "Sélectionner un groupe",
@ -1210,6 +1218,7 @@
"Sign up to {{WEBUI_NAME}}": "Inscrivez-vous à {{WEBUI_NAME}}",
"Significantly improves accuracy by using an LLM to enhance tables, forms, inline math, and layout detection. Will increase latency. Defaults to True.": "Améliore considérablement la précision en utilisant un LLM pour améliorer la détection des tableaux, des formulaires, des mathématiques en ligne et de la mise en page. Augmente la latence. Par défaut à True.",
"Signing in to {{WEBUI_NAME}}": "Connexion à {{WEBUI_NAME}}",
"Sink List": "",
"sk-1234": "sk-1234",
"Skip Cache": "Ne pas utiliser le cache",
"Skip the cache and re-run the inference. Defaults to False.": "Ne pas utiliser le cache et re executer l'inférence. Par defaut à False",
@ -1247,7 +1256,7 @@
"Tail free sampling is used to reduce the impact of less probable tokens from the output. A higher value (e.g., 2.0) will reduce the impact more, while a value of 1.0 disables this setting.": "Le sampling sans queue est utilisé pour réduire l'impact des tokens moins probables dans la sortie. Une valeur plus élevée (par exemple, 2.0) réduira davantage l'impact, tandis qu'une valeur de 1.0 désactive ce réglage.",
"Talk to model": "Parler au modèle",
"Tap to interrupt": "Appuyez pour interrompre",
"Task List": "",
"Task List": "Liste pour les tâches",
"Task Model": "Modèle pour les tâches",
"Tasks": "Tâches",
"Tavily API Key": "Clé API Tavily",
@ -1315,6 +1324,7 @@
"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "Pour accéder à l'interface Web, veuillez contacter l'administrateur. Les administrateurs peuvent gérer les statuts des utilisateurs depuis le panneau d'administration.",
"To attach knowledge base here, add them to the \"Knowledge\" workspace first.": "Pour attacher une base de connaissances ici, ajoutez-les d'abord à l'espace de travail « Connaissances ».",
"To learn more about available endpoints, visit our documentation.": "Pour en savoir plus sur les points de terminaison disponibles, consultez notre documentation.",
"To learn more about powerful prompt variables, click here": "",
"To protect your privacy, only ratings, model IDs, tags, and metadata are shared from your feedback—your chat logs remain private and are not included.": "Pour protéger votre confidentialité, seules les notes, les identifiants de modèle, les tags et les métadonnées de vos commentaires sont partagés. Vos journaux de discussion restent privés et ne sont pas inclus.",
"To select actions here, add them to the \"Functions\" workspace first.": "Pour sélectionner des actions ici, ajoutez-les d'abord à l'espace de travail « Fonctions ».",
"To select filters here, add them to the \"Functions\" workspace first.": "Pour sélectionner des filtres ici, ajoutez-les d'abord à l'espace de travail « Fonctions ». ",
@ -1356,11 +1366,12 @@
"Unarchive All": "Désarchiver tout",
"Unarchive All Archived Chats": "Désarchiver toutes les conversations archivées",
"Unarchive Chat": "Désarchiver la conversation",
"Underline": "",
"Underline": "Souligner",
"Unloads {{FROM_NOW}}": "Décharge {{FROM_NOW}}",
"Unlock mysteries": "Déverrouiller les mystères",
"Unpin": "Désépingler",
"Unravel secrets": "Dévoiler les secrets",
"Unsupported file type.": "",
"Untagged": "Pas de tag",
"Untitled": "Sans titre",
"Update": "Mise à jour",
@ -1396,14 +1407,14 @@
"User Webhooks": "Webhooks utilisateur",
"Username": "Nom d'utilisateur",
"Users": "Utilisateurs",
"Using Entire Document": "",
"Using Focused Retrieval": "",
"Using the default arena model with all models. Click the plus button to add custom models.": "Utilisation du modèle d'arène par défaut avec tous les modèles. Cliquez sur le bouton plus pour ajouter des modèles personnalisés.",
"Utilize": "Utilisez",
"Valid time units:": "Unités de temps valides\u00a0:",
"Valves": "Vannes",
"Valves updated": "Vannes mises à jour",
"Valves updated successfully": "Les vannes ont été mises à jour avec succès",
"variable": "variable",
"variable to have them replaced with clipboard content.": "variable pour qu'elles soient remplacées par le contenu du presse-papiers.",
"Verify Connection": "Vérifier la connexion",
"Verify SSL Certificate": "Vérifier le certificat SSL",
"Version": "Version:",

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