@@ -18,6 +18,7 @@ The most impactful way to contribute to Open WebUI is through well-written bug r
|
|||||||
|
|
||||||
**Before submitting, make sure you've checked the following:**
|
**Before submitting, make sure you've checked the following:**
|
||||||
|
|
||||||
|
- [ ] **Linked Issue/Discussion:** This PR references an existing [Issue](https://github.com/open-webui/open-webui/issues) or [Discussion](https://github.com/open-webui/open-webui/discussions) — `Closes #___` / `Relates to #___`. If one does not exist, create one first. PRs without a linked issue or discussion may be closed without review.
|
||||||
- [ ] **Target branch:** Verify that the pull request targets the `dev` branch. **PRs targeting `main` will be immediately closed.**
|
- [ ] **Target branch:** Verify that the pull request targets the `dev` branch. **PRs targeting `main` will be immediately closed.**
|
||||||
- [ ] **Description:** Provide a concise description of the changes made in this pull request down below.
|
- [ ] **Description:** Provide a concise description of the changes made in this pull request down below.
|
||||||
- [ ] **Changelog:** Ensure a changelog entry following the format of [Keep a Changelog](https://keepachangelog.com/) is added at the bottom of the PR description.
|
- [ ] **Changelog:** Ensure a changelog entry following the format of [Keep a Changelog](https://keepachangelog.com/) is added at the bottom of the PR description.
|
||||||
|
|||||||
@@ -5,6 +5,42 @@ 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/),
|
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).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [0.9.5] - 2026-05-09
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- 🛡️ **Redirect-based SSRF protection.** All outbound HTTP requests now block 3xx redirects by default via a new `AIOHTTP_CLIENT_ALLOW_REDIRECTS` environment variable, preventing redirect-based SSRF where a public URL silently redirects to internal addresses (RFC 1918, loopback, cloud-metadata endpoints). Affected call sites include web fetch, image loading, OAuth discovery, tool server execution, and code interpreter login. [#24491](https://github.com/open-webui/open-webui/pull/24491)
|
||||||
|
- 🛡️ **Iframe content security policy.** Administrators can now configure a Content-Security-Policy for all srcdoc iframes (Artifacts, tool embeds, file previews, citation modals) via the `IFRAME_CSP` environment variable, restricting what LLM-generated or user-uploaded HTML can load and execute inside previews. [Commit](https://github.com/open-webui/open-webui/commit/3bba1c227059a44c7eeefa97b8c69a63bf4f3454)
|
||||||
|
- 🎛️ **Granular markdown rendering controls.** Users can now independently disable Markdown rendering for user messages and assistant responses from Interface settings, preventing unintended formatting when pasting text that contains Markdown-sensitive characters. [Commit](https://github.com/open-webui/open-webui/commit/4a1064cefd6f48a8b3b02cd31f77838c8802b635)
|
||||||
|
- 🔧 **Terminal proxy response headers.** Administrators can now inject custom response headers into terminal proxy responses via the `TERMINAL_PROXY_HEADERS` environment variable (JSON object), enabling deployment-specific security headers like sandbox policies for proxied content. [Commit](https://github.com/open-webui/open-webui/commit/8d3133fe2835122bffaa4f2ce584730bc9c78981)
|
||||||
|
- 🔌 **Channel streaming and tool support.** Mentioning a model in a Channel now streams responses in real time and supports the full chat completion pipeline, including native and default function calling, built-in tools (web search, image generation), user tools, MCP tools, filters, and RAG knowledge injection — the same capabilities available in standard chats.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- 📝 **Notes create and open reliability.** Creating new notes and opening existing notes no longer fails with a TypeError caused by `is_pinned` being passed to the SQLAlchemy model on create, and passed twice to `NoteResponse` on read. [#24484](https://github.com/open-webui/open-webui/issues/24484), [#24486](https://github.com/open-webui/open-webui/pull/24486)
|
||||||
|
- 🔐 **Skill public sharing permission enforcement.** Creating or updating skills now filters access grants through the `sharing.public_skills` permission, preventing non-admin users from making skills publicly accessible without the required permission. [#24494](https://github.com/open-webui/open-webui/pull/24494)
|
||||||
|
- 🔐 **Calendar public sharing permission enforcement.** Creating or updating calendars now filters access grants through a new `sharing.public_calendars` permission, preventing users from making calendars publicly readable or writable without explicit admin-granted sharing permission. [#24493](https://github.com/open-webui/open-webui/pull/24493)
|
||||||
|
- 🔐 **Feedback user attribution spoofing.** Submitting evaluation feedback can no longer forge the `user_id` field through mass-assignment, preventing authenticated users from attributing ratings to other users and corrupting Elo leaderboard rankings and admin feedback exports. [#24508](https://github.com/open-webui/open-webui/pull/24508)
|
||||||
|
- 🛡️ **Image URL redirect-based SSRF.** Chat messages containing image URLs no longer follow 3xx redirects to internal addresses during base64 conversion, closing the most reachable redirect-based SSRF variant that required no special permissions or feature flags. [#24524](https://github.com/open-webui/open-webui/pull/24524)
|
||||||
|
- 🛡️ **Collection write access on file processing.** The `process_file` and `process_files_batch` retrieval endpoints now enforce collection write-access checks before embedding content, preventing authenticated users from injecting file content into another user's knowledge-base collection. [#24524](https://github.com/open-webui/open-webui/pull/24524)
|
||||||
|
- 🔐 **Tool source code update authorization.** Updating a tool's Python source code now requires `workspace.tools` or `workspace.tools_import` permission, preventing users with only a write-access grant from overwriting executable tool code while still allowing metadata edits. [#24513](https://github.com/open-webui/open-webui/pull/24513)
|
||||||
|
- 🔐 **Channel message ownership enforcement.** Updating or deleting messages in group and DM channels now requires message ownership, preventing channel members from tampering with or silently removing other members' messages. [#24506](https://github.com/open-webui/open-webui/pull/24506)
|
||||||
|
- 🔐 **Channel pin write permission.** Pinning and unpinning messages on standard channels now requires write permission instead of read permission, preventing read-only users from modifying pinned content. [#24521](https://github.com/open-webui/open-webui/pull/24521)
|
||||||
|
- 🛡️ **Image generation URL validation.** Generated image URLs are now validated through `validate_url()` before fetching, aligning the defense-in-depth posture with sibling image-loading paths. [#24518](https://github.com/open-webui/open-webui/pull/24518)
|
||||||
|
- 🔐 **Model params exposure for read-only users.** The per-model API endpoint now strips the `params` dict (including system prompts) from responses to callers without write access, preventing read-only users from viewing admin-curated model configuration. [#24525](https://github.com/open-webui/open-webui/pull/24525)
|
||||||
|
- 🛡️ **URL parser SSRF bypass.** URL validation now rejects backslash, tab, CR, and LF characters that cause urllib and requests/aiohttp to disagree on the target host, closing a parser-confusion SSRF bypass. [#24534](https://github.com/open-webui/open-webui/pull/24534)
|
||||||
|
- 🛡️ **Profile image MIME-type allowlist.** Serving profile images from data URIs now enforces a strict MIME-type allowlist (PNG, JPEG, GIF, WEBP by default, configurable via `PROFILE_IMAGE_ALLOWED_MIME_TYPES`) and sets `X-Content-Type-Options: nosniff`, preventing stored-XSS through SVG or other executable content types. [Commit](https://github.com/open-webui/open-webui/commit/15e696691cad98692c329de62ed8a5bdb3a26d4e)
|
||||||
|
- 🔐 **File ownership in folder and knowledge attachments.** Attaching files to folders or knowledge bases now verifies per-file read access, and folder file lists in chat middleware are filtered to entries the caller can read, preventing unauthorized file content from being injected into RAG context. [Commit](https://github.com/open-webui/open-webui/commit/2dbf7b6764a7922458d3b0139687ad6dcd7596d9)
|
||||||
|
- 🔐 **Shared chat access for owners and admins.** Chat owners can now view and clone their own shared chats without requiring an explicit access grant, and administrators can manage shared chat access controls on any chat. [Commit](https://github.com/open-webui/open-webui/commit/3a21b334cce30226750c5c537345dc51bb8bef17), [Commit](https://github.com/open-webui/open-webui/commit/315566064aedeff071854b023d09e5f1ea0eb950)
|
||||||
|
- 🧵 **Legacy chat history self-healing.** Loading legacy conversations now automatically detects broken parent-link graphs in migrated message records, merges missing messages from the embedded JSON history, and backfills them to the normalized table so future loads use the fast path without data loss. [Commit](https://github.com/open-webui/open-webui/commit/1388f4568b8f508c26542673dd01f1fa049e798a)
|
||||||
|
- 🎛️ **Filter selector reactivity.** Model filter checkboxes now derive state reactively from the current filter list and selected IDs instead of capturing a one-time snapshot at mount, so checkboxes update correctly when model contexts or filter configurations change at runtime. [Commit](https://github.com/open-webui/open-webui/commit/d1ef5382377f590f97a6dbaee88f369e6d7c5f6f)
|
||||||
|
- 🌐 **Portuguese (Brazil) translation updates.** Translations for newly added UI items were added along with a consistency pass across existing entries. [#24503](https://github.com/open-webui/open-webui/pull/24503)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- 🧹 **Removed unauthenticated retrieval status endpoint.** The unauthenticated `GET /api/v1/retrieval/` status endpoint has been removed as dead code — retrieval configuration is already available through authenticated admin endpoints. [#24497](https://github.com/open-webui/open-webui/pull/24497)
|
||||||
|
- 📋 **PR template issue requirement.** Pull requests now require a linked Issue or Discussion reference, ensuring better traceability for all contributions. PRs without a linked issue or discussion may be closed without review.
|
||||||
|
|
||||||
## [0.9.4] - 2026-05-09
|
## [0.9.4] - 2026-05-09
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
+1
-1
@@ -43,7 +43,7 @@ ENV APP_BUILD_HASH=${BUILD_HASH}
|
|||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
######## WebUI backend ########
|
######## WebUI backend ########
|
||||||
FROM python:3.11.14-slim-bookworm AS base
|
FROM python:3.11-slim-bookworm AS base
|
||||||
|
|
||||||
# Use args
|
# Use args
|
||||||
ARG USE_CUDA
|
ARG USE_CUDA
|
||||||
|
|||||||
@@ -1226,6 +1226,11 @@ TERMINAL_SERVER_CONNECTIONS = PersistentConfig(
|
|||||||
terminal_server_connections,
|
terminal_server_connections,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
TERMINAL_PROXY_HEADERS = json.loads(os.environ.get('TERMINAL_PROXY_HEADERS', '{}'))
|
||||||
|
except Exception:
|
||||||
|
TERMINAL_PROXY_HEADERS = {}
|
||||||
|
|
||||||
####################################
|
####################################
|
||||||
# WEBUI
|
# WEBUI
|
||||||
####################################
|
####################################
|
||||||
@@ -1371,6 +1376,7 @@ RESPONSE_WATERMARK = PersistentConfig(
|
|||||||
os.environ.get('RESPONSE_WATERMARK', ''),
|
os.environ.get('RESPONSE_WATERMARK', ''),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
IFRAME_CSP = os.environ.get('IFRAME_CSP', '')
|
||||||
|
|
||||||
USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS = (
|
USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS = (
|
||||||
os.environ.get('USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS', 'False').lower() == 'true'
|
os.environ.get('USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS', 'False').lower() == 'true'
|
||||||
@@ -1465,6 +1471,10 @@ USER_PERMISSIONS_NOTES_ALLOW_PUBLIC_SHARING = (
|
|||||||
os.environ.get('USER_PERMISSIONS_NOTES_ALLOW_PUBLIC_SHARING', 'False').lower() == 'true'
|
os.environ.get('USER_PERMISSIONS_NOTES_ALLOW_PUBLIC_SHARING', 'False').lower() == 'true'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
USER_PERMISSIONS_CALENDAR_ALLOW_PUBLIC_SHARING = (
|
||||||
|
os.environ.get('USER_PERMISSIONS_CALENDAR_ALLOW_PUBLIC_SHARING', 'False').lower() == 'true'
|
||||||
|
)
|
||||||
|
|
||||||
USER_PERMISSIONS_ACCESS_GRANTS_ALLOW_USERS = (
|
USER_PERMISSIONS_ACCESS_GRANTS_ALLOW_USERS = (
|
||||||
os.environ.get('USER_PERMISSIONS_ACCESS_GRANTS_ALLOW_USERS', 'True').lower() == 'true'
|
os.environ.get('USER_PERMISSIONS_ACCESS_GRANTS_ALLOW_USERS', 'True').lower() == 'true'
|
||||||
)
|
)
|
||||||
@@ -1585,6 +1595,7 @@ DEFAULT_USER_PERMISSIONS = {
|
|||||||
'notes': USER_PERMISSIONS_NOTES_ALLOW_SHARING,
|
'notes': USER_PERMISSIONS_NOTES_ALLOW_SHARING,
|
||||||
'public_notes': USER_PERMISSIONS_NOTES_ALLOW_PUBLIC_SHARING,
|
'public_notes': USER_PERMISSIONS_NOTES_ALLOW_PUBLIC_SHARING,
|
||||||
'public_chats': USER_PERMISSIONS_CHAT_ALLOW_PUBLIC_SHARING,
|
'public_chats': USER_PERMISSIONS_CHAT_ALLOW_PUBLIC_SHARING,
|
||||||
|
'public_calendars': USER_PERMISSIONS_CALENDAR_ALLOW_PUBLIC_SHARING,
|
||||||
},
|
},
|
||||||
'access_grants': {
|
'access_grants': {
|
||||||
'allow_users': USER_PERMISSIONS_ACCESS_GRANTS_ALLOW_USERS,
|
'allow_users': USER_PERMISSIONS_ACCESS_GRANTS_ALLOW_USERS,
|
||||||
|
|||||||
@@ -260,6 +260,15 @@ ENABLE_EASTER_EGGS = os.environ.get('ENABLE_EASTER_EGGS', 'True').lower() == 'tr
|
|||||||
# controlled origins) and fall through to the default image instead.
|
# controlled origins) and fall through to the default image instead.
|
||||||
ENABLE_PROFILE_IMAGE_URL_FORWARDING = os.environ.get('ENABLE_PROFILE_IMAGE_URL_FORWARDING', 'True').lower() == 'true'
|
ENABLE_PROFILE_IMAGE_URL_FORWARDING = os.environ.get('ENABLE_PROFILE_IMAGE_URL_FORWARDING', 'True').lower() == 'true'
|
||||||
|
|
||||||
|
PROFILE_IMAGE_ALLOWED_MIME_TYPES = frozenset(
|
||||||
|
t.strip()
|
||||||
|
for t in os.environ.get(
|
||||||
|
'PROFILE_IMAGE_ALLOWED_MIME_TYPES',
|
||||||
|
'image/png,image/jpeg,image/gif,image/webp',
|
||||||
|
).split(',')
|
||||||
|
if t.strip()
|
||||||
|
)
|
||||||
|
|
||||||
####################################
|
####################################
|
||||||
# WEBUI_BUILD_HASH
|
# WEBUI_BUILD_HASH
|
||||||
####################################
|
####################################
|
||||||
@@ -824,6 +833,13 @@ else:
|
|||||||
|
|
||||||
AIOHTTP_CLIENT_SESSION_SSL = os.environ.get('AIOHTTP_CLIENT_SESSION_SSL', 'True').lower() == 'true'
|
AIOHTTP_CLIENT_SESSION_SSL = os.environ.get('AIOHTTP_CLIENT_SESSION_SSL', 'True').lower() == 'true'
|
||||||
|
|
||||||
|
# When False (default), outbound HTTP requests do not follow 3xx redirects.
|
||||||
|
# This prevents redirect-based SSRF where a public URL 302-redirects to an
|
||||||
|
# internal address (RFC 1918, loopback, cloud-metadata 169.254.169.254).
|
||||||
|
# Set to True only if your deployment requires redirect following and you
|
||||||
|
# have other SSRF protections in place (e.g. egress firewall).
|
||||||
|
AIOHTTP_CLIENT_ALLOW_REDIRECTS = os.environ.get('AIOHTTP_CLIENT_ALLOW_REDIRECTS', 'False').lower() == 'true'
|
||||||
|
|
||||||
AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST = os.environ.get(
|
AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST = os.environ.get(
|
||||||
'AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST',
|
'AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST',
|
||||||
os.environ.get('AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST', '10'),
|
os.environ.get('AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST', '10'),
|
||||||
|
|||||||
@@ -460,6 +460,7 @@ from open_webui.config import (
|
|||||||
OAUTH_PROVIDERS,
|
OAUTH_PROVIDERS,
|
||||||
WEBUI_URL,
|
WEBUI_URL,
|
||||||
RESPONSE_WATERMARK,
|
RESPONSE_WATERMARK,
|
||||||
|
IFRAME_CSP,
|
||||||
# Admin
|
# Admin
|
||||||
ENABLE_ADMIN_CHAT_ACCESS,
|
ENABLE_ADMIN_CHAT_ACCESS,
|
||||||
ENABLE_ADMIN_ANALYTICS,
|
ENABLE_ADMIN_ANALYTICS,
|
||||||
@@ -1795,7 +1796,9 @@ async def chat_completion(
|
|||||||
|
|
||||||
if metadata.get('chat_id') and user:
|
if metadata.get('chat_id') and user:
|
||||||
chat_id = metadata['chat_id']
|
chat_id = metadata['chat_id']
|
||||||
if not chat_id.startswith('local:'): # temporary chats are not stored
|
if not chat_id.startswith('local:') and not chat_id.startswith(
|
||||||
|
'channel:'
|
||||||
|
): # temporary/channel chats are not stored
|
||||||
if is_new_chat:
|
if is_new_chat:
|
||||||
# Build the full history upfront with ALL assistant placeholders
|
# Build the full history upfront with ALL assistant placeholders
|
||||||
user_message = metadata.get('user_message') or {}
|
user_message = metadata.get('user_message') or {}
|
||||||
@@ -2011,7 +2014,7 @@ async def chat_completion(
|
|||||||
if metadata.get('chat_id') and metadata.get('message_id'):
|
if metadata.get('chat_id') and metadata.get('message_id'):
|
||||||
# Update the chat message with the error
|
# Update the chat message with the error
|
||||||
try:
|
try:
|
||||||
if not metadata['chat_id'].startswith('local:'):
|
if not metadata['chat_id'].startswith('local:') and not metadata['chat_id'].startswith('channel:'):
|
||||||
await Chats.upsert_message_to_chat_by_id_and_message_id(
|
await Chats.upsert_message_to_chat_by_id_and_message_id(
|
||||||
metadata['chat_id'],
|
metadata['chat_id'],
|
||||||
metadata['message_id'],
|
metadata['message_id'],
|
||||||
@@ -2274,7 +2277,7 @@ async def list_tasks_endpoint(request: Request, user=Depends(get_admin_user)):
|
|||||||
|
|
||||||
@app.get('/api/tasks/chat/{chat_id:path}')
|
@app.get('/api/tasks/chat/{chat_id:path}')
|
||||||
async def list_tasks_by_chat_id_endpoint(request: Request, chat_id: str, user=Depends(get_verified_user)):
|
async def list_tasks_by_chat_id_endpoint(request: Request, chat_id: str, user=Depends(get_verified_user)):
|
||||||
if chat_id.startswith('local:'):
|
if chat_id.startswith('local:') or chat_id.startswith('channel:'):
|
||||||
socket_id = chat_id[len('local:') :]
|
socket_id = chat_id[len('local:') :]
|
||||||
owner_id = get_user_id_from_session_pool(socket_id)
|
owner_id = get_user_id_from_session_pool(socket_id)
|
||||||
if owner_id != user.id and user.role != 'admin':
|
if owner_id != user.id and user.role != 'admin':
|
||||||
@@ -2292,7 +2295,7 @@ async def list_tasks_by_chat_id_endpoint(request: Request, chat_id: str, user=De
|
|||||||
|
|
||||||
@app.post('/api/tasks/chat/{chat_id:path}/stop')
|
@app.post('/api/tasks/chat/{chat_id:path}/stop')
|
||||||
async def stop_tasks_by_chat_id_endpoint(request: Request, chat_id: str, user=Depends(get_verified_user)):
|
async def stop_tasks_by_chat_id_endpoint(request: Request, chat_id: str, user=Depends(get_verified_user)):
|
||||||
if chat_id.startswith('local:'):
|
if chat_id.startswith('local:') or chat_id.startswith('channel:'):
|
||||||
socket_id = chat_id[len('local:') :]
|
socket_id = chat_id[len('local:') :]
|
||||||
owner_id = get_user_id_from_session_pool(socket_id)
|
owner_id = get_user_id_from_session_pool(socket_id)
|
||||||
if owner_id != user.id and user.role != 'admin':
|
if owner_id != user.id and user.role != 'admin':
|
||||||
@@ -2444,6 +2447,7 @@ async def get_app_config(request: Request):
|
|||||||
'pending_user_overlay_title': app.state.config.PENDING_USER_OVERLAY_TITLE,
|
'pending_user_overlay_title': app.state.config.PENDING_USER_OVERLAY_TITLE,
|
||||||
'pending_user_overlay_content': app.state.config.PENDING_USER_OVERLAY_CONTENT,
|
'pending_user_overlay_content': app.state.config.PENDING_USER_OVERLAY_CONTENT,
|
||||||
'response_watermark': app.state.config.RESPONSE_WATERMARK,
|
'response_watermark': app.state.config.RESPONSE_WATERMARK,
|
||||||
|
'iframe_csp': IFRAME_CSP,
|
||||||
},
|
},
|
||||||
'license_metadata': app.state.LICENSE_METADATA,
|
'license_metadata': app.state.LICENSE_METADATA,
|
||||||
**(
|
**(
|
||||||
|
|||||||
@@ -459,24 +459,87 @@ class ChatTable:
|
|||||||
return None
|
return None
|
||||||
return row[0] or 'New Chat'
|
return row[0] or 'New Chat'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_unresolved_parent_ids(messages_map: dict) -> set[str]:
|
||||||
|
"""Return parent IDs referenced by messages but absent from the map.
|
||||||
|
|
||||||
|
An empty set means the message graph is fully connected.
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
msg['parentId']
|
||||||
|
for msg in messages_map.values()
|
||||||
|
if msg.get('parentId') and msg['parentId'] not in messages_map
|
||||||
|
}
|
||||||
|
|
||||||
|
async def backfill_messages_by_chat_id(self, chat_id: str, user_id: str, messages: dict[str, dict]) -> None:
|
||||||
|
"""Write messages to the ``chat_message`` table so future lookups
|
||||||
|
use the fast path. Errors are logged but never raised.
|
||||||
|
"""
|
||||||
|
for message_id, message in messages.items():
|
||||||
|
if not isinstance(message, dict) or not message.get('role'):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
await ChatMessages.upsert_message(
|
||||||
|
message_id=message_id,
|
||||||
|
chat_id=chat_id,
|
||||||
|
user_id=user_id,
|
||||||
|
data=message,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
log.warning('Backfill failed for message %s in chat %s: %s', message_id, chat_id, e)
|
||||||
|
|
||||||
async def get_messages_map_by_chat_id(self, id: str) -> Optional[dict]:
|
async def get_messages_map_by_chat_id(self, id: str) -> Optional[dict]:
|
||||||
"""Message map for walking history (see ``get_message_list``).
|
"""Message map for walking history (see ``get_message_list``).
|
||||||
|
|
||||||
Prefer ``chat_message`` rows to avoid loading the large ``chat``
|
Prefer ``chat_message`` rows to avoid loading the large embedded
|
||||||
JSON blob; fall back to embedded history when no rows exist
|
history; fall back to the legacy JSON when no rows exist.
|
||||||
(legacy chats).
|
When rows exist but the parent-link graph has gaps (e.g. migration
|
||||||
|
failures), missing messages are merged from the legacy history
|
||||||
|
and backfilled so future requests self-heal.
|
||||||
"""
|
"""
|
||||||
# Fast path: build from normalized chat_message rows.
|
# Fast path: build from normalized chat_message rows.
|
||||||
messages_map = await ChatMessages.get_messages_map_by_chat_id(id)
|
messages_map = await ChatMessages.get_messages_map_by_chat_id(id)
|
||||||
|
|
||||||
if messages_map is not None:
|
if messages_map is not None:
|
||||||
|
unresolved_ids = self.get_unresolved_parent_ids(messages_map)
|
||||||
|
if not unresolved_ids:
|
||||||
|
return messages_map
|
||||||
|
|
||||||
|
# Graph has gaps — enrich from the legacy embedded history.
|
||||||
|
log.info(
|
||||||
|
'Chat %s: %d unresolved parent reference(s) in chat_message — enriching from legacy history',
|
||||||
|
id,
|
||||||
|
len(unresolved_ids),
|
||||||
|
)
|
||||||
|
chat = await self.get_chat_by_id(id)
|
||||||
|
if chat:
|
||||||
|
history_messages = chat.chat.get('history', {}).get('messages', {}) or {}
|
||||||
|
missing_messages = {
|
||||||
|
message_id: history_messages[message_id]
|
||||||
|
for message_id in unresolved_ids
|
||||||
|
if message_id in history_messages
|
||||||
|
}
|
||||||
|
|
||||||
|
if missing_messages:
|
||||||
|
messages_map.update(missing_messages)
|
||||||
|
|
||||||
|
# Backfill so future requests use the fast path.
|
||||||
|
await self.backfill_messages_by_chat_id(id, chat.user_id, missing_messages)
|
||||||
|
|
||||||
return messages_map
|
return messages_map
|
||||||
|
|
||||||
# No rows — fall back to the embedded JSON blob for legacy chats.
|
# No rows — fall back to the legacy embedded history.
|
||||||
chat = await self.get_chat_by_id(id)
|
chat = await self.get_chat_by_id(id)
|
||||||
if chat is None:
|
if chat is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return chat.chat.get('history', {}).get('messages', {}) or {}
|
history_messages = chat.chat.get('history', {}).get('messages', {}) or {}
|
||||||
|
|
||||||
|
# Backfill so future requests use the fast path.
|
||||||
|
if history_messages:
|
||||||
|
await self.backfill_messages_by_chat_id(id, chat.user_id, history_messages)
|
||||||
|
|
||||||
|
return history_messages
|
||||||
|
|
||||||
async def get_message_by_id_and_message_id(self, id: str, message_id: str) -> Optional[dict]:
|
async def get_message_by_id_and_message_id(self, id: str, message_id: str) -> Optional[dict]:
|
||||||
chat = await self.get_chat_by_id(id)
|
chat = await self.get_chat_by_id(id)
|
||||||
|
|||||||
@@ -103,7 +103,8 @@ class FeedbackForm(BaseModel):
|
|||||||
data: Optional[RatingData] = None
|
data: Optional[RatingData] = None
|
||||||
meta: Optional[dict] = None
|
meta: Optional[dict] = None
|
||||||
snapshot: Optional[SnapshotData] = None
|
snapshot: Optional[SnapshotData] = None
|
||||||
model_config = ConfigDict(extra='allow')
|
# ignore: drop client-supplied id/user_id/version/timestamps at parse time.
|
||||||
|
model_config = ConfigDict(extra='ignore')
|
||||||
|
|
||||||
|
|
||||||
class UserResponse(BaseModel):
|
class UserResponse(BaseModel):
|
||||||
@@ -145,12 +146,13 @@ class FeedbackTable:
|
|||||||
) -> Optional[FeedbackModel]:
|
) -> Optional[FeedbackModel]:
|
||||||
async with get_async_db_context(db) as db:
|
async with get_async_db_context(db) as db:
|
||||||
id = str(uuid.uuid4())
|
id = str(uuid.uuid4())
|
||||||
|
# Spread form_data first so server-controlled fields win on duplicate keys.
|
||||||
feedback = FeedbackModel(
|
feedback = FeedbackModel(
|
||||||
**{
|
**{
|
||||||
|
**form_data.model_dump(),
|
||||||
'id': id,
|
'id': id,
|
||||||
'user_id': user_id,
|
'user_id': user_id,
|
||||||
'version': 0,
|
'version': 0,
|
||||||
**form_data.model_dump(),
|
|
||||||
'created_at': int(time.time()),
|
'created_at': int(time.time()),
|
||||||
'updated_at': int(time.time()),
|
'updated_at': int(time.time()),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ class NoteTable:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
new_note = Note(**note.model_dump(exclude={'access_grants'}))
|
new_note = Note(**note.model_dump(exclude={'access_grants', 'is_pinned'}))
|
||||||
|
|
||||||
db.add(new_note)
|
db.add(new_note)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ from open_webui.retrieval.loaders.youtube import YoutubeLoader
|
|||||||
|
|
||||||
from open_webui.env import (
|
from open_webui.env import (
|
||||||
AIOHTTP_CLIENT_TIMEOUT,
|
AIOHTTP_CLIENT_TIMEOUT,
|
||||||
|
AIOHTTP_CLIENT_ALLOW_REDIRECTS,
|
||||||
OFFLINE_MODE,
|
OFFLINE_MODE,
|
||||||
ENABLE_FORWARD_USER_INFO_HEADERS,
|
ENABLE_FORWARD_USER_INFO_HEADERS,
|
||||||
AIOHTTP_CLIENT_SESSION_SSL,
|
AIOHTTP_CLIENT_SESSION_SSL,
|
||||||
@@ -180,8 +181,12 @@ def get_content_from_url(request, url: str) -> str:
|
|||||||
validate_url(url)
|
validate_url(url)
|
||||||
|
|
||||||
# Streamed GET to check Content-Type without downloading the body.
|
# Streamed GET to check Content-Type without downloading the body.
|
||||||
|
# allow_redirects=False prevents redirect-based SSRF: validate_url() above is
|
||||||
|
# called on the originally-submitted URL only; following 3xx redirects without
|
||||||
|
# re-validation would let an attacker reach private IPs (RFC1918, loopback,
|
||||||
|
# cloud-metadata 169.254.169.254) via a public host that redirects internally.
|
||||||
try:
|
try:
|
||||||
response = requests.get(url, stream=True, timeout=30)
|
response = requests.get(url, stream=True, timeout=30, allow_redirects=AIOHTTP_CLIENT_ALLOW_REDIRECTS)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
content_type = response.headers.get('Content-Type', '')
|
content_type = response.headers.get('Content-Type', '')
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ from open_webui.config import (
|
|||||||
WEB_FETCH_FILTER_LIST,
|
WEB_FETCH_FILTER_LIST,
|
||||||
)
|
)
|
||||||
from open_webui.utils.misc import is_string_allowed
|
from open_webui.utils.misc import is_string_allowed
|
||||||
from open_webui.env import AIOHTTP_CLIENT_SESSION_SSL
|
from open_webui.env import AIOHTTP_CLIENT_SESSION_SSL, AIOHTTP_CLIENT_ALLOW_REDIRECTS
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -69,6 +69,14 @@ def validate_url(url: Union[str, Sequence[str]]):
|
|||||||
if isinstance(validators.url(url), validators.ValidationError):
|
if isinstance(validators.url(url), validators.ValidationError):
|
||||||
raise ValueError(ERROR_MESSAGES.INVALID_URL)
|
raise ValueError(ERROR_MESSAGES.INVALID_URL)
|
||||||
|
|
||||||
|
# Reject parser-confusing chars: urlparse and requests/aiohttp split
|
||||||
|
# on these differently, e.g. http://127.0.0.1\@1.1.1.1 → urlparse
|
||||||
|
# extracts 1.1.1.1 (public, passes filter) while requests connects
|
||||||
|
# to 127.0.0.1 (internal). Same shape with tab/CR/LF.
|
||||||
|
if any(ch in url for ch in ('\\', '\t', '\n', '\r')):
|
||||||
|
log.warning(f'Blocked URL with parser-confusing char: {url!r}')
|
||||||
|
raise ValueError(ERROR_MESSAGES.INVALID_URL)
|
||||||
|
|
||||||
parsed_url = urllib.parse.urlparse(url)
|
parsed_url = urllib.parse.urlparse(url)
|
||||||
|
|
||||||
# Protocol validation - only allow http/https
|
# Protocol validation - only allow http/https
|
||||||
@@ -485,6 +493,17 @@ class SafeWebBaseLoader(WebBaseLoader):
|
|||||||
"""
|
"""
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.trust_env = trust_env
|
self.trust_env = trust_env
|
||||||
|
# Prevent redirect-based SSRF on the synchronous _scrape() path.
|
||||||
|
# validate_url() is called once on the originally-submitted URL, but the
|
||||||
|
# parent WebBaseLoader's _scrape() invokes self.session.get(url, **self.requests_kwargs)
|
||||||
|
# which by default follows redirects. Without the override below, an attacker
|
||||||
|
# can submit a public URL that 302-redirects to an internal address (RFC1918,
|
||||||
|
# 127.0.0.1, 169.254.169.254, etc.) and the redirected target is fetched without
|
||||||
|
# re-validation. Matches the policy enforced on the async _fetch() path below.
|
||||||
|
self.requests_kwargs = {
|
||||||
|
**(self.requests_kwargs or {}),
|
||||||
|
'allow_redirects': AIOHTTP_CLIENT_ALLOW_REDIRECTS,
|
||||||
|
}
|
||||||
|
|
||||||
async def _fetch(self, url: str, retries: int = 3, cooldown: int = 2, backoff: float = 1.5) -> str:
|
async def _fetch(self, url: str, retries: int = 3, cooldown: int = 2, backoff: float = 1.5) -> str:
|
||||||
async with aiohttp.ClientSession(trust_env=self.trust_env) as session:
|
async with aiohttp.ClientSession(trust_env=self.trust_env) as session:
|
||||||
@@ -502,7 +521,7 @@ class SafeWebBaseLoader(WebBaseLoader):
|
|||||||
async with session.get(
|
async with session.get(
|
||||||
url,
|
url,
|
||||||
**(self.requests_kwargs | kwargs),
|
**(self.requests_kwargs | kwargs),
|
||||||
allow_redirects=False,
|
allow_redirects=AIOHTTP_CLIENT_ALLOW_REDIRECTS,
|
||||||
) as response:
|
) as response:
|
||||||
if self.raise_for_status:
|
if self.raise_for_status:
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ from open_webui.models.access_grants import AccessGrants
|
|||||||
from open_webui.models.groups import Groups
|
from open_webui.models.groups import Groups
|
||||||
from open_webui.models.users import UserModel
|
from open_webui.models.users import UserModel
|
||||||
from open_webui.utils.auth import get_verified_user
|
from open_webui.utils.auth import get_verified_user
|
||||||
from open_webui.utils.access_control import has_permission
|
from open_webui.utils.access_control import has_permission, filter_allowed_access_grants
|
||||||
from open_webui.utils.calendar import expand_recurring_event
|
from open_webui.utils.calendar import expand_recurring_event
|
||||||
from open_webui.constants import ERROR_MESSAGES
|
from open_webui.constants import ERROR_MESSAGES
|
||||||
|
|
||||||
@@ -112,6 +112,17 @@ async def get_calendars(request: Request, user: UserModel = Depends(get_verified
|
|||||||
async def create_calendar(request: Request, form_data: CalendarForm, user: UserModel = Depends(get_verified_user)):
|
async def create_calendar(request: Request, form_data: CalendarForm, user: UserModel = Depends(get_verified_user)):
|
||||||
"""Create a new user calendar."""
|
"""Create a new user calendar."""
|
||||||
await check_calendar_permission(request, user)
|
await check_calendar_permission(request, user)
|
||||||
|
# Strip public/user grants the requesting user is not permitted to assign
|
||||||
|
# (matches the channel/notes/models pattern). Without this, any verified user
|
||||||
|
# could create a calendar with `principal_id='*' permission='read'|'write'`,
|
||||||
|
# making their events readable or writable by any other verified user.
|
||||||
|
form_data.access_grants = await filter_allowed_access_grants(
|
||||||
|
request.app.state.config.USER_PERMISSIONS,
|
||||||
|
user.id,
|
||||||
|
user.role,
|
||||||
|
form_data.access_grants,
|
||||||
|
'sharing.public_calendars',
|
||||||
|
)
|
||||||
return await Calendars.insert_new_calendar(user.id, form_data)
|
return await Calendars.insert_new_calendar(user.id, form_data)
|
||||||
|
|
||||||
|
|
||||||
@@ -350,6 +361,20 @@ async def update_calendar(
|
|||||||
if form_data.access_grants is not None and cal.user_id != user.id and user.role != 'admin':
|
if form_data.access_grants is not None and cal.user_id != user.id and user.role != 'admin':
|
||||||
raise HTTPException(status_code=403, detail='Only owner can manage sharing')
|
raise HTTPException(status_code=403, detail='Only owner can manage sharing')
|
||||||
|
|
||||||
|
# Strip public/user grants the requesting user is not permitted to assign
|
||||||
|
# (matches the channel/notes/models pattern). The owner-only check above
|
||||||
|
# only restricts WHO can set grants; this filter restricts WHICH grants
|
||||||
|
# they may set, so a non-admin owner cannot make their calendar
|
||||||
|
# publicly readable/writable without the corresponding sharing permission.
|
||||||
|
if form_data.access_grants is not None:
|
||||||
|
form_data.access_grants = await filter_allowed_access_grants(
|
||||||
|
request.app.state.config.USER_PERMISSIONS,
|
||||||
|
user.id,
|
||||||
|
user.role,
|
||||||
|
form_data.access_grants,
|
||||||
|
'sharing.public_calendars',
|
||||||
|
)
|
||||||
|
|
||||||
updated = await Calendars.update_calendar_by_id(calendar_id, form_data)
|
updated = await Calendars.update_calendar_by_id(calendar_id, form_data)
|
||||||
if not updated:
|
if not updated:
|
||||||
raise HTTPException(status_code=500, detail='Failed to update')
|
raise HTTPException(status_code=500, detail='Failed to update')
|
||||||
|
|||||||
@@ -57,7 +57,6 @@ from open_webui.utils.models import (
|
|||||||
get_all_models,
|
get_all_models,
|
||||||
get_filtered_models,
|
get_filtered_models,
|
||||||
)
|
)
|
||||||
from open_webui.utils.chat import generate_chat_completion
|
|
||||||
|
|
||||||
|
|
||||||
from open_webui.utils.auth import get_admin_user, get_verified_user
|
from open_webui.utils.auth import get_admin_user, get_verified_user
|
||||||
@@ -979,57 +978,48 @@ async def model_response_handler(request, channel, message, user, db=None):
|
|||||||
],
|
],
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Resolve model config (same helpers automations use)
|
||||||
|
from open_webui.utils.automations import (
|
||||||
|
_resolve_model_tool_ids,
|
||||||
|
_resolve_model_features,
|
||||||
|
_resolve_model_filter_ids,
|
||||||
|
)
|
||||||
|
|
||||||
|
tool_ids = _resolve_model_tool_ids(request.app, model_id)
|
||||||
|
features = _resolve_model_features(request.app, model_id)
|
||||||
|
filter_ids = _resolve_model_filter_ids(request.app, model_id)
|
||||||
|
|
||||||
|
# Build full form_data — same shape as frontend POST.
|
||||||
|
# The channel: prefix routes pipeline events to the
|
||||||
|
# channel emitter in socket/main.py instead of the
|
||||||
|
# default chat emitter.
|
||||||
form_data = {
|
form_data = {
|
||||||
'model': model_id,
|
'model': model_id,
|
||||||
'messages': [
|
'messages': [
|
||||||
system_message,
|
system_message,
|
||||||
{'role': 'user', 'content': content},
|
{'role': 'user', 'content': content},
|
||||||
],
|
],
|
||||||
'stream': False,
|
'stream': True,
|
||||||
|
'chat_id': f'channel:{channel.id}',
|
||||||
|
'id': response_message.id,
|
||||||
|
'session_id': f'channel:{channel.id}',
|
||||||
|
'background_tasks': {},
|
||||||
}
|
}
|
||||||
|
if tool_ids:
|
||||||
|
form_data['tool_ids'] = tool_ids
|
||||||
|
if features:
|
||||||
|
form_data['features'] = features
|
||||||
|
if filter_ids:
|
||||||
|
form_data['filter_ids'] = filter_ids
|
||||||
|
|
||||||
res = await generate_chat_completion(
|
# Call the full chat completion pipeline — streaming,
|
||||||
request,
|
# tools, filters, RAG — everything. The pipeline runs as
|
||||||
form_data=form_data,
|
# an async task; the channel emitter handles progressive
|
||||||
user=user,
|
# message updates via socket events.
|
||||||
)
|
await request.app.state.CHAT_COMPLETION_HANDLER(request, form_data, user=user)
|
||||||
|
|
||||||
if res:
|
|
||||||
if res.get('choices', []) and len(res['choices']) > 0:
|
|
||||||
await update_message_by_id(
|
|
||||||
request,
|
|
||||||
channel.id,
|
|
||||||
response_message.id,
|
|
||||||
MessageForm(
|
|
||||||
**{
|
|
||||||
'content': res['choices'][0]['message']['content'],
|
|
||||||
'meta': {
|
|
||||||
'done': True,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
),
|
|
||||||
user,
|
|
||||||
db,
|
|
||||||
)
|
|
||||||
elif res.get('error', None):
|
|
||||||
await update_message_by_id(
|
|
||||||
request,
|
|
||||||
channel.id,
|
|
||||||
response_message.id,
|
|
||||||
MessageForm(
|
|
||||||
**{
|
|
||||||
'content': f'Error: {res["error"]}',
|
|
||||||
'meta': {
|
|
||||||
'done': True,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
),
|
|
||||||
user,
|
|
||||||
db,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.info(e)
|
log.exception(e)
|
||||||
pass
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -1256,7 +1246,8 @@ async def pin_channel_message(
|
|||||||
if not await Channels.is_user_channel_member(channel.id, user.id, db=db):
|
if not await Channels.is_user_channel_member(channel.id, user.id, db=db):
|
||||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT())
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT())
|
||||||
else:
|
else:
|
||||||
if user.role != 'admin' and not await channel_has_access(user.id, channel, permission='read', db=db):
|
# Pin/unpin mutates is_pinned/pinned_by/pinned_at — require write.
|
||||||
|
if user.role != 'admin' and not await channel_has_access(user.id, channel, permission='write', db=db):
|
||||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT())
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT())
|
||||||
|
|
||||||
message = await Messages.get_message_by_id(message_id, db=db)
|
message = await Messages.get_message_by_id(message_id, db=db)
|
||||||
@@ -1368,6 +1359,9 @@ async def update_message_by_id(
|
|||||||
if channel.type in ['group', 'dm']:
|
if channel.type in ['group', 'dm']:
|
||||||
if not await Channels.is_user_channel_member(channel.id, user.id, db=db):
|
if not await Channels.is_user_channel_member(channel.id, user.id, db=db):
|
||||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT())
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT())
|
||||||
|
# Membership is not authorship — block cross-member edits.
|
||||||
|
if user.role != 'admin' and message.user_id != user.id:
|
||||||
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT())
|
||||||
else:
|
else:
|
||||||
if (
|
if (
|
||||||
user.role != 'admin'
|
user.role != 'admin'
|
||||||
@@ -1569,6 +1563,9 @@ async def delete_message_by_id(
|
|||||||
if channel.type in ['group', 'dm']:
|
if channel.type in ['group', 'dm']:
|
||||||
if not await Channels.is_user_channel_member(channel.id, user.id, db=db):
|
if not await Channels.is_user_channel_member(channel.id, user.id, db=db):
|
||||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT())
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT())
|
||||||
|
# Membership is not authorship — block cross-member deletes.
|
||||||
|
if user.role != 'admin' and message.user_id != user.id:
|
||||||
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT())
|
||||||
else:
|
else:
|
||||||
if (
|
if (
|
||||||
user.role != 'admin'
|
user.role != 'admin'
|
||||||
|
|||||||
@@ -877,7 +877,7 @@ async def get_shared_chat_by_id(
|
|||||||
# Look up the original chat_id to check access grants (admins bypass)
|
# Look up the original chat_id to check access grants (admins bypass)
|
||||||
if user.role != 'admin' or not ENABLE_ADMIN_CHAT_ACCESS:
|
if user.role != 'admin' or not ENABLE_ADMIN_CHAT_ACCESS:
|
||||||
shared = await SharedChats.get_by_id(share_id, db=db)
|
shared = await SharedChats.get_by_id(share_id, db=db)
|
||||||
if shared:
|
if shared and shared.user_id != user.id:
|
||||||
has_grant = await AccessGrants.has_access(
|
has_grant = await AccessGrants.has_access(
|
||||||
user_id=user.id,
|
user_id=user.id,
|
||||||
resource_type='shared_chat',
|
resource_type='shared_chat',
|
||||||
@@ -1241,9 +1241,9 @@ async def clone_shared_chat_by_id(
|
|||||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Enforce access grants
|
# Enforce access grants (owner and admins bypass)
|
||||||
shared = await SharedChats.get_by_id(id, db=db)
|
shared = await SharedChats.get_by_id(id, db=db)
|
||||||
if shared and user.role != 'admin':
|
if shared and user.role != 'admin' and shared.user_id != user.id:
|
||||||
has_grant = await AccessGrants.has_access(
|
has_grant = await AccessGrants.has_access(
|
||||||
user_id=user.id,
|
user_id=user.id,
|
||||||
resource_type='shared_chat',
|
resource_type='shared_chat',
|
||||||
@@ -1412,19 +1412,16 @@ async def update_shared_chat_access_by_id(
|
|||||||
user=Depends(get_verified_user),
|
user=Depends(get_verified_user),
|
||||||
db: AsyncSession = Depends(get_async_session),
|
db: AsyncSession = Depends(get_async_session),
|
||||||
):
|
):
|
||||||
chat = await Chats.get_chat_by_id_and_user_id(id, user.id, db=db)
|
if user.role == 'admin':
|
||||||
|
chat = await Chats.get_chat_by_id(id, db=db)
|
||||||
|
else:
|
||||||
|
chat = await Chats.get_chat_by_id_and_user_id(id, user.id, db=db)
|
||||||
if not chat:
|
if not chat:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||||
)
|
)
|
||||||
|
|
||||||
if chat.user_id != user.id and user.role != 'admin':
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
||||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
|
||||||
)
|
|
||||||
|
|
||||||
form_data.access_grants = await filter_allowed_access_grants(
|
form_data.access_grants = await filter_allowed_access_grants(
|
||||||
request.app.state.config.USER_PERMISSIONS,
|
request.app.state.config.USER_PERMISSIONS,
|
||||||
user.id,
|
user.id,
|
||||||
@@ -1449,19 +1446,16 @@ async def get_shared_chat_access_by_id(
|
|||||||
user=Depends(get_verified_user),
|
user=Depends(get_verified_user),
|
||||||
db: AsyncSession = Depends(get_async_session),
|
db: AsyncSession = Depends(get_async_session),
|
||||||
):
|
):
|
||||||
chat = await Chats.get_chat_by_id_and_user_id(id, user.id, db=db)
|
if user.role == 'admin':
|
||||||
|
chat = await Chats.get_chat_by_id(id, db=db)
|
||||||
|
else:
|
||||||
|
chat = await Chats.get_chat_by_id_and_user_id(id, user.id, db=db)
|
||||||
if not chat:
|
if not chat:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||||
)
|
)
|
||||||
|
|
||||||
if chat.user_id != user.id and user.role != 'admin':
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
||||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
|
||||||
)
|
|
||||||
|
|
||||||
grants = await AccessGrants.get_grants_by_resource('shared_chat', id, db=db)
|
grants = await AccessGrants.get_grants_by_resource('shared_chat', id, db=db)
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -16,8 +16,6 @@ from open_webui.models.folders import (
|
|||||||
Folders,
|
Folders,
|
||||||
)
|
)
|
||||||
from open_webui.models.chats import Chats
|
from open_webui.models.chats import Chats
|
||||||
from open_webui.models.files import Files
|
|
||||||
from open_webui.models.knowledge import Knowledges
|
|
||||||
|
|
||||||
|
|
||||||
from open_webui.config import UPLOAD_DIR
|
from open_webui.config import UPLOAD_DIR
|
||||||
@@ -32,6 +30,7 @@ from fastapi.responses import FileResponse, StreamingResponse
|
|||||||
|
|
||||||
from open_webui.utils.auth import get_admin_user, get_verified_user
|
from open_webui.utils.auth import get_admin_user, get_verified_user
|
||||||
from open_webui.utils.access_control import has_permission
|
from open_webui.utils.access_control import has_permission
|
||||||
|
from open_webui.utils.access_control.files import get_accessible_folder_files
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -75,20 +74,10 @@ async def get_folders(
|
|||||||
if folder.parent_id and not await Folders.get_folder_by_id_and_user_id(folder.parent_id, user.id, db=db):
|
if folder.parent_id and not await Folders.get_folder_by_id_and_user_id(folder.parent_id, user.id, db=db):
|
||||||
folder = await Folders.update_folder_parent_id_by_id_and_user_id(folder.id, user.id, None, db=db)
|
folder = await Folders.update_folder_parent_id_by_id_and_user_id(folder.id, user.id, None, db=db)
|
||||||
|
|
||||||
if folder.data:
|
if folder.data and 'files' in folder.data:
|
||||||
if 'files' in folder.data:
|
accessible_files = await get_accessible_folder_files(folder.data['files'], user, db=db)
|
||||||
valid_files = []
|
if len(accessible_files) != len(folder.data.get('files', [])):
|
||||||
for file in folder.data['files']:
|
folder.data['files'] = accessible_files
|
||||||
if file.get('type') == 'file':
|
|
||||||
if await Files.check_access_by_user_id(file.get('id'), user.id, 'read', db=db):
|
|
||||||
valid_files.append(file)
|
|
||||||
elif file.get('type') == 'collection':
|
|
||||||
if await Knowledges.check_access_by_user_id(file.get('id'), user.id, 'read', db=db):
|
|
||||||
valid_files.append(file)
|
|
||||||
else:
|
|
||||||
valid_files.append(file)
|
|
||||||
|
|
||||||
folder.data['files'] = valid_files
|
|
||||||
await Folders.update_folder_by_id_and_user_id(
|
await Folders.update_folder_by_id_and_user_id(
|
||||||
folder.id, user.id, FolderUpdateForm(data=folder.data), db=db
|
folder.id, user.id, FolderUpdateForm(data=folder.data), db=db
|
||||||
)
|
)
|
||||||
@@ -173,6 +162,16 @@ async def update_folder_name_by_id(
|
|||||||
detail=ERROR_MESSAGES.DEFAULT('Folder already exists'),
|
detail=ERROR_MESSAGES.DEFAULT('Folder already exists'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Validate read access to every file/collection being attached.
|
||||||
|
# Folder files are consumed by chat middleware as RAG context.
|
||||||
|
if form_data.data and isinstance(form_data.data.get('files'), list):
|
||||||
|
accessible_files = await get_accessible_folder_files(form_data.data['files'], user, db=db)
|
||||||
|
if len(accessible_files) != len(form_data.data['files']):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
folder = await Folders.update_folder_by_id_and_user_id(id, user.id, form_data, db=db)
|
folder = await Folders.update_folder_by_id_and_user_id(id, user.id, form_data, db=db)
|
||||||
return folder
|
return folder
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ from open_webui.config import (
|
|||||||
)
|
)
|
||||||
from open_webui.constants import ERROR_MESSAGES
|
from open_webui.constants import ERROR_MESSAGES
|
||||||
from open_webui.retrieval.web.utils import validate_url
|
from open_webui.retrieval.web.utils import validate_url
|
||||||
from open_webui.env import AIOHTTP_CLIENT_SESSION_SSL, ENABLE_FORWARD_USER_INFO_HEADERS
|
from open_webui.env import AIOHTTP_CLIENT_SESSION_SSL, AIOHTTP_CLIENT_ALLOW_REDIRECTS, ENABLE_FORWARD_USER_INFO_HEADERS
|
||||||
from open_webui.utils.session_pool import get_session
|
from open_webui.utils.session_pool import get_session
|
||||||
|
|
||||||
from open_webui.models.chats import Chats
|
from open_webui.models.chats import Chats
|
||||||
@@ -442,6 +442,8 @@ GenerateImageForm = CreateImageForm # Alias for backward compatibility
|
|||||||
async def get_image_data(data: str, headers=None):
|
async def get_image_data(data: str, headers=None):
|
||||||
try:
|
try:
|
||||||
if data.startswith('http://') or data.startswith('https://'):
|
if data.startswith('http://') or data.startswith('https://'):
|
||||||
|
# Defense-in-depth: gate before fetch (mirrors load_url_image).
|
||||||
|
validate_url(data)
|
||||||
session = await get_session()
|
session = await get_session()
|
||||||
async with session.get(
|
async with session.get(
|
||||||
data,
|
data,
|
||||||
@@ -807,10 +809,16 @@ async def image_edits(
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
if data.startswith('http://') or data.startswith('https://'):
|
if data.startswith('http://') or data.startswith('https://'):
|
||||||
# Validate URL to prevent SSRF attacks against local/private networks
|
# Validate URL to prevent SSRF attacks against local/private networks.
|
||||||
|
# allow_redirects=False prevents redirect-based SSRF: validate_url() is
|
||||||
|
# called only on the originally-submitted URL; following 3xx redirects
|
||||||
|
# without re-validation would let an attacker reach private IPs via a
|
||||||
|
# public host that redirects internally (e.g. cloud-metadata exfil).
|
||||||
validate_url(data)
|
validate_url(data)
|
||||||
session = await get_session()
|
session = await get_session()
|
||||||
async with session.get(data, ssl=AIOHTTP_CLIENT_SESSION_SSL) as r:
|
async with session.get(
|
||||||
|
data, ssl=AIOHTTP_CLIENT_SESSION_SSL, allow_redirects=AIOHTTP_CLIENT_ALLOW_REDIRECTS
|
||||||
|
) as r:
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
|
|
||||||
image_data = base64.b64encode(await r.read()).decode('utf-8')
|
image_data = base64.b64encode(await r.read()).decode('utf-8')
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ from open_webui.storage.provider import Storage
|
|||||||
from open_webui.constants import ERROR_MESSAGES
|
from open_webui.constants import ERROR_MESSAGES
|
||||||
from open_webui.utils.auth import get_verified_user, get_admin_user
|
from open_webui.utils.auth import get_verified_user, get_admin_user
|
||||||
from open_webui.utils.access_control import has_permission, filter_allowed_access_grants
|
from open_webui.utils.access_control import has_permission, filter_allowed_access_grants
|
||||||
|
from open_webui.utils.access_control.files import has_access_to_file
|
||||||
from open_webui.models.access_grants import AccessGrants
|
from open_webui.models.access_grants import AccessGrants
|
||||||
|
|
||||||
|
|
||||||
@@ -656,6 +657,14 @@ async def add_file_to_knowledge_by_id(
|
|||||||
detail=ERROR_MESSAGES.FILE_NOT_PROCESSED,
|
detail=ERROR_MESSAGES.FILE_NOT_PROCESSED,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# KB write-access alone is not enough — caller must also be able to read the file.
|
||||||
|
if file.user_id != user.id and user.role != 'admin':
|
||||||
|
if not await has_access_to_file(file.id, 'read', user, db=db):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||||
|
)
|
||||||
|
|
||||||
# Add content to the vector database
|
# Add content to the vector database
|
||||||
try:
|
try:
|
||||||
await process_file(
|
await process_file(
|
||||||
@@ -1017,6 +1026,15 @@ async def add_files_to_knowledge_batch(
|
|||||||
detail=f'File {missing_ids[0]} not found',
|
detail=f'File {missing_ids[0]} not found',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Per-file read-access check — same gate as the single-file endpoint.
|
||||||
|
if user.role != 'admin':
|
||||||
|
for file in files:
|
||||||
|
if file.user_id != user.id and not await has_access_to_file(file.id, 'read', user, db=db):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||||
|
)
|
||||||
|
|
||||||
# Process files
|
# Process files
|
||||||
try:
|
try:
|
||||||
result = await process_files_batch(
|
result = await process_files_batch(
|
||||||
|
|||||||
@@ -413,30 +413,37 @@ class ModelIdForm(BaseModel):
|
|||||||
async def get_model_by_id(id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)):
|
async def get_model_by_id(id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)):
|
||||||
model = await Models.get_model_by_id(id, db=db)
|
model = await Models.get_model_by_id(id, db=db)
|
||||||
if model:
|
if model:
|
||||||
if (
|
write_access = (
|
||||||
(user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL)
|
(user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL)
|
||||||
or model.user_id == user.id
|
or user.id == model.user_id
|
||||||
or await AccessGrants.has_access(
|
or await AccessGrants.has_access(
|
||||||
user_id=user.id,
|
user_id=user.id,
|
||||||
resource_type='model',
|
resource_type='model',
|
||||||
resource_id=model.id,
|
resource_id=model.id,
|
||||||
permission='read',
|
permission='write',
|
||||||
db=db,
|
db=db,
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if write_access or await AccessGrants.has_access(
|
||||||
|
user_id=user.id,
|
||||||
|
resource_type='model',
|
||||||
|
resource_id=model.id,
|
||||||
|
permission='read',
|
||||||
|
db=db,
|
||||||
):
|
):
|
||||||
|
model_dict = model.model_dump()
|
||||||
|
# Strip params (system prompt and other admin-curated config)
|
||||||
|
# for read-only callers — matches the params strip already
|
||||||
|
# enforced on /api/models in utils/models.py. Owners, admins
|
||||||
|
# under BYPASS_ADMIN_ACCESS_CONTROL, and write-grant holders
|
||||||
|
# still receive the full object so the workspace edit UI keeps
|
||||||
|
# working for users who legitimately curate the model.
|
||||||
|
if not write_access:
|
||||||
|
model_dict['params'] = {}
|
||||||
return ModelAccessResponse(
|
return ModelAccessResponse(
|
||||||
**model.model_dump(),
|
**model_dict,
|
||||||
write_access=(
|
write_access=write_access,
|
||||||
(user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL)
|
|
||||||
or user.id == model.user_id
|
|
||||||
or await AccessGrants.has_access(
|
|
||||||
user_id=user.id,
|
|
||||||
resource_type='model',
|
|
||||||
resource_id=model.id,
|
|
||||||
permission='write',
|
|
||||||
db=db,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|||||||
@@ -294,7 +294,10 @@ async def get_note_by_id(
|
|||||||
)
|
)
|
||||||
|
|
||||||
pinned_note_ids = await Notes.get_pinned_note_ids(user.id, db=db)
|
pinned_note_ids = await Notes.get_pinned_note_ids(user.id, db=db)
|
||||||
return NoteResponse(**note.model_dump(), write_access=write_access, is_pinned=note.id in pinned_note_ids)
|
return NoteResponse(
|
||||||
|
**{**note.model_dump(), 'is_pinned': note.id in pinned_note_ids},
|
||||||
|
write_access=write_access,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
############################
|
############################
|
||||||
|
|||||||
@@ -260,22 +260,6 @@ class SearchForm(BaseModel):
|
|||||||
queries: List[str]
|
queries: List[str]
|
||||||
|
|
||||||
|
|
||||||
@router.get('/')
|
|
||||||
async def get_status(request: Request):
|
|
||||||
return {
|
|
||||||
'status': True,
|
|
||||||
'CHUNK_SIZE': request.app.state.config.CHUNK_SIZE,
|
|
||||||
'CHUNK_OVERLAP': request.app.state.config.CHUNK_OVERLAP,
|
|
||||||
'RAG_TEMPLATE': request.app.state.config.RAG_TEMPLATE,
|
|
||||||
'RAG_EMBEDDING_ENGINE': request.app.state.config.RAG_EMBEDDING_ENGINE,
|
|
||||||
'RAG_EMBEDDING_MODEL': request.app.state.config.RAG_EMBEDDING_MODEL,
|
|
||||||
'RAG_RERANKING_MODEL': request.app.state.config.RAG_RERANKING_MODEL,
|
|
||||||
'RAG_EMBEDDING_BATCH_SIZE': request.app.state.config.RAG_EMBEDDING_BATCH_SIZE,
|
|
||||||
'ENABLE_ASYNC_EMBEDDING': request.app.state.config.ENABLE_ASYNC_EMBEDDING,
|
|
||||||
'RAG_EMBEDDING_CONCURRENT_REQUESTS': request.app.state.config.RAG_EMBEDDING_CONCURRENT_REQUESTS,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@router.get('/embedding')
|
@router.get('/embedding')
|
||||||
async def get_embedding_config(request: Request, user=Depends(get_admin_user)):
|
async def get_embedding_config(request: Request, user=Depends(get_admin_user)):
|
||||||
return {
|
return {
|
||||||
@@ -1583,6 +1567,8 @@ async def process_file(
|
|||||||
|
|
||||||
if collection_name is None:
|
if collection_name is None:
|
||||||
collection_name = f'file-{file.id}'
|
collection_name = f'file-{file.id}'
|
||||||
|
else:
|
||||||
|
await _validate_collection_access([collection_name], user, access_type='write')
|
||||||
|
|
||||||
if form_data.content:
|
if form_data.content:
|
||||||
# Update the content in the file
|
# Update the content in the file
|
||||||
@@ -2633,6 +2619,9 @@ async def process_files_batch(
|
|||||||
|
|
||||||
collection_name = form_data.collection_name
|
collection_name = form_data.collection_name
|
||||||
|
|
||||||
|
if collection_name:
|
||||||
|
await _validate_collection_access([collection_name], user, access_type='write')
|
||||||
|
|
||||||
file_results: List[BatchProcessFilesResult] = []
|
file_results: List[BatchProcessFilesResult] = []
|
||||||
file_errors: List[BatchProcessFilesResult] = []
|
file_errors: List[BatchProcessFilesResult] = []
|
||||||
file_updates: List[FileUpdateForm] = []
|
file_updates: List[FileUpdateForm] = []
|
||||||
|
|||||||
@@ -176,6 +176,19 @@ async def create_new_skill(
|
|||||||
detail=ERROR_MESSAGES.ID_TAKEN,
|
detail=ERROR_MESSAGES.ID_TAKEN,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Strip public/user grants the requesting user is not permitted to assign
|
||||||
|
# (matches the channel/notes/calendar pattern). Without this, a user with
|
||||||
|
# workspace.skills permission could attach principal_id='*' read/write
|
||||||
|
# grants in the create payload, bypassing the sharing.public_skills gate
|
||||||
|
# that the dedicated /access/update endpoint already enforces.
|
||||||
|
form_data.access_grants = await filter_allowed_access_grants(
|
||||||
|
request.app.state.config.USER_PERMISSIONS,
|
||||||
|
user.id,
|
||||||
|
user.role,
|
||||||
|
form_data.access_grants,
|
||||||
|
'sharing.public_skills',
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
skill = await Skills.insert_new_skill(user.id, form_data, db=db)
|
skill = await Skills.insert_new_skill(user.id, form_data, db=db)
|
||||||
if skill:
|
if skill:
|
||||||
@@ -276,6 +289,19 @@ async def update_skill_by_id(
|
|||||||
detail=ERROR_MESSAGES.UNAUTHORIZED,
|
detail=ERROR_MESSAGES.UNAUTHORIZED,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Strip public/user grants the requesting user is not permitted to assign
|
||||||
|
# (matches the channel/notes/calendar pattern). The access check above only
|
||||||
|
# restricts WHO can write to the skill; this filter restricts WHICH grants
|
||||||
|
# they may set, so a non-admin owner cannot make their own skill publicly
|
||||||
|
# readable/writable without sharing.public_skills permission.
|
||||||
|
form_data.access_grants = await filter_allowed_access_grants(
|
||||||
|
request.app.state.config.USER_PERMISSIONS,
|
||||||
|
user.id,
|
||||||
|
user.role,
|
||||||
|
form_data.access_grants,
|
||||||
|
'sharing.public_skills',
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
updated = {
|
updated = {
|
||||||
**form_data.model_dump(exclude={'id'}),
|
**form_data.model_dump(exclude={'id'}),
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from starlette.background import BackgroundTask
|
|||||||
from open_webui.utils.auth import get_verified_user
|
from open_webui.utils.auth import get_verified_user
|
||||||
from open_webui.utils.access_control import has_connection_access
|
from open_webui.utils.access_control import has_connection_access
|
||||||
from open_webui.env import AIOHTTP_CLIENT_SESSION_SSL
|
from open_webui.env import AIOHTTP_CLIENT_SESSION_SSL
|
||||||
|
from open_webui.config import TERMINAL_PROXY_HEADERS
|
||||||
from open_webui.models.groups import Groups
|
from open_webui.models.groups import Groups
|
||||||
from open_webui.models.users import Users
|
from open_webui.models.users import Users
|
||||||
|
|
||||||
@@ -151,6 +152,8 @@ async def proxy_terminal(
|
|||||||
for key, value in upstream_response.headers.items()
|
for key, value in upstream_response.headers.items()
|
||||||
if key.lower() not in STRIPPED_RESPONSE_HEADERS
|
if key.lower() not in STRIPPED_RESPONSE_HEADERS
|
||||||
}
|
}
|
||||||
|
if TERMINAL_PROXY_HEADERS:
|
||||||
|
filtered_headers.update(TERMINAL_PROXY_HEADERS)
|
||||||
|
|
||||||
# Stream binary responses directly
|
# Stream binary responses directly
|
||||||
if any(t in upstream_content_type for t in STREAMING_CONTENT_TYPES):
|
if any(t in upstream_content_type for t in STREAMING_CONTENT_TYPES):
|
||||||
|
|||||||
@@ -480,6 +480,17 @@ async def update_tools_by_id(
|
|||||||
detail=ERROR_MESSAGES.UNAUTHORIZED,
|
detail=ERROR_MESSAGES.UNAUTHORIZED,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Content edits trigger exec on load — gate them behind workspace.tools (matches /create).
|
||||||
|
if form_data.content != tools.content:
|
||||||
|
if user.role != 'admin' and not (
|
||||||
|
await has_permission(user.id, 'workspace.tools', request.app.state.config.USER_PERMISSIONS, db=db)
|
||||||
|
or await has_permission(user.id, 'workspace.tools_import', request.app.state.config.USER_PERMISSIONS, db=db)
|
||||||
|
):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail=ERROR_MESSAGES.UNAUTHORIZED,
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
form_data.content = replace_imports(form_data.content)
|
form_data.content = replace_imports(form_data.content)
|
||||||
tool_module, frontmatter = await load_tool_module_by_id(id, content=form_data.content)
|
tool_module, frontmatter = await load_tool_module_by_id(id, content=form_data.content)
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ from open_webui.models.users import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from open_webui.constants import ERROR_MESSAGES
|
from open_webui.constants import ERROR_MESSAGES
|
||||||
from open_webui.env import ENABLE_PROFILE_IMAGE_URL_FORWARDING, STATIC_DIR
|
from open_webui.env import ENABLE_PROFILE_IMAGE_URL_FORWARDING, PROFILE_IMAGE_ALLOWED_MIME_TYPES, STATIC_DIR
|
||||||
from open_webui.internal.db import get_async_session
|
from open_webui.internal.db import get_async_session
|
||||||
|
|
||||||
|
|
||||||
@@ -194,6 +194,7 @@ class SharingPermissions(BaseModel):
|
|||||||
notes: bool = False
|
notes: bool = False
|
||||||
public_notes: bool = True
|
public_notes: bool = True
|
||||||
public_chats: bool = False
|
public_chats: bool = False
|
||||||
|
public_calendars: bool = False
|
||||||
|
|
||||||
|
|
||||||
class AccessGrantsPermissions(BaseModel):
|
class AccessGrantsPermissions(BaseModel):
|
||||||
@@ -235,6 +236,7 @@ class FeaturesPermissions(BaseModel):
|
|||||||
code_interpreter: bool = True
|
code_interpreter: bool = True
|
||||||
memories: bool = True
|
memories: bool = True
|
||||||
automations: bool = False
|
automations: bool = False
|
||||||
|
calendar: bool = True
|
||||||
|
|
||||||
|
|
||||||
class SettingsPermissions(BaseModel):
|
class SettingsPermissions(BaseModel):
|
||||||
@@ -492,12 +494,18 @@ async def get_user_profile_image_by_id(user_id: str, user=Depends(get_verified_u
|
|||||||
header, base64_data = user.profile_image_url.split(',', 1)
|
header, base64_data = user.profile_image_url.split(',', 1)
|
||||||
image_data = base64.b64decode(base64_data)
|
image_data = base64.b64decode(base64_data)
|
||||||
image_buffer = io.BytesIO(image_data)
|
image_buffer = io.BytesIO(image_data)
|
||||||
media_type = header.split(';')[0].lstrip('data:')
|
media_type = header.split(';')[0].lstrip('data:').lower()
|
||||||
|
|
||||||
|
if media_type not in PROFILE_IMAGE_ALLOWED_MIME_TYPES:
|
||||||
|
return FileResponse(f'{STATIC_DIR}/user.png')
|
||||||
|
|
||||||
return StreamingResponse(
|
return StreamingResponse(
|
||||||
image_buffer,
|
image_buffer,
|
||||||
media_type=media_type,
|
media_type=media_type,
|
||||||
headers={'Content-Disposition': 'inline'},
|
headers={
|
||||||
|
'Content-Disposition': 'inline',
|
||||||
|
'X-Content-Type-Options': 'nosniff',
|
||||||
|
},
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -832,7 +832,76 @@ async def disconnect(sid):
|
|||||||
# print(f"Unknown session ID {sid} disconnected")
|
# print(f"Unknown session ID {sid} disconnected")
|
||||||
|
|
||||||
|
|
||||||
|
async def _make_channel_emitter(request_info):
|
||||||
|
"""Event emitter that routes pipeline output to a channel message.
|
||||||
|
|
||||||
|
Translates chat:completion events into channel message:update socket
|
||||||
|
emissions, throttled to avoid flooding with per-token updates.
|
||||||
|
"""
|
||||||
|
channel_id = request_info['chat_id'].removeprefix('channel:')
|
||||||
|
message_id = request_info['message_id']
|
||||||
|
|
||||||
|
state = {'last_emit_at': 0.0}
|
||||||
|
THROTTLE_INTERVAL = 0.15 # ~6 updates/sec
|
||||||
|
|
||||||
|
async def _emit_channel_update(content: str, done: bool = False):
|
||||||
|
from open_webui.models.messages import Messages, MessageForm
|
||||||
|
|
||||||
|
update_form = MessageForm(content=content)
|
||||||
|
if done:
|
||||||
|
# Merge done flag into existing meta (preserve model_id etc.)
|
||||||
|
msg = await Messages.get_message_by_id(message_id)
|
||||||
|
existing_meta = (msg.meta or {}) if msg else {}
|
||||||
|
update_form = MessageForm(
|
||||||
|
content=content,
|
||||||
|
meta={**existing_meta, 'done': True},
|
||||||
|
)
|
||||||
|
|
||||||
|
await Messages.update_message_by_id(message_id, update_form)
|
||||||
|
message = await Messages.get_message_by_id(message_id)
|
||||||
|
if message:
|
||||||
|
await sio.emit(
|
||||||
|
'events:channel',
|
||||||
|
{
|
||||||
|
'channel_id': channel_id,
|
||||||
|
'message_id': message_id,
|
||||||
|
'data': {
|
||||||
|
'type': 'message:update',
|
||||||
|
'data': message.model_dump(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
to=f'channel:{channel_id}',
|
||||||
|
)
|
||||||
|
|
||||||
|
async def __channel_emitter__(event_data):
|
||||||
|
event_type = event_data.get('type')
|
||||||
|
|
||||||
|
if event_type == 'chat:completion':
|
||||||
|
data = event_data.get('data', {})
|
||||||
|
content = data.get('content', '')
|
||||||
|
done = data.get('done', False)
|
||||||
|
|
||||||
|
if not content and not done:
|
||||||
|
return
|
||||||
|
|
||||||
|
now = __import__('time').time()
|
||||||
|
if done or (now - state['last_emit_at']) >= THROTTLE_INTERVAL:
|
||||||
|
state['last_emit_at'] = now
|
||||||
|
await _emit_channel_update(content, done)
|
||||||
|
|
||||||
|
elif event_type == 'chat:message:error':
|
||||||
|
error = event_data.get('data', {}).get('error', {})
|
||||||
|
error_content = error.get('content', 'An error occurred') if isinstance(error, dict) else str(error)
|
||||||
|
await _emit_channel_update(f'Error: {error_content}', done=True)
|
||||||
|
|
||||||
|
return __channel_emitter__
|
||||||
|
|
||||||
|
|
||||||
async def get_event_emitter(request_info, update_db=True):
|
async def get_event_emitter(request_info, update_db=True):
|
||||||
|
# Channel mode: route pipeline output to channel message updates
|
||||||
|
if request_info.get('chat_id', '').startswith('channel:'):
|
||||||
|
return await _make_channel_emitter(request_info)
|
||||||
|
|
||||||
async def __event_emitter__(event_data):
|
async def __event_emitter__(event_data):
|
||||||
user_id = request_info['user_id']
|
user_id = request_info['user_id']
|
||||||
chat_id = request_info['chat_id']
|
chat_id = request_info['chat_id']
|
||||||
|
|||||||
@@ -87,3 +87,38 @@ async def has_access_to_file(
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
async def get_accessible_folder_files(
|
||||||
|
entries: list[dict] | None,
|
||||||
|
user: UserModel,
|
||||||
|
db: AsyncSession | None = None,
|
||||||
|
) -> list[dict]:
|
||||||
|
"""Filter folder.data['files'] entries to those the caller can read.
|
||||||
|
|
||||||
|
Each entry is expected to have 'type' ('file' or 'collection') and 'id'.
|
||||||
|
Admins bypass all checks. Unknown types are kept as-is.
|
||||||
|
"""
|
||||||
|
if not entries:
|
||||||
|
return []
|
||||||
|
if user.role == 'admin':
|
||||||
|
return list(entries)
|
||||||
|
|
||||||
|
accessible: list[dict] = []
|
||||||
|
for entry in entries:
|
||||||
|
if not isinstance(entry, dict):
|
||||||
|
continue
|
||||||
|
entry_type = entry.get('type')
|
||||||
|
entry_id = entry.get('id')
|
||||||
|
if not entry_id:
|
||||||
|
accessible.append(entry)
|
||||||
|
continue
|
||||||
|
if entry_type == 'file':
|
||||||
|
if await has_access_to_file(entry_id, 'read', user, db=db):
|
||||||
|
accessible.append(entry)
|
||||||
|
elif entry_type == 'collection':
|
||||||
|
if await Knowledges.check_access_by_user_id(entry_id, user.id, 'read', db=db):
|
||||||
|
accessible.append(entry)
|
||||||
|
else:
|
||||||
|
accessible.append(entry)
|
||||||
|
return accessible
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import aiohttp
|
|||||||
import websockets
|
import websockets
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from open_webui.env import AIOHTTP_CLIENT_ALLOW_REDIRECTS
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@@ -88,7 +90,7 @@ class JupyterCodeExecuter:
|
|||||||
async with self.session.post(
|
async with self.session.post(
|
||||||
'login',
|
'login',
|
||||||
data={'_xsrf': xsrf_token, 'password': self.password},
|
data={'_xsrf': xsrf_token, 'password': self.password},
|
||||||
allow_redirects=False,
|
allow_redirects=AIOHTTP_CLIENT_ALLOW_REDIRECTS,
|
||||||
) as response:
|
) as response:
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
self.session.cookie_jar.update_cookies(response.cookies)
|
self.session.cookie_jar.update_cookies(response.cookies)
|
||||||
|
|||||||
@@ -26,7 +26,11 @@ import base64
|
|||||||
import io
|
import io
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from open_webui.env import AIOHTTP_CLIENT_SESSION_SSL, ENABLE_IMAGE_CONTENT_TYPE_EXTENSION_FALLBACK
|
from open_webui.env import (
|
||||||
|
AIOHTTP_CLIENT_ALLOW_REDIRECTS,
|
||||||
|
AIOHTTP_CLIENT_SESSION_SSL,
|
||||||
|
ENABLE_IMAGE_CONTENT_TYPE_EXTENSION_FALLBACK,
|
||||||
|
)
|
||||||
from open_webui.utils.session_pool import get_session
|
from open_webui.utils.session_pool import get_session
|
||||||
|
|
||||||
BASE64_IMAGE_URL_PREFIX = re.compile(r'data:image/\w+;base64,', re.IGNORECASE)
|
BASE64_IMAGE_URL_PREFIX = re.compile(r'data:image/\w+;base64,', re.IGNORECASE)
|
||||||
@@ -53,11 +57,17 @@ _IMAGE_MIME_FALLBACK = {
|
|||||||
async def get_image_base64_from_url(url: str) -> Optional[str]:
|
async def get_image_base64_from_url(url: str) -> Optional[str]:
|
||||||
try:
|
try:
|
||||||
if url.startswith('http'):
|
if url.startswith('http'):
|
||||||
# Validate URL to prevent SSRF attacks against local/private networks
|
# Validate URL to prevent SSRF attacks against local/private networks.
|
||||||
|
# allow_redirects=False prevents redirect-based SSRF: validate_url() is
|
||||||
|
# called only on the originally-submitted URL; following 3xx redirects
|
||||||
|
# without re-validation would let an attacker reach private IPs via a
|
||||||
|
# public host that redirects internally (e.g. cloud-metadata exfil).
|
||||||
validate_url(url)
|
validate_url(url)
|
||||||
# Download the image from the URL
|
# Download the image from the URL
|
||||||
session = await get_session()
|
session = await get_session()
|
||||||
async with session.get(url, ssl=AIOHTTP_CLIENT_SESSION_SSL) as response:
|
async with session.get(
|
||||||
|
url, ssl=AIOHTTP_CLIENT_SESSION_SSL, allow_redirects=AIOHTTP_CLIENT_ALLOW_REDIRECTS
|
||||||
|
) as response:
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
image_data = await response.read()
|
image_data = await response.read()
|
||||||
encoded_string = base64.b64encode(image_data).decode('utf-8')
|
encoded_string = base64.b64encode(image_data).decode('utf-8')
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ from open_webui.utils.tools import (
|
|||||||
get_terminal_tools,
|
get_terminal_tools,
|
||||||
)
|
)
|
||||||
from open_webui.utils.access_control import has_connection_access
|
from open_webui.utils.access_control import has_connection_access
|
||||||
|
from open_webui.utils.access_control.files import get_accessible_folder_files
|
||||||
from open_webui.utils.plugin import load_function_module_by_id
|
from open_webui.utils.plugin import load_function_module_by_id
|
||||||
from open_webui.utils.filter import (
|
from open_webui.utils.filter import (
|
||||||
get_sorted_filter_ids,
|
get_sorted_filter_ids,
|
||||||
@@ -1707,7 +1708,7 @@ async def add_file_context(messages: list, chat_id: str, user) -> list:
|
|||||||
"""
|
"""
|
||||||
Add file URLs to messages for native function calling.
|
Add file URLs to messages for native function calling.
|
||||||
"""
|
"""
|
||||||
if not chat_id or chat_id.startswith('local:'):
|
if not chat_id or chat_id.startswith('local:') or chat_id.startswith('channel:'):
|
||||||
return messages
|
return messages
|
||||||
|
|
||||||
chat = await Chats.get_chat_by_id_and_user_id(chat_id, user.id)
|
chat = await Chats.get_chat_by_id_and_user_id(chat_id, user.id)
|
||||||
@@ -1763,7 +1764,7 @@ async def chat_image_generation_handler(request: Request, form_data: dict, extra
|
|||||||
if not chat_id or not isinstance(chat_id, str) or not __event_emitter__:
|
if not chat_id or not isinstance(chat_id, str) or not __event_emitter__:
|
||||||
return form_data
|
return form_data
|
||||||
|
|
||||||
if chat_id.startswith('local:'):
|
if chat_id.startswith('local:') or chat_id.startswith('channel:'):
|
||||||
message_list = form_data.get('messages', [])
|
message_list = form_data.get('messages', [])
|
||||||
else:
|
else:
|
||||||
chat = await Chats.get_chat_by_id_and_user_id(chat_id, user.id)
|
chat = await Chats.get_chat_by_id_and_user_id(chat_id, user.id)
|
||||||
@@ -2295,7 +2296,7 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
|||||||
chat_id = metadata.get('chat_id')
|
chat_id = metadata.get('chat_id')
|
||||||
user_message_id = metadata.get('user_message_id')
|
user_message_id = metadata.get('user_message_id')
|
||||||
|
|
||||||
if chat_id and user_message_id and not chat_id.startswith('local:'):
|
if chat_id and user_message_id and not chat_id.startswith('local:') and not chat_id.startswith('channel:'):
|
||||||
db_messages = await load_messages_from_db(chat_id, user_message_id)
|
db_messages = await load_messages_from_db(chat_id, user_message_id)
|
||||||
if db_messages:
|
if db_messages:
|
||||||
# Continue: frontend sends assistant_message_id when continuing
|
# Continue: frontend sends assistant_message_id when continuing
|
||||||
@@ -2407,15 +2408,17 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
|||||||
if 'system_prompt' in folder.data:
|
if 'system_prompt' in folder.data:
|
||||||
form_data = await apply_system_prompt_to_body(folder.data['system_prompt'], form_data, metadata, user)
|
form_data = await apply_system_prompt_to_body(folder.data['system_prompt'], form_data, metadata, user)
|
||||||
if 'files' in folder.data:
|
if 'files' in folder.data:
|
||||||
|
# Defensive: filter to entries the caller can still read.
|
||||||
|
allowed_files = await get_accessible_folder_files(folder.data['files'], user)
|
||||||
if metadata.get('params', {}).get('function_calling') != 'native':
|
if metadata.get('params', {}).get('function_calling') != 'native':
|
||||||
form_data['files'] = [
|
form_data['files'] = [
|
||||||
*folder.data['files'],
|
*allowed_files,
|
||||||
*form_data.get('files', []),
|
*form_data.get('files', []),
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
# Native FC: skip RAG injection, builtin tools
|
# Native FC: skip RAG injection, builtin tools
|
||||||
# will read folder knowledge from metadata.
|
# will read folder knowledge from metadata.
|
||||||
metadata['folder_knowledge'] = folder.data['files']
|
metadata['folder_knowledge'] = allowed_files
|
||||||
|
|
||||||
# Model "Knowledge" handling
|
# Model "Knowledge" handling
|
||||||
user_message = get_last_user_message(form_data['messages'])
|
user_message = get_last_user_message(form_data['messages'])
|
||||||
@@ -2615,7 +2618,7 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
|||||||
folder = await Folders.get_folder_by_id_and_user_id(folder_id, user.id)
|
folder = await Folders.get_folder_by_id_and_user_id(folder_id, user.id)
|
||||||
if folder and folder.data and 'files' in folder.data:
|
if folder and folder.data and 'files' in folder.data:
|
||||||
files = [f for f in files if f.get('id', None) != folder_id]
|
files = [f for f in files if f.get('id', None) != folder_id]
|
||||||
files = [*files, *folder.data['files']]
|
files = [*files, *await get_accessible_folder_files(folder.data['files'], user)]
|
||||||
|
|
||||||
# files = [*files, *[{"type": "url", "url": url, "name": url} for url in urls]]
|
# files = [*files, *[{"type": "url", "url": url, "name": url} for url in urls]]
|
||||||
# Remove duplicate files based on their content
|
# Remove duplicate files based on their content
|
||||||
@@ -3055,7 +3058,11 @@ async def background_tasks_handler(ctx):
|
|||||||
message = None
|
message = None
|
||||||
messages = []
|
messages = []
|
||||||
|
|
||||||
if 'chat_id' in metadata and not metadata['chat_id'].startswith('local:'):
|
if (
|
||||||
|
'chat_id' in metadata
|
||||||
|
and not metadata['chat_id'].startswith('local:')
|
||||||
|
and not metadata['chat_id'].startswith('channel:')
|
||||||
|
):
|
||||||
messages_map = await Chats.get_messages_map_by_chat_id(metadata['chat_id'])
|
messages_map = await Chats.get_messages_map_by_chat_id(metadata['chat_id'])
|
||||||
message = messages_map.get(metadata['message_id']) if messages_map else None
|
message = messages_map.get(metadata['message_id']) if messages_map else None
|
||||||
|
|
||||||
@@ -3135,7 +3142,9 @@ async def background_tasks_handler(ctx):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
if not metadata.get('chat_id', '').startswith('local:'):
|
if not metadata.get('chat_id', '').startswith('local:') and not metadata.get(
|
||||||
|
'chat_id', ''
|
||||||
|
).startswith('channel:'):
|
||||||
await Chats.upsert_message_to_chat_by_id_and_message_id(
|
await Chats.upsert_message_to_chat_by_id_and_message_id(
|
||||||
metadata['chat_id'],
|
metadata['chat_id'],
|
||||||
metadata['message_id'],
|
metadata['message_id'],
|
||||||
@@ -3147,7 +3156,9 @@ async def background_tasks_handler(ctx):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if not metadata.get('chat_id', '').startswith('local:'): # Only update titles and tags for non-temp chats
|
if not metadata.get('chat_id', '').startswith('local:') and not metadata.get('chat_id', '').startswith(
|
||||||
|
'channel:'
|
||||||
|
): # Only update titles and tags for non-temp chats
|
||||||
if TASKS.TITLE_GENERATION in tasks:
|
if TASKS.TITLE_GENERATION in tasks:
|
||||||
user_message = get_last_user_message(messages)
|
user_message = get_last_user_message(messages)
|
||||||
if user_message and len(user_message) > 100:
|
if user_message and len(user_message) > 100:
|
||||||
@@ -3271,7 +3282,7 @@ async def outlet_filter_handler(ctx):
|
|||||||
if not chat_id or not message_id:
|
if not chat_id or not message_id:
|
||||||
return
|
return
|
||||||
|
|
||||||
is_temp_chat = chat_id.startswith('local:')
|
is_temp_chat = chat_id.startswith('local:') or chat_id.startswith('channel:')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
messages_map = None
|
messages_map = None
|
||||||
@@ -3413,13 +3424,14 @@ async def non_streaming_chat_response_handler(response, ctx):
|
|||||||
|
|
||||||
log.error('Provider returned error (non-streaming): %s', error)
|
log.error('Provider returned error (non-streaming): %s', error)
|
||||||
|
|
||||||
await Chats.upsert_message_to_chat_by_id_and_message_id(
|
if not metadata['chat_id'].startswith('channel:'):
|
||||||
metadata['chat_id'],
|
await Chats.upsert_message_to_chat_by_id_and_message_id(
|
||||||
metadata['message_id'],
|
metadata['chat_id'],
|
||||||
{
|
metadata['message_id'],
|
||||||
'error': {'content': error},
|
{
|
||||||
},
|
'error': {'content': error},
|
||||||
)
|
},
|
||||||
|
)
|
||||||
if isinstance(error, str) or isinstance(error, dict):
|
if isinstance(error, str) or isinstance(error, dict):
|
||||||
await event_emitter(
|
await event_emitter(
|
||||||
{
|
{
|
||||||
@@ -3428,7 +3440,7 @@ async def non_streaming_chat_response_handler(response, ctx):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
if 'selected_model_id' in response_data:
|
if 'selected_model_id' in response_data and not metadata['chat_id'].startswith('channel:'):
|
||||||
await Chats.upsert_message_to_chat_by_id_and_message_id(
|
await Chats.upsert_message_to_chat_by_id_and_message_id(
|
||||||
metadata['chat_id'],
|
metadata['chat_id'],
|
||||||
metadata['message_id'],
|
metadata['message_id'],
|
||||||
@@ -3449,7 +3461,11 @@ async def non_streaming_chat_response_handler(response, ctx):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
title = await Chats.get_chat_title_by_id(metadata['chat_id'])
|
title = (
|
||||||
|
await Chats.get_chat_title_by_id(metadata['chat_id'])
|
||||||
|
if not metadata['chat_id'].startswith('channel:')
|
||||||
|
else ''
|
||||||
|
)
|
||||||
|
|
||||||
# Use output from backend if provided (OR-compliant backends),
|
# Use output from backend if provided (OR-compliant backends),
|
||||||
# otherwise generate from response content
|
# otherwise generate from response content
|
||||||
@@ -3480,17 +3496,18 @@ async def non_streaming_chat_response_handler(response, ctx):
|
|||||||
# Save message in the database
|
# Save message in the database
|
||||||
usage = normalize_usage(response_data.get('usage', {}) or {})
|
usage = normalize_usage(response_data.get('usage', {}) or {})
|
||||||
|
|
||||||
await Chats.upsert_message_to_chat_by_id_and_message_id(
|
if not metadata['chat_id'].startswith('channel:'):
|
||||||
metadata['chat_id'],
|
await Chats.upsert_message_to_chat_by_id_and_message_id(
|
||||||
metadata['message_id'],
|
metadata['chat_id'],
|
||||||
{
|
metadata['message_id'],
|
||||||
'done': True,
|
{
|
||||||
'role': 'assistant',
|
'done': True,
|
||||||
'content': content,
|
'role': 'assistant',
|
||||||
'output': response_output,
|
'content': content,
|
||||||
**({'usage': usage} if usage else {}),
|
'output': response_output,
|
||||||
},
|
**({'usage': usage} if usage else {}),
|
||||||
)
|
},
|
||||||
|
)
|
||||||
|
|
||||||
# Send a webhook notification if the user is not active
|
# Send a webhook notification if the user is not active
|
||||||
if request.app.state.config.ENABLE_USER_WEBHOOKS and not await Users.is_user_active(user.id):
|
if request.app.state.config.ENABLE_USER_WEBHOOKS and not await Users.is_user_active(user.id):
|
||||||
@@ -4345,7 +4362,7 @@ async def streaming_chat_response_handler(response, ctx):
|
|||||||
if end:
|
if end:
|
||||||
break
|
break
|
||||||
|
|
||||||
if ENABLE_REALTIME_CHAT_SAVE:
|
if ENABLE_REALTIME_CHAT_SAVE and not metadata['chat_id'].startswith('channel:'):
|
||||||
# Save message in the database
|
# Save message in the database
|
||||||
await Chats.upsert_message_to_chat_by_id_and_message_id(
|
await Chats.upsert_message_to_chat_by_id_and_message_id(
|
||||||
metadata['chat_id'],
|
metadata['chat_id'],
|
||||||
@@ -5021,7 +5038,11 @@ async def streaming_chat_response_handler(response, ctx):
|
|||||||
if item.get('status') == 'in_progress':
|
if item.get('status') == 'in_progress':
|
||||||
item['status'] = 'completed'
|
item['status'] = 'completed'
|
||||||
|
|
||||||
title = await Chats.get_chat_title_by_id(metadata['chat_id'])
|
title = (
|
||||||
|
await Chats.get_chat_title_by_id(metadata['chat_id'])
|
||||||
|
if not metadata['chat_id'].startswith('channel:')
|
||||||
|
else ''
|
||||||
|
)
|
||||||
data = {
|
data = {
|
||||||
'done': True,
|
'done': True,
|
||||||
'content': serialize_output(output),
|
'content': serialize_output(output),
|
||||||
@@ -5030,30 +5051,31 @@ async def streaming_chat_response_handler(response, ctx):
|
|||||||
**({'usage': usage} if usage else {}),
|
**({'usage': usage} if usage else {}),
|
||||||
}
|
}
|
||||||
|
|
||||||
if not ENABLE_REALTIME_CHAT_SAVE:
|
if not metadata['chat_id'].startswith('channel:'):
|
||||||
# Save message in the database
|
if not ENABLE_REALTIME_CHAT_SAVE:
|
||||||
await Chats.upsert_message_to_chat_by_id_and_message_id(
|
# Save message in the database
|
||||||
metadata['chat_id'],
|
await Chats.upsert_message_to_chat_by_id_and_message_id(
|
||||||
metadata['message_id'],
|
metadata['chat_id'],
|
||||||
{
|
metadata['message_id'],
|
||||||
'done': True,
|
{
|
||||||
'content': serialize_output(output),
|
'done': True,
|
||||||
'output': output,
|
'content': serialize_output(output),
|
||||||
**({'usage': usage} if usage else {}),
|
'output': output,
|
||||||
},
|
**({'usage': usage} if usage else {}),
|
||||||
)
|
},
|
||||||
elif usage:
|
)
|
||||||
await Chats.upsert_message_to_chat_by_id_and_message_id(
|
elif usage:
|
||||||
metadata['chat_id'],
|
await Chats.upsert_message_to_chat_by_id_and_message_id(
|
||||||
metadata['message_id'],
|
metadata['chat_id'],
|
||||||
{'done': True, 'usage': usage},
|
metadata['message_id'],
|
||||||
)
|
{'done': True, 'usage': usage},
|
||||||
else:
|
)
|
||||||
await Chats.upsert_message_to_chat_by_id_and_message_id(
|
else:
|
||||||
metadata['chat_id'],
|
await Chats.upsert_message_to_chat_by_id_and_message_id(
|
||||||
metadata['message_id'],
|
metadata['chat_id'],
|
||||||
{'done': True},
|
metadata['message_id'],
|
||||||
)
|
{'done': True},
|
||||||
|
)
|
||||||
|
|
||||||
# Send a webhook notification if the user is not active
|
# Send a webhook notification if the user is not active
|
||||||
if request.app.state.config.ENABLE_USER_WEBHOOKS and not await Users.is_user_active(user.id):
|
if request.app.state.config.ENABLE_USER_WEBHOOKS and not await Users.is_user_active(user.id):
|
||||||
@@ -5100,22 +5122,23 @@ async def streaming_chat_response_handler(response, ctx):
|
|||||||
|
|
||||||
async def save_cancelled_state():
|
async def save_cancelled_state():
|
||||||
await event_emitter({'type': 'chat:tasks:cancel'})
|
await event_emitter({'type': 'chat:tasks:cancel'})
|
||||||
if not ENABLE_REALTIME_CHAT_SAVE:
|
if not metadata['chat_id'].startswith('channel:'):
|
||||||
await Chats.upsert_message_to_chat_by_id_and_message_id(
|
if not ENABLE_REALTIME_CHAT_SAVE:
|
||||||
metadata['chat_id'],
|
await Chats.upsert_message_to_chat_by_id_and_message_id(
|
||||||
metadata['message_id'],
|
metadata['chat_id'],
|
||||||
{
|
metadata['message_id'],
|
||||||
'done': True,
|
{
|
||||||
'content': serialize_output(output),
|
'done': True,
|
||||||
'output': output,
|
'content': serialize_output(output),
|
||||||
},
|
'output': output,
|
||||||
)
|
},
|
||||||
else:
|
)
|
||||||
await Chats.upsert_message_to_chat_by_id_and_message_id(
|
else:
|
||||||
metadata['chat_id'],
|
await Chats.upsert_message_to_chat_by_id_and_message_id(
|
||||||
metadata['message_id'],
|
metadata['chat_id'],
|
||||||
{'done': True},
|
metadata['message_id'],
|
||||||
)
|
{'done': True},
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await asyncio.shield(save_cancelled_state())
|
await asyncio.shield(save_cancelled_state())
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ from open_webui.config import (
|
|||||||
from open_webui.constants import ERROR_MESSAGES, WEBHOOK_MESSAGES
|
from open_webui.constants import ERROR_MESSAGES, WEBHOOK_MESSAGES
|
||||||
from open_webui.env import (
|
from open_webui.env import (
|
||||||
AIOHTTP_CLIENT_SESSION_SSL,
|
AIOHTTP_CLIENT_SESSION_SSL,
|
||||||
|
AIOHTTP_CLIENT_ALLOW_REDIRECTS,
|
||||||
WEBUI_NAME,
|
WEBUI_NAME,
|
||||||
WEBUI_AUTH_COOKIE_SAME_SITE,
|
WEBUI_AUTH_COOKIE_SAME_SITE,
|
||||||
WEBUI_AUTH_COOKIE_SECURE,
|
WEBUI_AUTH_COOKIE_SECURE,
|
||||||
@@ -740,7 +741,7 @@ class OAuthClientManager:
|
|||||||
async with aiohttp.ClientSession(trust_env=True) as session:
|
async with aiohttp.ClientSession(trust_env=True) as session:
|
||||||
async with session.get(
|
async with session.get(
|
||||||
authorization_url,
|
authorization_url,
|
||||||
allow_redirects=False,
|
allow_redirects=AIOHTTP_CLIENT_ALLOW_REDIRECTS,
|
||||||
ssl=AIOHTTP_CLIENT_SESSION_SSL,
|
ssl=AIOHTTP_CLIENT_SESSION_SSL,
|
||||||
) as resp:
|
) as resp:
|
||||||
if resp.status < 400:
|
if resp.status < 400:
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ from open_webui.utils.access_control import has_access, has_connection_access
|
|||||||
from open_webui.config import BYPASS_ADMIN_ACCESS_CONTROL
|
from open_webui.config import BYPASS_ADMIN_ACCESS_CONTROL
|
||||||
from open_webui.env import (
|
from open_webui.env import (
|
||||||
AIOHTTP_CLIENT_SESSION_SSL,
|
AIOHTTP_CLIENT_SESSION_SSL,
|
||||||
|
AIOHTTP_CLIENT_ALLOW_REDIRECTS,
|
||||||
AIOHTTP_CLIENT_TIMEOUT,
|
AIOHTTP_CLIENT_TIMEOUT,
|
||||||
AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER,
|
AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER,
|
||||||
AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA,
|
AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA,
|
||||||
@@ -1433,7 +1434,7 @@ async def execute_tool_server(
|
|||||||
headers=headers,
|
headers=headers,
|
||||||
cookies=cookies,
|
cookies=cookies,
|
||||||
ssl=AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL,
|
ssl=AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL,
|
||||||
allow_redirects=False,
|
allow_redirects=AIOHTTP_CLIENT_ALLOW_REDIRECTS,
|
||||||
) as response:
|
) as response:
|
||||||
if response.status >= 400:
|
if response.status >= 400:
|
||||||
text = await response.text()
|
text = await response.text()
|
||||||
@@ -1458,7 +1459,7 @@ async def execute_tool_server(
|
|||||||
headers=headers,
|
headers=headers,
|
||||||
cookies=cookies,
|
cookies=cookies,
|
||||||
ssl=AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL,
|
ssl=AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL,
|
||||||
allow_redirects=False,
|
allow_redirects=AIOHTTP_CLIENT_ALLOW_REDIRECTS,
|
||||||
) as response:
|
) as response:
|
||||||
if response.status >= 400:
|
if response.status >= 400:
|
||||||
text = await response.text()
|
text = await response.text()
|
||||||
|
|||||||
@@ -3,17 +3,13 @@
|
|||||||
import re
|
import re
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
# Matches the OWUI-generated profile image route. ``[^/?#]+`` accepts
|
from open_webui.env import PROFILE_IMAGE_ALLOWED_MIME_TYPES
|
||||||
# any user-ID without allowing path-traversal or query/fragment injection,
|
|
||||||
# and the ``$`` anchor rejects trailing path components.
|
|
||||||
_USER_PROFILE_IMAGE_RE = re.compile(r'^/api/v1/users/[^/?#]+/profile/image$')
|
_USER_PROFILE_IMAGE_RE = re.compile(r'^/api/v1/users/[^/?#]+/profile/image$')
|
||||||
|
|
||||||
# Validates MIME type and structure of base64 data URIs. Only the prefix
|
# Data-URI prefix validator derived from PROFILE_IMAGE_ALLOWED_MIME_TYPES.
|
||||||
# is checked — validating the full base64 payload would mean running a
|
_mime_suffixes = '|'.join(re.escape(t.split('/')[-1]) for t in sorted(PROFILE_IMAGE_ALLOWED_MIME_TYPES))
|
||||||
# regex across megabytes of data on every Pydantic instantiation for zero
|
_SAFE_DATA_URI_RE = re.compile(rf'^data:image/({_mime_suffixes});base64,', re.IGNORECASE)
|
||||||
# security benefit (corrupt base64 simply renders a broken image, same as
|
|
||||||
# a 404 URL). SVG is intentionally excluded: it can carry embedded scripts.
|
|
||||||
_SAFE_DATA_URI_RE = re.compile(r'^data:image/(png|jpeg|gif|webp);base64,', re.IGNORECASE)
|
|
||||||
|
|
||||||
# Exact relative paths accepted as profile images. These are the only
|
# Exact relative paths accepted as profile images. These are the only
|
||||||
# static-asset paths OWUI itself assigns; no prefix/wildcard matching is
|
# static-asset paths OWUI itself assigns; no prefix/wildcard matching is
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "open-webui",
|
"name": "open-webui",
|
||||||
"version": "0.9.4",
|
"version": "0.9.5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "open-webui",
|
"name": "open-webui",
|
||||||
"version": "0.9.4",
|
"version": "0.9.5",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@azure/msal-browser": "^4.5.0",
|
"@azure/msal-browser": "^4.5.0",
|
||||||
"@codemirror/lang-javascript": "^6.2.2",
|
"@codemirror/lang-javascript": "^6.2.2",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "open-webui",
|
"name": "open-webui",
|
||||||
"version": "0.9.4",
|
"version": "0.9.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "npm run pyodide:fetch && vite dev --host",
|
"dev": "npm run pyodide:fetch && vite dev --host",
|
||||||
|
|||||||
@@ -410,6 +410,24 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if permissions.features.calendar}
|
||||||
|
<div class="flex flex-col w-full">
|
||||||
|
<div class="flex w-full justify-between my-1">
|
||||||
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('Calendars Public Sharing')}
|
||||||
|
</div>
|
||||||
|
<Switch bind:state={permissions.sharing.public_calendars} />
|
||||||
|
</div>
|
||||||
|
{#if defaultPermissions?.sharing?.public_calendars && !permissions.sharing.public_calendars}
|
||||||
|
<div>
|
||||||
|
<div class="text-xs text-gray-500">
|
||||||
|
{$i18n.t('This is a default user permission and will remain enabled.')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class=" border-gray-100/30 dark:border-gray-850/30" />
|
<hr class=" border-gray-100/30 dark:border-gray-850/30" />
|
||||||
|
|||||||
@@ -7,12 +7,14 @@
|
|||||||
import {
|
import {
|
||||||
artifactCode,
|
artifactCode,
|
||||||
chatId,
|
chatId,
|
||||||
|
config,
|
||||||
settings,
|
settings,
|
||||||
showArtifacts,
|
showArtifacts,
|
||||||
showControls,
|
showControls,
|
||||||
artifactContents
|
artifactContents
|
||||||
} from '$lib/stores';
|
} from '$lib/stores';
|
||||||
import { copyToClipboard, createMessagesList } from '$lib/utils';
|
import { copyToClipboard, createMessagesList } from '$lib/utils';
|
||||||
|
import { injectCsp } from '$lib/utils/csp';
|
||||||
|
|
||||||
import XMark from '../icons/XMark.svelte';
|
import XMark from '../icons/XMark.svelte';
|
||||||
import ArrowsPointingOut from '../icons/ArrowsPointingOut.svelte';
|
import ArrowsPointingOut from '../icons/ArrowsPointingOut.svelte';
|
||||||
@@ -242,7 +244,10 @@
|
|||||||
<iframe
|
<iframe
|
||||||
bind:this={iframeElement}
|
bind:this={iframeElement}
|
||||||
title="Content"
|
title="Content"
|
||||||
srcdoc={contents[selectedContentIdx].content}
|
srcdoc={injectCsp(
|
||||||
|
contents[selectedContentIdx].content,
|
||||||
|
$config?.ui?.iframe_csp ?? ''
|
||||||
|
)}
|
||||||
class="w-full border-0 h-full rounded-none"
|
class="w-full border-0 h-full rounded-none"
|
||||||
sandbox="allow-scripts allow-downloads{($settings?.iframeSandboxAllowForms ?? false)
|
sandbox="allow-scripts allow-downloads{($settings?.iframeSandboxAllowForms ?? false)
|
||||||
? ' allow-forms'
|
? ' allow-forms'
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
import { getContext, tick } from 'svelte';
|
import { getContext, tick } from 'svelte';
|
||||||
import { marked } from 'marked';
|
import { marked } from 'marked';
|
||||||
import DOMPurify from 'dompurify';
|
import DOMPurify from 'dompurify';
|
||||||
import { settings } from '$lib/stores';
|
import { settings, config } from '$lib/stores';
|
||||||
|
import { injectCsp } from '$lib/utils/csp';
|
||||||
import { isCodeFile } from '$lib/utils/codeHighlight';
|
import { isCodeFile } from '$lib/utils/codeHighlight';
|
||||||
import { initMermaid, renderMermaidDiagram } from '$lib/utils';
|
import { initMermaid, renderMermaidDiagram } from '$lib/utils';
|
||||||
import Spinner from '../../common/Spinner.svelte';
|
import Spinner from '../../common/Spinner.svelte';
|
||||||
@@ -411,7 +412,7 @@
|
|||||||
<div class="absolute top-0 left-0 right-0 bottom-0 z-10"></div>
|
<div class="absolute top-0 left-0 right-0 bottom-0 z-10"></div>
|
||||||
{/if}
|
{/if}
|
||||||
<iframe
|
<iframe
|
||||||
srcdoc={fileContent}
|
srcdoc={injectCsp(fileContent, $config?.ui?.iframe_csp ?? '')}
|
||||||
sandbox="allow-scripts allow-downloads{($settings?.iframeSandboxAllowForms ?? false)
|
sandbox="allow-scripts allow-downloads{($settings?.iframeSandboxAllowForms ?? false)
|
||||||
? ' allow-forms'
|
? ' allow-forms'
|
||||||
: ''}{($settings?.iframeSandboxAllowSameOrigin ?? false) ? ' allow-same-origin' : ''}"
|
: ''}{($settings?.iframeSandboxAllowSameOrigin ?? false) ? ' allow-same-origin' : ''}"
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
import Markdown from '$lib/components/chat/Messages/Markdown.svelte';
|
import Markdown from '$lib/components/chat/Messages/Markdown.svelte';
|
||||||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||||
import { settings } from '$lib/stores';
|
import { settings, config } from '$lib/stores';
|
||||||
|
import { injectCsp } from '$lib/utils/csp';
|
||||||
|
|
||||||
import XMark from '$lib/components/icons/XMark.svelte';
|
import XMark from '$lib/components/icons/XMark.svelte';
|
||||||
import Textarea from '$lib/components/common/Textarea.svelte';
|
import Textarea from '$lib/components/common/Textarea.svelte';
|
||||||
@@ -218,7 +219,7 @@
|
|||||||
false)
|
false)
|
||||||
? ' allow-same-origin'
|
? ' allow-same-origin'
|
||||||
: ''}"
|
: ''}"
|
||||||
srcdoc={document.document}
|
srcdoc={injectCsp(document.document, $config?.ui?.iframe_csp ?? '')}
|
||||||
title={$i18n.t('Content')}
|
title={$i18n.t('Content')}
|
||||||
></iframe>
|
></iframe>
|
||||||
{:else}
|
{:else}
|
||||||
|
|||||||
@@ -15,6 +15,57 @@
|
|||||||
import FloatingButtons from '../ContentRenderer/FloatingButtons.svelte';
|
import FloatingButtons from '../ContentRenderer/FloatingButtons.svelte';
|
||||||
import { createMessagesList } from '$lib/utils';
|
import { createMessagesList } from '$lib/utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts all top-level <details>...</details> blocks from content,
|
||||||
|
* handling nested <details> via depth tracking.
|
||||||
|
* Returns { detailsContent, plainContent }.
|
||||||
|
*/
|
||||||
|
const extractDetailsBlocks = (text) => {
|
||||||
|
const blocks = [];
|
||||||
|
let remaining = text;
|
||||||
|
let result = '';
|
||||||
|
const openTag = '<details';
|
||||||
|
const closeTag = '</details>';
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const start = remaining.indexOf(openTag);
|
||||||
|
if (start === -1) {
|
||||||
|
result += remaining;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
result += remaining.slice(0, start);
|
||||||
|
|
||||||
|
// Find matching closing tag with depth tracking
|
||||||
|
let depth = 1;
|
||||||
|
let idx = start + openTag.length;
|
||||||
|
while (depth > 0 && idx < remaining.length) {
|
||||||
|
if (remaining.startsWith(openTag, idx)) {
|
||||||
|
depth++;
|
||||||
|
} else if (remaining.startsWith(closeTag, idx)) {
|
||||||
|
depth--;
|
||||||
|
}
|
||||||
|
if (depth > 0) idx++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (depth === 0) {
|
||||||
|
const end = idx + closeTag.length;
|
||||||
|
blocks.push(remaining.slice(start, end));
|
||||||
|
remaining = remaining.slice(end);
|
||||||
|
} else {
|
||||||
|
// Unmatched opening tag, treat as plain text
|
||||||
|
result += remaining.slice(start);
|
||||||
|
remaining = '';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
detailsContent: blocks.join('\n'),
|
||||||
|
plainContent: result.trim()
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export let id;
|
export let id;
|
||||||
export let content;
|
export let content;
|
||||||
|
|
||||||
@@ -174,43 +225,55 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div bind:this={contentContainerElement}>
|
<div bind:this={contentContainerElement}>
|
||||||
<Markdown
|
{#if $settings?.renderMarkdownInAssistantMessages ?? true}
|
||||||
{id}
|
<Markdown
|
||||||
content={model?.info?.meta?.capabilities?.citations == false
|
{id}
|
||||||
? content.replace(/\s*(\[(?:\d+(?:#[^,\]\s]+)?(?:,\s*\d+(?:#[^,\]\s]+)?)*)\])+/g, '')
|
content={model?.info?.meta?.capabilities?.citations == false
|
||||||
: content}
|
? content.replace(/\s*(\[(?:\d+(?:#[^,\]\s]+)?(?:,\s*\d+(?:#[^,\]\s]+)?)*)\])+/g, '')
|
||||||
{model}
|
: content}
|
||||||
{save}
|
{model}
|
||||||
{preview}
|
{save}
|
||||||
{done}
|
{preview}
|
||||||
{editCodeBlock}
|
{done}
|
||||||
{topPadding}
|
{editCodeBlock}
|
||||||
{sourceIds}
|
{topPadding}
|
||||||
{onSourceClick}
|
{sourceIds}
|
||||||
{onTaskClick}
|
{onSourceClick}
|
||||||
{onSave}
|
{onTaskClick}
|
||||||
onUpdate={async (token) => {
|
{onSave}
|
||||||
const { lang, text: code } = token;
|
onUpdate={async (token) => {
|
||||||
|
const { lang, text: code } = token;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
($settings?.detectArtifacts ?? true) &&
|
($settings?.detectArtifacts ?? true) &&
|
||||||
(['html', 'svg'].includes(lang) || (lang === 'xml' && code.includes('svg'))) &&
|
(['html', 'svg'].includes(lang) || (lang === 'xml' && code.includes('svg'))) &&
|
||||||
!$mobile &&
|
!$mobile &&
|
||||||
$chatId
|
$chatId
|
||||||
) {
|
) {
|
||||||
await tick();
|
await tick();
|
||||||
showArtifacts.set(true);
|
showArtifacts.set(true);
|
||||||
showControls.set(true);
|
showControls.set(true);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onPreview={async (value) => {
|
onPreview={async (value) => {
|
||||||
console.log('Preview', value);
|
console.log('Preview', value);
|
||||||
await artifactCode.set(value);
|
await artifactCode.set(value);
|
||||||
await showControls.set(true);
|
await showControls.set(true);
|
||||||
await showArtifacts.set(true);
|
await showArtifacts.set(true);
|
||||||
await showEmbeds.set(false);
|
await showEmbeds.set(false);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{:else}
|
||||||
|
{@const extracted = extractDetailsBlocks(content)}
|
||||||
|
|
||||||
|
{#if extracted.detailsContent}
|
||||||
|
<!-- Render structural blocks (tool calls, reasoning, etc.) through Markdown -->
|
||||||
|
<Markdown {id} content={extracted.detailsContent} {done} />
|
||||||
|
{/if}
|
||||||
|
{#if extracted.plainContent}
|
||||||
|
<div class="whitespace-pre-wrap">{extracted.plainContent}</div>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if floatingButtons}
|
{#if floatingButtons}
|
||||||
|
|||||||
@@ -376,12 +376,18 @@
|
|||||||
: ' w-full'}"
|
: ' w-full'}"
|
||||||
>
|
>
|
||||||
{#if message.content}
|
{#if message.content}
|
||||||
<Markdown
|
{#if $settings?.renderMarkdownInUserMessages ?? true}
|
||||||
id={`${chatId}-${message.id}`}
|
<Markdown
|
||||||
content={message.content}
|
id={`${chatId}-${message.id}`}
|
||||||
{editCodeBlock}
|
content={message.content}
|
||||||
{topPadding}
|
{editCodeBlock}
|
||||||
/>
|
{topPadding}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<div class="whitespace-pre-wrap" dir={$settings?.chatDirection ?? 'auto'}>
|
||||||
|
{message.content}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -69,6 +69,8 @@
|
|||||||
let temporaryChatByDefault = false;
|
let temporaryChatByDefault = false;
|
||||||
let chatFadeStreamingText = true;
|
let chatFadeStreamingText = true;
|
||||||
let collapseCodeBlocks = false;
|
let collapseCodeBlocks = false;
|
||||||
|
let renderMarkdownInUserMessages = true;
|
||||||
|
let renderMarkdownInAssistantMessages = true;
|
||||||
let expandDetails = false;
|
let expandDetails = false;
|
||||||
let renderMarkdownInPreviews = true;
|
let renderMarkdownInPreviews = true;
|
||||||
let showChatTitleInTab = true;
|
let showChatTitleInTab = true;
|
||||||
@@ -232,6 +234,8 @@
|
|||||||
copyFormatted = $settings?.copyFormatted ?? false;
|
copyFormatted = $settings?.copyFormatted ?? false;
|
||||||
|
|
||||||
collapseCodeBlocks = $settings?.collapseCodeBlocks ?? false;
|
collapseCodeBlocks = $settings?.collapseCodeBlocks ?? false;
|
||||||
|
renderMarkdownInUserMessages = $settings?.renderMarkdownInUserMessages ?? true;
|
||||||
|
renderMarkdownInAssistantMessages = $settings?.renderMarkdownInAssistantMessages ?? true;
|
||||||
expandDetails = $settings?.expandDetails ?? false;
|
expandDetails = $settings?.expandDetails ?? false;
|
||||||
renderMarkdownInPreviews = $settings?.renderMarkdownInPreviews ?? true;
|
renderMarkdownInPreviews = $settings?.renderMarkdownInPreviews ?? true;
|
||||||
|
|
||||||
@@ -776,6 +780,44 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class=" py-0.5 flex w-full justify-between">
|
||||||
|
<div id="render-markdown-user-label" class=" self-center text-xs">
|
||||||
|
{$i18n.t('Render Markdown in User Messages')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2 p-1">
|
||||||
|
<Switch
|
||||||
|
ariaLabelledbyId="render-markdown-user-label"
|
||||||
|
tooltip={true}
|
||||||
|
bind:state={renderMarkdownInUserMessages}
|
||||||
|
on:change={() => {
|
||||||
|
saveSettings({ renderMarkdownInUserMessages });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class=" py-0.5 flex w-full justify-between">
|
||||||
|
<div id="render-markdown-assistant-label" class=" self-center text-xs">
|
||||||
|
{$i18n.t('Render Markdown in Assistant Messages')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2 p-1">
|
||||||
|
<Switch
|
||||||
|
ariaLabelledbyId="render-markdown-assistant-label"
|
||||||
|
tooltip={true}
|
||||||
|
bind:state={renderMarkdownInAssistantMessages}
|
||||||
|
on:change={() => {
|
||||||
|
saveSettings({ renderMarkdownInAssistantMessages });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" py-0.5 flex w-full justify-between">
|
<div class=" py-0.5 flex w-full justify-between">
|
||||||
<div id="auto-generation-label" class=" self-center text-xs">
|
<div id="auto-generation-label" class=" self-center text-xs">
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onDestroy, onMount, tick } from 'svelte';
|
import { onDestroy, onMount, tick } from 'svelte';
|
||||||
|
import { config } from '$lib/stores';
|
||||||
|
import { injectCsp } from '$lib/utils/csp';
|
||||||
|
|
||||||
// Props
|
// Props
|
||||||
export let src: string | null = null; // URL or raw HTML (auto-detected)
|
export let src: string | null = null; // URL or raw HTML (auto-detected)
|
||||||
@@ -192,7 +194,7 @@ window.Chart = parent.Chart; // Chart previously assigned on parent
|
|||||||
{#if iframeDoc}
|
{#if iframeDoc}
|
||||||
<iframe
|
<iframe
|
||||||
bind:this={iframe}
|
bind:this={iframe}
|
||||||
srcdoc={iframeDoc}
|
srcdoc={injectCsp(iframeDoc, $config?.ui?.iframe_csp ?? '')}
|
||||||
{title}
|
{title}
|
||||||
class={iframeClassName}
|
class={iframeClassName}
|
||||||
style={`${initialHeight ? `height:${initialHeight}px;` : ''}`}
|
style={`${initialHeight ? `height:${initialHeight}px;` : ''}`}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getContext, onMount } from 'svelte';
|
import { getContext } from 'svelte';
|
||||||
import Checkbox from '$lib/components/common/Checkbox.svelte';
|
import Checkbox from '$lib/components/common/Checkbox.svelte';
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
|
|
||||||
@@ -7,19 +7,6 @@
|
|||||||
|
|
||||||
export let filters = [];
|
export let filters = [];
|
||||||
export let selectedFilterIds = [];
|
export let selectedFilterIds = [];
|
||||||
|
|
||||||
let _filters = {};
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
_filters = filters.reduce((acc, filter) => {
|
|
||||||
acc[filter.id] = {
|
|
||||||
...filter,
|
|
||||||
selected: selectedFilterIds.includes(filter.id)
|
|
||||||
};
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -30,21 +17,27 @@
|
|||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
{#if filters.length > 0}
|
{#if filters.length > 0}
|
||||||
<div class=" flex items-center flex-wrap">
|
<div class=" flex items-center flex-wrap">
|
||||||
{#each Object.keys(_filters) as filter, filterIdx}
|
{#each filters as filter}
|
||||||
|
{@const isSelected = selectedFilterIds.includes(filter.id)}
|
||||||
<div class=" flex items-center gap-2 mr-3">
|
<div class=" flex items-center gap-2 mr-3">
|
||||||
<div class="self-center flex items-center">
|
<div class="self-center flex items-center">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
state={_filters[filter].selected ? 'checked' : 'unchecked'}
|
state={isSelected ? 'checked' : 'unchecked'}
|
||||||
on:change={(e) => {
|
on:change={(e) => {
|
||||||
_filters[filter].selected = e.detail === 'checked';
|
if (e.detail === 'checked') {
|
||||||
selectedFilterIds = Object.keys(_filters).filter((t) => _filters[t].selected);
|
if (!selectedFilterIds.includes(filter.id)) {
|
||||||
|
selectedFilterIds = [...selectedFilterIds, filter.id];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
selectedFilterIds = selectedFilterIds.filter((id) => id !== filter.id);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" py-0.5 text-sm w-full capitalize font-medium">
|
<div class=" py-0.5 text-sm w-full capitalize font-medium">
|
||||||
<Tooltip content={_filters[filter].meta.description}>
|
<Tooltip content={filter.meta.description}>
|
||||||
{_filters[filter].name}
|
{filter.name}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getContext, onMount } from 'svelte';
|
import { getContext } from 'svelte';
|
||||||
import Checkbox from '$lib/components/common/Checkbox.svelte';
|
import Checkbox from '$lib/components/common/Checkbox.svelte';
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
|
|
||||||
@@ -7,19 +7,6 @@
|
|||||||
|
|
||||||
export let filters = [];
|
export let filters = [];
|
||||||
export let selectedFilterIds = [];
|
export let selectedFilterIds = [];
|
||||||
|
|
||||||
let _filters = {};
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
_filters = filters.reduce((acc, filter) => {
|
|
||||||
acc[filter.id] = {
|
|
||||||
...filter,
|
|
||||||
selected: selectedFilterIds.includes(filter.id)
|
|
||||||
};
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if filters.length > 0}
|
{#if filters.length > 0}
|
||||||
@@ -28,31 +15,33 @@
|
|||||||
<div class=" self-center text-xs font-medium text-gray-500">{$i18n.t('Filters')}</div>
|
<div class=" self-center text-xs font-medium text-gray-500">{$i18n.t('Filters')}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- TODO: Filer order matters -->
|
<!-- TODO: Filter order matters -->
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<div class=" flex items-center flex-wrap">
|
<div class=" flex items-center flex-wrap">
|
||||||
{#each Object.keys(_filters) as filter, filterIdx}
|
{#each filters as filter}
|
||||||
|
{@const isSelected = filter.is_global || selectedFilterIds.includes(filter.id)}
|
||||||
<div class=" flex items-center gap-2 mr-3">
|
<div class=" flex items-center gap-2 mr-3">
|
||||||
<div class="self-center flex items-center">
|
<div class="self-center flex items-center">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
state={_filters[filter].is_global
|
state={isSelected ? 'checked' : 'unchecked'}
|
||||||
? 'checked'
|
disabled={filter.is_global}
|
||||||
: _filters[filter].selected
|
|
||||||
? 'checked'
|
|
||||||
: 'unchecked'}
|
|
||||||
disabled={_filters[filter].is_global}
|
|
||||||
on:change={(e) => {
|
on:change={(e) => {
|
||||||
if (!_filters[filter].is_global) {
|
if (filter.is_global) return;
|
||||||
_filters[filter].selected = e.detail === 'checked';
|
|
||||||
selectedFilterIds = Object.keys(_filters).filter((t) => _filters[t].selected);
|
if (e.detail === 'checked') {
|
||||||
|
if (!selectedFilterIds.includes(filter.id)) {
|
||||||
|
selectedFilterIds = [...selectedFilterIds, filter.id];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
selectedFilterIds = selectedFilterIds.filter((id) => id !== filter.id);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" py-0.5 text-sm w-full capitalize font-medium">
|
<div class=" py-0.5 text-sm w-full capitalize font-medium">
|
||||||
<Tooltip content={_filters[filter].meta.description}>
|
<Tooltip content={filter.meta.description}>
|
||||||
{_filters[filter].name}
|
{filter.name}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ export const DEFAULT_PERMISSIONS = {
|
|||||||
public_skills: false,
|
public_skills: false,
|
||||||
notes: false,
|
notes: false,
|
||||||
public_notes: false,
|
public_notes: false,
|
||||||
public_chats: false
|
public_chats: false,
|
||||||
|
public_calendars: false
|
||||||
},
|
},
|
||||||
access_grants: {
|
access_grants: {
|
||||||
allow_users: true
|
allow_users: true
|
||||||
@@ -62,7 +63,8 @@ export const DEFAULT_PERMISSIONS = {
|
|||||||
image_generation: true,
|
image_generation: true,
|
||||||
code_interpreter: true,
|
code_interpreter: true,
|
||||||
memories: true,
|
memories: true,
|
||||||
automations: false
|
automations: false,
|
||||||
|
calendar: true
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
interface: true
|
interface: true
|
||||||
|
|||||||
@@ -288,6 +288,7 @@
|
|||||||
"Calendar deleted": "",
|
"Calendar deleted": "",
|
||||||
"Calendar name": "",
|
"Calendar name": "",
|
||||||
"Calendars": "",
|
"Calendars": "",
|
||||||
|
"Calendars Public Sharing": "",
|
||||||
"Call": "",
|
"Call": "",
|
||||||
"Call feature is not supported when using Web STT engine": "",
|
"Call feature is not supported when using Web STT engine": "",
|
||||||
"Camera": "",
|
"Camera": "",
|
||||||
@@ -1700,7 +1701,9 @@
|
|||||||
"Remove Model": "حذف الموديل",
|
"Remove Model": "حذف الموديل",
|
||||||
"Rename": "إعادة تسمية",
|
"Rename": "إعادة تسمية",
|
||||||
"Renamed to {{name}}": "",
|
"Renamed to {{name}}": "",
|
||||||
|
"Render Markdown in Assistant Messages": "",
|
||||||
"Render Markdown in Previews": "",
|
"Render Markdown in Previews": "",
|
||||||
|
"Render Markdown in User Messages": "",
|
||||||
"Reorder Models": "",
|
"Reorder Models": "",
|
||||||
"Repeats": "",
|
"Repeats": "",
|
||||||
"Reply": "",
|
"Reply": "",
|
||||||
|
|||||||
@@ -288,6 +288,7 @@
|
|||||||
"Calendar deleted": "",
|
"Calendar deleted": "",
|
||||||
"Calendar name": "",
|
"Calendar name": "",
|
||||||
"Calendars": "",
|
"Calendars": "",
|
||||||
|
"Calendars Public Sharing": "",
|
||||||
"Call": "مكالمة",
|
"Call": "مكالمة",
|
||||||
"Call feature is not supported when using Web STT engine": "ميزة الاتصال غير مدعومة عند استخدام محرك Web STT",
|
"Call feature is not supported when using Web STT engine": "ميزة الاتصال غير مدعومة عند استخدام محرك Web STT",
|
||||||
"Camera": "الكاميرا",
|
"Camera": "الكاميرا",
|
||||||
@@ -1700,7 +1701,9 @@
|
|||||||
"Remove Model": "حذف الموديل",
|
"Remove Model": "حذف الموديل",
|
||||||
"Rename": "إعادة تسمية",
|
"Rename": "إعادة تسمية",
|
||||||
"Renamed to {{name}}": "",
|
"Renamed to {{name}}": "",
|
||||||
|
"Render Markdown in Assistant Messages": "",
|
||||||
"Render Markdown in Previews": "",
|
"Render Markdown in Previews": "",
|
||||||
|
"Render Markdown in User Messages": "",
|
||||||
"Reorder Models": "إعادة ترتيب النماذج",
|
"Reorder Models": "إعادة ترتيب النماذج",
|
||||||
"Repeats": "",
|
"Repeats": "",
|
||||||
"Reply": "",
|
"Reply": "",
|
||||||
|
|||||||
@@ -284,6 +284,7 @@
|
|||||||
"Calendar deleted": "",
|
"Calendar deleted": "",
|
||||||
"Calendar name": "",
|
"Calendar name": "",
|
||||||
"Calendars": "",
|
"Calendars": "",
|
||||||
|
"Calendars Public Sharing": "",
|
||||||
"Call": "Zəng",
|
"Call": "Zəng",
|
||||||
"Call feature is not supported when using Web STT engine": "Veb STT mühərriki istifadə edildikdə zəng funksiyası dəstəklənmir",
|
"Call feature is not supported when using Web STT engine": "Veb STT mühərriki istifadə edildikdə zəng funksiyası dəstəklənmir",
|
||||||
"Camera": "Kamera",
|
"Camera": "Kamera",
|
||||||
@@ -1696,7 +1697,9 @@
|
|||||||
"Remove Model": "Modeli Sil",
|
"Remove Model": "Modeli Sil",
|
||||||
"Rename": "Adını dəyiş",
|
"Rename": "Adını dəyiş",
|
||||||
"Renamed to {{name}}": "",
|
"Renamed to {{name}}": "",
|
||||||
|
"Render Markdown in Assistant Messages": "",
|
||||||
"Render Markdown in Previews": "Önizləmələrdə Markdown-u emal et",
|
"Render Markdown in Previews": "Önizləmələrdə Markdown-u emal et",
|
||||||
|
"Render Markdown in User Messages": "",
|
||||||
"Reorder Models": "Modelləri yenidən sırala",
|
"Reorder Models": "Modelləri yenidən sırala",
|
||||||
"Repeats": "",
|
"Repeats": "",
|
||||||
"Reply": "Cavabla",
|
"Reply": "Cavabla",
|
||||||
|
|||||||
@@ -284,6 +284,7 @@
|
|||||||
"Calendar deleted": "",
|
"Calendar deleted": "",
|
||||||
"Calendar name": "",
|
"Calendar name": "",
|
||||||
"Calendars": "",
|
"Calendars": "",
|
||||||
|
"Calendars Public Sharing": "",
|
||||||
"Call": "Обаждане",
|
"Call": "Обаждане",
|
||||||
"Call feature is not supported when using Web STT engine": "Функцията за обаждане не се поддържа при използването на Web STT двигател",
|
"Call feature is not supported when using Web STT engine": "Функцията за обаждане не се поддържа при използването на Web STT двигател",
|
||||||
"Camera": "Камера",
|
"Camera": "Камера",
|
||||||
@@ -1696,7 +1697,9 @@
|
|||||||
"Remove Model": "Изтриване на модела",
|
"Remove Model": "Изтриване на модела",
|
||||||
"Rename": "Преименуване",
|
"Rename": "Преименуване",
|
||||||
"Renamed to {{name}}": "",
|
"Renamed to {{name}}": "",
|
||||||
|
"Render Markdown in Assistant Messages": "",
|
||||||
"Render Markdown in Previews": "",
|
"Render Markdown in Previews": "",
|
||||||
|
"Render Markdown in User Messages": "",
|
||||||
"Reorder Models": "Преорганизиране на моделите",
|
"Reorder Models": "Преорганизиране на моделите",
|
||||||
"Repeats": "",
|
"Repeats": "",
|
||||||
"Reply": "",
|
"Reply": "",
|
||||||
|
|||||||
@@ -284,6 +284,7 @@
|
|||||||
"Calendar deleted": "",
|
"Calendar deleted": "",
|
||||||
"Calendar name": "",
|
"Calendar name": "",
|
||||||
"Calendars": "",
|
"Calendars": "",
|
||||||
|
"Calendars Public Sharing": "",
|
||||||
"Call": "",
|
"Call": "",
|
||||||
"Call feature is not supported when using Web STT engine": "",
|
"Call feature is not supported when using Web STT engine": "",
|
||||||
"Camera": "",
|
"Camera": "",
|
||||||
@@ -1696,7 +1697,9 @@
|
|||||||
"Remove Model": "মডেল রিমুভ করুন",
|
"Remove Model": "মডেল রিমুভ করুন",
|
||||||
"Rename": "রেনেম",
|
"Rename": "রেনেম",
|
||||||
"Renamed to {{name}}": "",
|
"Renamed to {{name}}": "",
|
||||||
|
"Render Markdown in Assistant Messages": "",
|
||||||
"Render Markdown in Previews": "",
|
"Render Markdown in Previews": "",
|
||||||
|
"Render Markdown in User Messages": "",
|
||||||
"Reorder Models": "",
|
"Reorder Models": "",
|
||||||
"Repeats": "",
|
"Repeats": "",
|
||||||
"Reply": "",
|
"Reply": "",
|
||||||
|
|||||||
@@ -283,6 +283,7 @@
|
|||||||
"Calendar deleted": "",
|
"Calendar deleted": "",
|
||||||
"Calendar name": "",
|
"Calendar name": "",
|
||||||
"Calendars": "",
|
"Calendars": "",
|
||||||
|
"Calendars Public Sharing": "",
|
||||||
"Call": "སྐད་འབོད།",
|
"Call": "སྐད་འབོད།",
|
||||||
"Call feature is not supported when using Web STT engine": "Web STT མ་ལག་སྤྱོད་སྐབས་སྐད་འབོད་ཀྱི་ཁྱད་ཆོས་ལ་རྒྱབ་སྐྱོར་མེད།",
|
"Call feature is not supported when using Web STT engine": "Web STT མ་ལག་སྤྱོད་སྐབས་སྐད་འབོད་ཀྱི་ཁྱད་ཆོས་ལ་རྒྱབ་སྐྱོར་མེད།",
|
||||||
"Camera": "པར་ཆས།",
|
"Camera": "པར་ཆས།",
|
||||||
@@ -1695,7 +1696,9 @@
|
|||||||
"Remove Model": "དཔེ་དབྱིབས་འདོར་བ།",
|
"Remove Model": "དཔེ་དབྱིབས་འདོར་བ།",
|
||||||
"Rename": "མིང་བསྐྱར་འདོགས།",
|
"Rename": "མིང་བསྐྱར་འདོགས།",
|
||||||
"Renamed to {{name}}": "",
|
"Renamed to {{name}}": "",
|
||||||
|
"Render Markdown in Assistant Messages": "",
|
||||||
"Render Markdown in Previews": "",
|
"Render Markdown in Previews": "",
|
||||||
|
"Render Markdown in User Messages": "",
|
||||||
"Reorder Models": "དཔེ་དབྱིབས་བསྐྱར་སྒྲིག",
|
"Reorder Models": "དཔེ་དབྱིབས་བསྐྱར་སྒྲིག",
|
||||||
"Repeats": "",
|
"Repeats": "",
|
||||||
"Reply": "",
|
"Reply": "",
|
||||||
|
|||||||
@@ -285,6 +285,7 @@
|
|||||||
"Calendar deleted": "",
|
"Calendar deleted": "",
|
||||||
"Calendar name": "",
|
"Calendar name": "",
|
||||||
"Calendars": "",
|
"Calendars": "",
|
||||||
|
"Calendars Public Sharing": "",
|
||||||
"Call": "Poziv",
|
"Call": "Poziv",
|
||||||
"Call feature is not supported when using Web STT engine": "Značajka poziva nije podržana kada se koristi Web STT mehanizam",
|
"Call feature is not supported when using Web STT engine": "Značajka poziva nije podržana kada se koristi Web STT mehanizam",
|
||||||
"Camera": "Kamera",
|
"Camera": "Kamera",
|
||||||
@@ -1697,7 +1698,9 @@
|
|||||||
"Remove Model": "Ukloni model",
|
"Remove Model": "Ukloni model",
|
||||||
"Rename": "Preimenuj",
|
"Rename": "Preimenuj",
|
||||||
"Renamed to {{name}}": "",
|
"Renamed to {{name}}": "",
|
||||||
|
"Render Markdown in Assistant Messages": "",
|
||||||
"Render Markdown in Previews": "",
|
"Render Markdown in Previews": "",
|
||||||
|
"Render Markdown in User Messages": "",
|
||||||
"Reorder Models": "",
|
"Reorder Models": "",
|
||||||
"Repeats": "",
|
"Repeats": "",
|
||||||
"Reply": "",
|
"Reply": "",
|
||||||
|
|||||||
@@ -285,6 +285,7 @@
|
|||||||
"Calendar deleted": "Calendari eliminat",
|
"Calendar deleted": "Calendari eliminat",
|
||||||
"Calendar name": "",
|
"Calendar name": "",
|
||||||
"Calendars": "Calendaris",
|
"Calendars": "Calendaris",
|
||||||
|
"Calendars Public Sharing": "",
|
||||||
"Call": "Trucada",
|
"Call": "Trucada",
|
||||||
"Call feature is not supported when using Web STT engine": "La funció de trucada no s'admet quan s'utilitza el motor Web STT",
|
"Call feature is not supported when using Web STT engine": "La funció de trucada no s'admet quan s'utilitza el motor Web STT",
|
||||||
"Camera": "Càmera",
|
"Camera": "Càmera",
|
||||||
@@ -1697,7 +1698,9 @@
|
|||||||
"Remove Model": "Eliminar el model",
|
"Remove Model": "Eliminar el model",
|
||||||
"Rename": "Canviar el nom",
|
"Rename": "Canviar el nom",
|
||||||
"Renamed to {{name}}": "S'ha renombrat a {{name}}",
|
"Renamed to {{name}}": "S'ha renombrat a {{name}}",
|
||||||
|
"Render Markdown in Assistant Messages": "",
|
||||||
"Render Markdown in Previews": "Compila el Markdown a les previsualitzacions",
|
"Render Markdown in Previews": "Compila el Markdown a les previsualitzacions",
|
||||||
|
"Render Markdown in User Messages": "",
|
||||||
"Reorder Models": "Reordenar els models",
|
"Reorder Models": "Reordenar els models",
|
||||||
"Repeats": "Repeticions",
|
"Repeats": "Repeticions",
|
||||||
"Reply": "Respondre",
|
"Reply": "Respondre",
|
||||||
|
|||||||
@@ -284,6 +284,7 @@
|
|||||||
"Calendar deleted": "",
|
"Calendar deleted": "",
|
||||||
"Calendar name": "",
|
"Calendar name": "",
|
||||||
"Calendars": "",
|
"Calendars": "",
|
||||||
|
"Calendars Public Sharing": "",
|
||||||
"Call": "",
|
"Call": "",
|
||||||
"Call feature is not supported when using Web STT engine": "",
|
"Call feature is not supported when using Web STT engine": "",
|
||||||
"Camera": "",
|
"Camera": "",
|
||||||
@@ -1696,7 +1697,9 @@
|
|||||||
"Remove Model": "",
|
"Remove Model": "",
|
||||||
"Rename": "",
|
"Rename": "",
|
||||||
"Renamed to {{name}}": "",
|
"Renamed to {{name}}": "",
|
||||||
|
"Render Markdown in Assistant Messages": "",
|
||||||
"Render Markdown in Previews": "",
|
"Render Markdown in Previews": "",
|
||||||
|
"Render Markdown in User Messages": "",
|
||||||
"Reorder Models": "",
|
"Reorder Models": "",
|
||||||
"Repeats": "",
|
"Repeats": "",
|
||||||
"Reply": "",
|
"Reply": "",
|
||||||
|
|||||||
@@ -286,6 +286,7 @@
|
|||||||
"Calendar deleted": "",
|
"Calendar deleted": "",
|
||||||
"Calendar name": "",
|
"Calendar name": "",
|
||||||
"Calendars": "",
|
"Calendars": "",
|
||||||
|
"Calendars Public Sharing": "",
|
||||||
"Call": "Volání",
|
"Call": "Volání",
|
||||||
"Call feature is not supported when using Web STT engine": "Funkce volání není podporována při použití webového STT jádra.",
|
"Call feature is not supported when using Web STT engine": "Funkce volání není podporována při použití webového STT jádra.",
|
||||||
"Camera": "Kamera",
|
"Camera": "Kamera",
|
||||||
@@ -1698,7 +1699,9 @@
|
|||||||
"Remove Model": "Odebrat model",
|
"Remove Model": "Odebrat model",
|
||||||
"Rename": "Přejmenovat",
|
"Rename": "Přejmenovat",
|
||||||
"Renamed to {{name}}": "",
|
"Renamed to {{name}}": "",
|
||||||
|
"Render Markdown in Assistant Messages": "",
|
||||||
"Render Markdown in Previews": "",
|
"Render Markdown in Previews": "",
|
||||||
|
"Render Markdown in User Messages": "",
|
||||||
"Reorder Models": "Změnit pořadí modelů",
|
"Reorder Models": "Změnit pořadí modelů",
|
||||||
"Repeats": "",
|
"Repeats": "",
|
||||||
"Reply": "",
|
"Reply": "",
|
||||||
|
|||||||
@@ -284,6 +284,7 @@
|
|||||||
"Calendar deleted": "",
|
"Calendar deleted": "",
|
||||||
"Calendar name": "",
|
"Calendar name": "",
|
||||||
"Calendars": "",
|
"Calendars": "",
|
||||||
|
"Calendars Public Sharing": "",
|
||||||
"Call": "Opkald",
|
"Call": "Opkald",
|
||||||
"Call feature is not supported when using Web STT engine": "Opkaldsfunktion er ikke understøttet for Web STT engine",
|
"Call feature is not supported when using Web STT engine": "Opkaldsfunktion er ikke understøttet for Web STT engine",
|
||||||
"Camera": "Kamera",
|
"Camera": "Kamera",
|
||||||
@@ -1696,7 +1697,9 @@
|
|||||||
"Remove Model": "Fjern model",
|
"Remove Model": "Fjern model",
|
||||||
"Rename": "Omdøb",
|
"Rename": "Omdøb",
|
||||||
"Renamed to {{name}}": "",
|
"Renamed to {{name}}": "",
|
||||||
|
"Render Markdown in Assistant Messages": "",
|
||||||
"Render Markdown in Previews": "",
|
"Render Markdown in Previews": "",
|
||||||
|
"Render Markdown in User Messages": "",
|
||||||
"Reorder Models": "Omarranger modeller",
|
"Reorder Models": "Omarranger modeller",
|
||||||
"Repeats": "",
|
"Repeats": "",
|
||||||
"Reply": "Svar",
|
"Reply": "Svar",
|
||||||
|
|||||||
@@ -284,6 +284,7 @@
|
|||||||
"Calendar deleted": "",
|
"Calendar deleted": "",
|
||||||
"Calendar name": "",
|
"Calendar name": "",
|
||||||
"Calendars": "",
|
"Calendars": "",
|
||||||
|
"Calendars Public Sharing": "",
|
||||||
"Call": "Anruf",
|
"Call": "Anruf",
|
||||||
"Call feature is not supported when using Web STT engine": "Die Anruffunktion wird bei Verwendung der Web-STT-Engine nicht unterstützt.",
|
"Call feature is not supported when using Web STT engine": "Die Anruffunktion wird bei Verwendung der Web-STT-Engine nicht unterstützt.",
|
||||||
"Camera": "Kamera",
|
"Camera": "Kamera",
|
||||||
@@ -1696,7 +1697,9 @@
|
|||||||
"Remove Model": "Modell entfernen",
|
"Remove Model": "Modell entfernen",
|
||||||
"Rename": "Umbenennen",
|
"Rename": "Umbenennen",
|
||||||
"Renamed to {{name}}": "In {{name}} umbenannt",
|
"Renamed to {{name}}": "In {{name}} umbenannt",
|
||||||
|
"Render Markdown in Assistant Messages": "",
|
||||||
"Render Markdown in Previews": "Markdown in der Vorschau rendern",
|
"Render Markdown in Previews": "Markdown in der Vorschau rendern",
|
||||||
|
"Render Markdown in User Messages": "",
|
||||||
"Reorder Models": "Modelle neu anordnen",
|
"Reorder Models": "Modelle neu anordnen",
|
||||||
"Repeats": "Wiederholen",
|
"Repeats": "Wiederholen",
|
||||||
"Reply": "Antworten",
|
"Reply": "Antworten",
|
||||||
|
|||||||
@@ -284,6 +284,7 @@
|
|||||||
"Calendar deleted": "",
|
"Calendar deleted": "",
|
||||||
"Calendar name": "",
|
"Calendar name": "",
|
||||||
"Calendars": "",
|
"Calendars": "",
|
||||||
|
"Calendars Public Sharing": "",
|
||||||
"Call": "",
|
"Call": "",
|
||||||
"Call feature is not supported when using Web STT engine": "",
|
"Call feature is not supported when using Web STT engine": "",
|
||||||
"Camera": "",
|
"Camera": "",
|
||||||
@@ -1696,7 +1697,9 @@
|
|||||||
"Remove Model": "",
|
"Remove Model": "",
|
||||||
"Rename": "",
|
"Rename": "",
|
||||||
"Renamed to {{name}}": "",
|
"Renamed to {{name}}": "",
|
||||||
|
"Render Markdown in Assistant Messages": "",
|
||||||
"Render Markdown in Previews": "",
|
"Render Markdown in Previews": "",
|
||||||
|
"Render Markdown in User Messages": "",
|
||||||
"Reorder Models": "",
|
"Reorder Models": "",
|
||||||
"Repeats": "",
|
"Repeats": "",
|
||||||
"Reply": "",
|
"Reply": "",
|
||||||
|
|||||||
@@ -284,6 +284,7 @@
|
|||||||
"Calendar deleted": "",
|
"Calendar deleted": "",
|
||||||
"Calendar name": "",
|
"Calendar name": "",
|
||||||
"Calendars": "",
|
"Calendars": "",
|
||||||
|
"Calendars Public Sharing": "",
|
||||||
"Call": "Κλήση",
|
"Call": "Κλήση",
|
||||||
"Call feature is not supported when using Web STT engine": "Η λειτουργία κλήσης δεν υποστηρίζεται όταν χρησιμοποιείται η μηχανή Web STT",
|
"Call feature is not supported when using Web STT engine": "Η λειτουργία κλήσης δεν υποστηρίζεται όταν χρησιμοποιείται η μηχανή Web STT",
|
||||||
"Camera": "Κάμερα",
|
"Camera": "Κάμερα",
|
||||||
@@ -1696,7 +1697,9 @@
|
|||||||
"Remove Model": "Αφαίρεση Μοντέλου",
|
"Remove Model": "Αφαίρεση Μοντέλου",
|
||||||
"Rename": "Μετονομασία",
|
"Rename": "Μετονομασία",
|
||||||
"Renamed to {{name}}": "",
|
"Renamed to {{name}}": "",
|
||||||
|
"Render Markdown in Assistant Messages": "",
|
||||||
"Render Markdown in Previews": "",
|
"Render Markdown in Previews": "",
|
||||||
|
"Render Markdown in User Messages": "",
|
||||||
"Reorder Models": "Επαναταξινόμηση Μοντέλων",
|
"Reorder Models": "Επαναταξινόμηση Μοντέλων",
|
||||||
"Repeats": "",
|
"Repeats": "",
|
||||||
"Reply": "Απάντηση",
|
"Reply": "Απάντηση",
|
||||||
|
|||||||
@@ -284,6 +284,7 @@
|
|||||||
"Calendar deleted": "",
|
"Calendar deleted": "",
|
||||||
"Calendar name": "",
|
"Calendar name": "",
|
||||||
"Calendars": "",
|
"Calendars": "",
|
||||||
|
"Calendars Public Sharing": "",
|
||||||
"Call": "",
|
"Call": "",
|
||||||
"Call feature is not supported when using Web STT engine": "",
|
"Call feature is not supported when using Web STT engine": "",
|
||||||
"Camera": "",
|
"Camera": "",
|
||||||
@@ -1696,7 +1697,9 @@
|
|||||||
"Remove Model": "",
|
"Remove Model": "",
|
||||||
"Rename": "",
|
"Rename": "",
|
||||||
"Renamed to {{name}}": "",
|
"Renamed to {{name}}": "",
|
||||||
|
"Render Markdown in Assistant Messages": "",
|
||||||
"Render Markdown in Previews": "",
|
"Render Markdown in Previews": "",
|
||||||
|
"Render Markdown in User Messages": "",
|
||||||
"Reorder Models": "",
|
"Reorder Models": "",
|
||||||
"Repeats": "",
|
"Repeats": "",
|
||||||
"Reply": "",
|
"Reply": "",
|
||||||
|
|||||||
@@ -284,6 +284,7 @@
|
|||||||
"Calendar deleted": "",
|
"Calendar deleted": "",
|
||||||
"Calendar name": "",
|
"Calendar name": "",
|
||||||
"Calendars": "",
|
"Calendars": "",
|
||||||
|
"Calendars Public Sharing": "",
|
||||||
"Call": "",
|
"Call": "",
|
||||||
"Call feature is not supported when using Web STT engine": "",
|
"Call feature is not supported when using Web STT engine": "",
|
||||||
"Camera": "",
|
"Camera": "",
|
||||||
@@ -1696,7 +1697,9 @@
|
|||||||
"Remove Model": "",
|
"Remove Model": "",
|
||||||
"Rename": "",
|
"Rename": "",
|
||||||
"Renamed to {{name}}": "",
|
"Renamed to {{name}}": "",
|
||||||
|
"Render Markdown in Assistant Messages": "",
|
||||||
"Render Markdown in Previews": "",
|
"Render Markdown in Previews": "",
|
||||||
|
"Render Markdown in User Messages": "",
|
||||||
"Reorder Models": "",
|
"Reorder Models": "",
|
||||||
"Repeats": "",
|
"Repeats": "",
|
||||||
"Reply": "",
|
"Reply": "",
|
||||||
|
|||||||
@@ -285,6 +285,7 @@
|
|||||||
"Calendar deleted": "",
|
"Calendar deleted": "",
|
||||||
"Calendar name": "",
|
"Calendar name": "",
|
||||||
"Calendars": "",
|
"Calendars": "",
|
||||||
|
"Calendars Public Sharing": "",
|
||||||
"Call": "Llamada",
|
"Call": "Llamada",
|
||||||
"Call feature is not supported when using Web STT engine": "La funcionalidad de Llamada no está soportada cuando se usa el motor Web STT",
|
"Call feature is not supported when using Web STT engine": "La funcionalidad de Llamada no está soportada cuando se usa el motor Web STT",
|
||||||
"Camera": "Cámara",
|
"Camera": "Cámara",
|
||||||
@@ -1697,7 +1698,9 @@
|
|||||||
"Remove Model": "Eliminar Modelo",
|
"Remove Model": "Eliminar Modelo",
|
||||||
"Rename": "Renombrar",
|
"Rename": "Renombrar",
|
||||||
"Renamed to {{name}}": "Renombrado como {{name}}",
|
"Renamed to {{name}}": "Renombrado como {{name}}",
|
||||||
|
"Render Markdown in Assistant Messages": "",
|
||||||
"Render Markdown in Previews": "Renderizar Markdown en Vista Previa",
|
"Render Markdown in Previews": "Renderizar Markdown en Vista Previa",
|
||||||
|
"Render Markdown in User Messages": "",
|
||||||
"Reorder Models": "Reordenar Modelos",
|
"Reorder Models": "Reordenar Modelos",
|
||||||
"Repeats": "Repeticiones",
|
"Repeats": "Repeticiones",
|
||||||
"Reply": "Responder",
|
"Reply": "Responder",
|
||||||
|
|||||||
@@ -284,6 +284,7 @@
|
|||||||
"Calendar deleted": "",
|
"Calendar deleted": "",
|
||||||
"Calendar name": "",
|
"Calendar name": "",
|
||||||
"Calendars": "",
|
"Calendars": "",
|
||||||
|
"Calendars Public Sharing": "",
|
||||||
"Call": "Kõne",
|
"Call": "Kõne",
|
||||||
"Call feature is not supported when using Web STT engine": "Kõnefunktsioon ei ole Web STT mootorit kasutades toetatud",
|
"Call feature is not supported when using Web STT engine": "Kõnefunktsioon ei ole Web STT mootorit kasutades toetatud",
|
||||||
"Camera": "Kaamera",
|
"Camera": "Kaamera",
|
||||||
@@ -1696,7 +1697,9 @@
|
|||||||
"Remove Model": "Eemalda mudel",
|
"Remove Model": "Eemalda mudel",
|
||||||
"Rename": "Nimeta ümber",
|
"Rename": "Nimeta ümber",
|
||||||
"Renamed to {{name}}": "",
|
"Renamed to {{name}}": "",
|
||||||
|
"Render Markdown in Assistant Messages": "",
|
||||||
"Render Markdown in Previews": "Renderda Markdown eelvaadetes",
|
"Render Markdown in Previews": "Renderda Markdown eelvaadetes",
|
||||||
|
"Render Markdown in User Messages": "",
|
||||||
"Reorder Models": "Muuda mudelite järjekorda",
|
"Reorder Models": "Muuda mudelite järjekorda",
|
||||||
"Repeats": "",
|
"Repeats": "",
|
||||||
"Reply": "Vasta",
|
"Reply": "Vasta",
|
||||||
|
|||||||
@@ -284,6 +284,7 @@
|
|||||||
"Calendar deleted": "",
|
"Calendar deleted": "",
|
||||||
"Calendar name": "",
|
"Calendar name": "",
|
||||||
"Calendars": "",
|
"Calendars": "",
|
||||||
|
"Calendars Public Sharing": "",
|
||||||
"Call": "Deia",
|
"Call": "Deia",
|
||||||
"Call feature is not supported when using Web STT engine": "Dei funtzioa ez da onartzen Web STT motorra erabiltzean",
|
"Call feature is not supported when using Web STT engine": "Dei funtzioa ez da onartzen Web STT motorra erabiltzean",
|
||||||
"Camera": "Kamera",
|
"Camera": "Kamera",
|
||||||
@@ -1696,7 +1697,9 @@
|
|||||||
"Remove Model": "Kendu modeloa",
|
"Remove Model": "Kendu modeloa",
|
||||||
"Rename": "Berrizendatu",
|
"Rename": "Berrizendatu",
|
||||||
"Renamed to {{name}}": "",
|
"Renamed to {{name}}": "",
|
||||||
|
"Render Markdown in Assistant Messages": "",
|
||||||
"Render Markdown in Previews": "",
|
"Render Markdown in Previews": "",
|
||||||
|
"Render Markdown in User Messages": "",
|
||||||
"Reorder Models": "Berrantolatu modeloak",
|
"Reorder Models": "Berrantolatu modeloak",
|
||||||
"Repeats": "",
|
"Repeats": "",
|
||||||
"Reply": "",
|
"Reply": "",
|
||||||
|
|||||||
@@ -284,6 +284,7 @@
|
|||||||
"Calendar deleted": "",
|
"Calendar deleted": "",
|
||||||
"Calendar name": "",
|
"Calendar name": "",
|
||||||
"Calendars": "",
|
"Calendars": "",
|
||||||
|
"Calendars Public Sharing": "",
|
||||||
"Call": "تماس",
|
"Call": "تماس",
|
||||||
"Call feature is not supported when using Web STT engine": "ویژگی تماس هنگام استفاده از موتور Web STT پشتیبانی نمی\u200cشود",
|
"Call feature is not supported when using Web STT engine": "ویژگی تماس هنگام استفاده از موتور Web STT پشتیبانی نمی\u200cشود",
|
||||||
"Camera": "دوربین",
|
"Camera": "دوربین",
|
||||||
@@ -1696,7 +1697,9 @@
|
|||||||
"Remove Model": "حذف مدل",
|
"Remove Model": "حذف مدل",
|
||||||
"Rename": "تغییر نام",
|
"Rename": "تغییر نام",
|
||||||
"Renamed to {{name}}": "",
|
"Renamed to {{name}}": "",
|
||||||
|
"Render Markdown in Assistant Messages": "",
|
||||||
"Render Markdown in Previews": "",
|
"Render Markdown in Previews": "",
|
||||||
|
"Render Markdown in User Messages": "",
|
||||||
"Reorder Models": "ترتیب مجدد مدل\u200cها",
|
"Reorder Models": "ترتیب مجدد مدل\u200cها",
|
||||||
"Repeats": "",
|
"Repeats": "",
|
||||||
"Reply": "پاسخ",
|
"Reply": "پاسخ",
|
||||||
|
|||||||
@@ -284,6 +284,7 @@
|
|||||||
"Calendar deleted": "Kalenteri poistettu",
|
"Calendar deleted": "Kalenteri poistettu",
|
||||||
"Calendar name": "",
|
"Calendar name": "",
|
||||||
"Calendars": "Kalenterit",
|
"Calendars": "Kalenterit",
|
||||||
|
"Calendars Public Sharing": "",
|
||||||
"Call": "Puhelu",
|
"Call": "Puhelu",
|
||||||
"Call feature is not supported when using Web STT engine": "Puhelutoimintoa ei tueta käytettäessä web-puheentunnistusmoottoria",
|
"Call feature is not supported when using Web STT engine": "Puhelutoimintoa ei tueta käytettäessä web-puheentunnistusmoottoria",
|
||||||
"Camera": "Kamera",
|
"Camera": "Kamera",
|
||||||
@@ -1696,7 +1697,9 @@
|
|||||||
"Remove Model": "Poista malli",
|
"Remove Model": "Poista malli",
|
||||||
"Rename": "Nimeä uudelleen",
|
"Rename": "Nimeä uudelleen",
|
||||||
"Renamed to {{name}}": "Nimetty uudelleen {{name}}",
|
"Renamed to {{name}}": "Nimetty uudelleen {{name}}",
|
||||||
|
"Render Markdown in Assistant Messages": "",
|
||||||
"Render Markdown in Previews": "Renderöi Markdown esikatseluissa",
|
"Render Markdown in Previews": "Renderöi Markdown esikatseluissa",
|
||||||
|
"Render Markdown in User Messages": "",
|
||||||
"Reorder Models": "Uudelleenjärjestä malleja",
|
"Reorder Models": "Uudelleenjärjestä malleja",
|
||||||
"Repeats": "Toistot",
|
"Repeats": "Toistot",
|
||||||
"Reply": "Vastaa",
|
"Reply": "Vastaa",
|
||||||
|
|||||||
@@ -285,6 +285,7 @@
|
|||||||
"Calendar deleted": "",
|
"Calendar deleted": "",
|
||||||
"Calendar name": "",
|
"Calendar name": "",
|
||||||
"Calendars": "Mga Kalendaryo",
|
"Calendars": "Mga Kalendaryo",
|
||||||
|
"Calendars Public Sharing": "",
|
||||||
"Call": "Tawag",
|
"Call": "Tawag",
|
||||||
"Call feature is not supported when using Web STT engine": "",
|
"Call feature is not supported when using Web STT engine": "",
|
||||||
"Camera": "",
|
"Camera": "",
|
||||||
@@ -1697,7 +1698,9 @@
|
|||||||
"Remove Model": "",
|
"Remove Model": "",
|
||||||
"Rename": "Palitan ng Pangalan",
|
"Rename": "Palitan ng Pangalan",
|
||||||
"Renamed to {{name}}": "",
|
"Renamed to {{name}}": "",
|
||||||
|
"Render Markdown in Assistant Messages": "",
|
||||||
"Render Markdown in Previews": "",
|
"Render Markdown in Previews": "",
|
||||||
|
"Render Markdown in User Messages": "",
|
||||||
"Reorder Models": "",
|
"Reorder Models": "",
|
||||||
"Repeats": "",
|
"Repeats": "",
|
||||||
"Reply": "Sumagot",
|
"Reply": "Sumagot",
|
||||||
|
|||||||
@@ -285,6 +285,7 @@
|
|||||||
"Calendar deleted": "",
|
"Calendar deleted": "",
|
||||||
"Calendar name": "",
|
"Calendar name": "",
|
||||||
"Calendars": "",
|
"Calendars": "",
|
||||||
|
"Calendars Public Sharing": "",
|
||||||
"Call": "Appeler",
|
"Call": "Appeler",
|
||||||
"Call feature is not supported when using Web STT engine": "La fonction d'appel n'est pas prise en charge lors de l'utilisation du moteur Web STT",
|
"Call feature is not supported when using Web STT engine": "La fonction d'appel n'est pas prise en charge lors de l'utilisation du moteur Web STT",
|
||||||
"Camera": "Appareil photo",
|
"Camera": "Appareil photo",
|
||||||
@@ -1697,7 +1698,9 @@
|
|||||||
"Remove Model": "Retirer le modèle",
|
"Remove Model": "Retirer le modèle",
|
||||||
"Rename": "Renommer",
|
"Rename": "Renommer",
|
||||||
"Renamed to {{name}}": "",
|
"Renamed to {{name}}": "",
|
||||||
|
"Render Markdown in Assistant Messages": "",
|
||||||
"Render Markdown in Previews": "",
|
"Render Markdown in Previews": "",
|
||||||
|
"Render Markdown in User Messages": "",
|
||||||
"Reorder Models": "Réorganiser les modèles",
|
"Reorder Models": "Réorganiser les modèles",
|
||||||
"Repeats": "",
|
"Repeats": "",
|
||||||
"Reply": "",
|
"Reply": "",
|
||||||
|
|||||||
@@ -285,6 +285,7 @@
|
|||||||
"Calendar deleted": "",
|
"Calendar deleted": "",
|
||||||
"Calendar name": "",
|
"Calendar name": "",
|
||||||
"Calendars": "",
|
"Calendars": "",
|
||||||
|
"Calendars Public Sharing": "",
|
||||||
"Call": "Appeler",
|
"Call": "Appeler",
|
||||||
"Call feature is not supported when using Web STT engine": "La fonction d'appel n'est pas prise en charge lors de l'utilisation du moteur Web STT",
|
"Call feature is not supported when using Web STT engine": "La fonction d'appel n'est pas prise en charge lors de l'utilisation du moteur Web STT",
|
||||||
"Camera": "Appareil photo",
|
"Camera": "Appareil photo",
|
||||||
@@ -1697,7 +1698,9 @@
|
|||||||
"Remove Model": "Retirer le modèle",
|
"Remove Model": "Retirer le modèle",
|
||||||
"Rename": "Renommer",
|
"Rename": "Renommer",
|
||||||
"Renamed to {{name}}": "",
|
"Renamed to {{name}}": "",
|
||||||
|
"Render Markdown in Assistant Messages": "",
|
||||||
"Render Markdown in Previews": "Afficher le Markdown dans les aperçus",
|
"Render Markdown in Previews": "Afficher le Markdown dans les aperçus",
|
||||||
|
"Render Markdown in User Messages": "",
|
||||||
"Reorder Models": "Réorganiser les modèles",
|
"Reorder Models": "Réorganiser les modèles",
|
||||||
"Repeats": "",
|
"Repeats": "",
|
||||||
"Reply": "Répondre",
|
"Reply": "Répondre",
|
||||||
|
|||||||
@@ -284,6 +284,7 @@
|
|||||||
"Calendar deleted": "",
|
"Calendar deleted": "",
|
||||||
"Calendar name": "",
|
"Calendar name": "",
|
||||||
"Calendars": "",
|
"Calendars": "",
|
||||||
|
"Calendars Public Sharing": "",
|
||||||
"Call": "Chamada",
|
"Call": "Chamada",
|
||||||
"Call feature is not supported when using Web STT engine": "A funcionalidade da chamada non pode usarse xunto co motor da STT Web",
|
"Call feature is not supported when using Web STT engine": "A funcionalidade da chamada non pode usarse xunto co motor da STT Web",
|
||||||
"Camera": "Cámara",
|
"Camera": "Cámara",
|
||||||
@@ -1696,7 +1697,9 @@
|
|||||||
"Remove Model": "Eliminar modelo",
|
"Remove Model": "Eliminar modelo",
|
||||||
"Rename": "Renombrar",
|
"Rename": "Renombrar",
|
||||||
"Renamed to {{name}}": "",
|
"Renamed to {{name}}": "",
|
||||||
|
"Render Markdown in Assistant Messages": "",
|
||||||
"Render Markdown in Previews": "",
|
"Render Markdown in Previews": "",
|
||||||
|
"Render Markdown in User Messages": "",
|
||||||
"Reorder Models": "Reordenar modelos",
|
"Reorder Models": "Reordenar modelos",
|
||||||
"Repeats": "",
|
"Repeats": "",
|
||||||
"Reply": "",
|
"Reply": "",
|
||||||
|
|||||||
@@ -285,6 +285,7 @@
|
|||||||
"Calendar deleted": "",
|
"Calendar deleted": "",
|
||||||
"Calendar name": "",
|
"Calendar name": "",
|
||||||
"Calendars": "",
|
"Calendars": "",
|
||||||
|
"Calendars Public Sharing": "",
|
||||||
"Call": "",
|
"Call": "",
|
||||||
"Call feature is not supported when using Web STT engine": "",
|
"Call feature is not supported when using Web STT engine": "",
|
||||||
"Camera": "מצלמה",
|
"Camera": "מצלמה",
|
||||||
@@ -1697,7 +1698,9 @@
|
|||||||
"Remove Model": "הסר מודל",
|
"Remove Model": "הסר מודל",
|
||||||
"Rename": "שנה שם",
|
"Rename": "שנה שם",
|
||||||
"Renamed to {{name}}": "",
|
"Renamed to {{name}}": "",
|
||||||
|
"Render Markdown in Assistant Messages": "",
|
||||||
"Render Markdown in Previews": "",
|
"Render Markdown in Previews": "",
|
||||||
|
"Render Markdown in User Messages": "",
|
||||||
"Reorder Models": "",
|
"Reorder Models": "",
|
||||||
"Repeats": "",
|
"Repeats": "",
|
||||||
"Reply": "",
|
"Reply": "",
|
||||||
|
|||||||
@@ -284,6 +284,7 @@
|
|||||||
"Calendar deleted": "",
|
"Calendar deleted": "",
|
||||||
"Calendar name": "",
|
"Calendar name": "",
|
||||||
"Calendars": "",
|
"Calendars": "",
|
||||||
|
"Calendars Public Sharing": "",
|
||||||
"Call": "",
|
"Call": "",
|
||||||
"Call feature is not supported when using Web STT engine": "",
|
"Call feature is not supported when using Web STT engine": "",
|
||||||
"Camera": "",
|
"Camera": "",
|
||||||
@@ -1696,7 +1697,9 @@
|
|||||||
"Remove Model": "मोडेल हटाएँ",
|
"Remove Model": "मोडेल हटाएँ",
|
||||||
"Rename": "नाम बदलें",
|
"Rename": "नाम बदलें",
|
||||||
"Renamed to {{name}}": "",
|
"Renamed to {{name}}": "",
|
||||||
|
"Render Markdown in Assistant Messages": "",
|
||||||
"Render Markdown in Previews": "",
|
"Render Markdown in Previews": "",
|
||||||
|
"Render Markdown in User Messages": "",
|
||||||
"Reorder Models": "",
|
"Reorder Models": "",
|
||||||
"Repeats": "",
|
"Repeats": "",
|
||||||
"Reply": "",
|
"Reply": "",
|
||||||
|
|||||||
@@ -285,6 +285,7 @@
|
|||||||
"Calendar deleted": "",
|
"Calendar deleted": "",
|
||||||
"Calendar name": "",
|
"Calendar name": "",
|
||||||
"Calendars": "",
|
"Calendars": "",
|
||||||
|
"Calendars Public Sharing": "",
|
||||||
"Call": "Poziv",
|
"Call": "Poziv",
|
||||||
"Call feature is not supported when using Web STT engine": "Značajka poziva nije podržana kada se koristi Web STT mehanizam",
|
"Call feature is not supported when using Web STT engine": "Značajka poziva nije podržana kada se koristi Web STT mehanizam",
|
||||||
"Camera": "Kamera",
|
"Camera": "Kamera",
|
||||||
@@ -1697,7 +1698,9 @@
|
|||||||
"Remove Model": "Ukloni model",
|
"Remove Model": "Ukloni model",
|
||||||
"Rename": "Preimenuj",
|
"Rename": "Preimenuj",
|
||||||
"Renamed to {{name}}": "",
|
"Renamed to {{name}}": "",
|
||||||
|
"Render Markdown in Assistant Messages": "",
|
||||||
"Render Markdown in Previews": "",
|
"Render Markdown in Previews": "",
|
||||||
|
"Render Markdown in User Messages": "",
|
||||||
"Reorder Models": "",
|
"Reorder Models": "",
|
||||||
"Repeats": "",
|
"Repeats": "",
|
||||||
"Reply": "",
|
"Reply": "",
|
||||||
|
|||||||
@@ -284,6 +284,7 @@
|
|||||||
"Calendar deleted": "",
|
"Calendar deleted": "",
|
||||||
"Calendar name": "",
|
"Calendar name": "",
|
||||||
"Calendars": "",
|
"Calendars": "",
|
||||||
|
"Calendars Public Sharing": "",
|
||||||
"Call": "Hívás",
|
"Call": "Hívás",
|
||||||
"Call feature is not supported when using Web STT engine": "A hívás funkció nem támogatott Web STT motor használatakor",
|
"Call feature is not supported when using Web STT engine": "A hívás funkció nem támogatott Web STT motor használatakor",
|
||||||
"Camera": "Kamera",
|
"Camera": "Kamera",
|
||||||
@@ -1696,7 +1697,9 @@
|
|||||||
"Remove Model": "Modell eltávolítása",
|
"Remove Model": "Modell eltávolítása",
|
||||||
"Rename": "Átnevezés",
|
"Rename": "Átnevezés",
|
||||||
"Renamed to {{name}}": "",
|
"Renamed to {{name}}": "",
|
||||||
|
"Render Markdown in Assistant Messages": "",
|
||||||
"Render Markdown in Previews": "",
|
"Render Markdown in Previews": "",
|
||||||
|
"Render Markdown in User Messages": "",
|
||||||
"Reorder Models": "Modellek átrendezése",
|
"Reorder Models": "Modellek átrendezése",
|
||||||
"Repeats": "",
|
"Repeats": "",
|
||||||
"Reply": "",
|
"Reply": "",
|
||||||
|
|||||||
@@ -283,6 +283,7 @@
|
|||||||
"Calendar deleted": "",
|
"Calendar deleted": "",
|
||||||
"Calendar name": "",
|
"Calendar name": "",
|
||||||
"Calendars": "",
|
"Calendars": "",
|
||||||
|
"Calendars Public Sharing": "",
|
||||||
"Call": "Panggilan",
|
"Call": "Panggilan",
|
||||||
"Call feature is not supported when using Web STT engine": "Fitur panggilan tidak didukung saat menggunakan mesin Web STT",
|
"Call feature is not supported when using Web STT engine": "Fitur panggilan tidak didukung saat menggunakan mesin Web STT",
|
||||||
"Camera": "Kamera",
|
"Camera": "Kamera",
|
||||||
@@ -1695,7 +1696,9 @@
|
|||||||
"Remove Model": "Hapus Model",
|
"Remove Model": "Hapus Model",
|
||||||
"Rename": "Ganti nama",
|
"Rename": "Ganti nama",
|
||||||
"Renamed to {{name}}": "",
|
"Renamed to {{name}}": "",
|
||||||
|
"Render Markdown in Assistant Messages": "",
|
||||||
"Render Markdown in Previews": "",
|
"Render Markdown in Previews": "",
|
||||||
|
"Render Markdown in User Messages": "",
|
||||||
"Reorder Models": "",
|
"Reorder Models": "",
|
||||||
"Repeats": "",
|
"Repeats": "",
|
||||||
"Reply": "",
|
"Reply": "",
|
||||||
|
|||||||
@@ -284,6 +284,7 @@
|
|||||||
"Calendar deleted": "",
|
"Calendar deleted": "",
|
||||||
"Calendar name": "",
|
"Calendar name": "",
|
||||||
"Calendars": "",
|
"Calendars": "",
|
||||||
|
"Calendars Public Sharing": "",
|
||||||
"Call": "Glaoigh",
|
"Call": "Glaoigh",
|
||||||
"Call feature is not supported when using Web STT engine": "Ní thacaítear le gné glaonna agus inneall Web STT á úsáid",
|
"Call feature is not supported when using Web STT engine": "Ní thacaítear le gné glaonna agus inneall Web STT á úsáid",
|
||||||
"Camera": "Ceamara",
|
"Camera": "Ceamara",
|
||||||
@@ -1696,7 +1697,9 @@
|
|||||||
"Remove Model": "Bain an tSamhail",
|
"Remove Model": "Bain an tSamhail",
|
||||||
"Rename": "Athainmnigh",
|
"Rename": "Athainmnigh",
|
||||||
"Renamed to {{name}}": "Athainmnithe go {{name}}",
|
"Renamed to {{name}}": "Athainmnithe go {{name}}",
|
||||||
|
"Render Markdown in Assistant Messages": "",
|
||||||
"Render Markdown in Previews": "Rindreáil Markdown i Réamhamhairc",
|
"Render Markdown in Previews": "Rindreáil Markdown i Réamhamhairc",
|
||||||
|
"Render Markdown in User Messages": "",
|
||||||
"Reorder Models": "Athordú na Samhlacha",
|
"Reorder Models": "Athordú na Samhlacha",
|
||||||
"Repeats": "Athdhéantar",
|
"Repeats": "Athdhéantar",
|
||||||
"Reply": "Freagra",
|
"Reply": "Freagra",
|
||||||
|
|||||||
@@ -285,6 +285,7 @@
|
|||||||
"Calendar deleted": "",
|
"Calendar deleted": "",
|
||||||
"Calendar name": "",
|
"Calendar name": "",
|
||||||
"Calendars": "",
|
"Calendars": "",
|
||||||
|
"Calendars Public Sharing": "",
|
||||||
"Call": "Chiamata",
|
"Call": "Chiamata",
|
||||||
"Call feature is not supported when using Web STT engine": "La funzione di chiamata non è supportata quando si utilizza il motore Web STT",
|
"Call feature is not supported when using Web STT engine": "La funzione di chiamata non è supportata quando si utilizza il motore Web STT",
|
||||||
"Camera": "Fotocamera",
|
"Camera": "Fotocamera",
|
||||||
@@ -1697,7 +1698,9 @@
|
|||||||
"Remove Model": "Rimuovi Modello",
|
"Remove Model": "Rimuovi Modello",
|
||||||
"Rename": "Rinomina",
|
"Rename": "Rinomina",
|
||||||
"Renamed to {{name}}": "",
|
"Renamed to {{name}}": "",
|
||||||
|
"Render Markdown in Assistant Messages": "",
|
||||||
"Render Markdown in Previews": "",
|
"Render Markdown in Previews": "",
|
||||||
|
"Render Markdown in User Messages": "",
|
||||||
"Reorder Models": "Riordina Modelli",
|
"Reorder Models": "Riordina Modelli",
|
||||||
"Repeats": "",
|
"Repeats": "",
|
||||||
"Reply": "",
|
"Reply": "",
|
||||||
|
|||||||
@@ -283,6 +283,7 @@
|
|||||||
"Calendar deleted": "",
|
"Calendar deleted": "",
|
||||||
"Calendar name": "",
|
"Calendar name": "",
|
||||||
"Calendars": "",
|
"Calendars": "",
|
||||||
|
"Calendars Public Sharing": "",
|
||||||
"Call": "コール",
|
"Call": "コール",
|
||||||
"Call feature is not supported when using Web STT engine": "Web STTエンジンを使用している場合、コール機能は使用できません",
|
"Call feature is not supported when using Web STT engine": "Web STTエンジンを使用している場合、コール機能は使用できません",
|
||||||
"Camera": "カメラ",
|
"Camera": "カメラ",
|
||||||
@@ -1695,7 +1696,9 @@
|
|||||||
"Remove Model": "モデルを削除",
|
"Remove Model": "モデルを削除",
|
||||||
"Rename": "名前を変更",
|
"Rename": "名前を変更",
|
||||||
"Renamed to {{name}}": "",
|
"Renamed to {{name}}": "",
|
||||||
|
"Render Markdown in Assistant Messages": "",
|
||||||
"Render Markdown in Previews": "",
|
"Render Markdown in Previews": "",
|
||||||
|
"Render Markdown in User Messages": "",
|
||||||
"Reorder Models": "モデルを並べ替え",
|
"Reorder Models": "モデルを並べ替え",
|
||||||
"Repeats": "繰り返し",
|
"Repeats": "繰り返し",
|
||||||
"Reply": "",
|
"Reply": "",
|
||||||
|
|||||||
@@ -284,6 +284,7 @@
|
|||||||
"Calendar deleted": "",
|
"Calendar deleted": "",
|
||||||
"Calendar name": "",
|
"Calendar name": "",
|
||||||
"Calendars": "",
|
"Calendars": "",
|
||||||
|
"Calendars Public Sharing": "",
|
||||||
"Call": "ზარი",
|
"Call": "ზარი",
|
||||||
"Call feature is not supported when using Web STT engine": "",
|
"Call feature is not supported when using Web STT engine": "",
|
||||||
"Camera": "კამერა",
|
"Camera": "კამერა",
|
||||||
@@ -1696,7 +1697,9 @@
|
|||||||
"Remove Model": "მოდელის წაშლა",
|
"Remove Model": "მოდელის წაშლა",
|
||||||
"Rename": "სახელის გადარქმევა",
|
"Rename": "სახელის გადარქმევა",
|
||||||
"Renamed to {{name}}": "",
|
"Renamed to {{name}}": "",
|
||||||
|
"Render Markdown in Assistant Messages": "",
|
||||||
"Render Markdown in Previews": "",
|
"Render Markdown in Previews": "",
|
||||||
|
"Render Markdown in User Messages": "",
|
||||||
"Reorder Models": "მოდელების გადალაგება",
|
"Reorder Models": "მოდელების გადალაგება",
|
||||||
"Repeats": "",
|
"Repeats": "",
|
||||||
"Reply": "პასუხი",
|
"Reply": "პასუხი",
|
||||||
|
|||||||
@@ -284,6 +284,7 @@
|
|||||||
"Calendar deleted": "",
|
"Calendar deleted": "",
|
||||||
"Calendar name": "",
|
"Calendar name": "",
|
||||||
"Calendars": "",
|
"Calendars": "",
|
||||||
|
"Calendars Public Sharing": "",
|
||||||
"Call": "Siwel",
|
"Call": "Siwel",
|
||||||
"Call feature is not supported when using Web STT engine": "Tamahilt n usiwel ur tettwasefrak ara mi ara tesqedceḍ amsedday Web STT",
|
"Call feature is not supported when using Web STT engine": "Tamahilt n usiwel ur tettwasefrak ara mi ara tesqedceḍ amsedday Web STT",
|
||||||
"Camera": "Takamiṛatt",
|
"Camera": "Takamiṛatt",
|
||||||
@@ -1696,7 +1697,9 @@
|
|||||||
"Remove Model": "Kkes tamudemt",
|
"Remove Model": "Kkes tamudemt",
|
||||||
"Rename": "Snifel isem",
|
"Rename": "Snifel isem",
|
||||||
"Renamed to {{name}}": "",
|
"Renamed to {{name}}": "",
|
||||||
|
"Render Markdown in Assistant Messages": "",
|
||||||
"Render Markdown in Previews": "",
|
"Render Markdown in Previews": "",
|
||||||
|
"Render Markdown in User Messages": "",
|
||||||
"Reorder Models": "Ales n umizwer n tmudmiwin",
|
"Reorder Models": "Ales n umizwer n tmudmiwin",
|
||||||
"Repeats": "",
|
"Repeats": "",
|
||||||
"Reply": "Tiririt",
|
"Reply": "Tiririt",
|
||||||
|
|||||||
@@ -283,6 +283,7 @@
|
|||||||
"Calendar deleted": "",
|
"Calendar deleted": "",
|
||||||
"Calendar name": "",
|
"Calendar name": "",
|
||||||
"Calendars": "",
|
"Calendars": "",
|
||||||
|
"Calendars Public Sharing": "",
|
||||||
"Call": "",
|
"Call": "",
|
||||||
"Call feature is not supported when using Web STT engine": "",
|
"Call feature is not supported when using Web STT engine": "",
|
||||||
"Camera": "",
|
"Camera": "",
|
||||||
@@ -1695,7 +1696,9 @@
|
|||||||
"Remove Model": "",
|
"Remove Model": "",
|
||||||
"Rename": "",
|
"Rename": "",
|
||||||
"Renamed to {{name}}": "",
|
"Renamed to {{name}}": "",
|
||||||
|
"Render Markdown in Assistant Messages": "",
|
||||||
"Render Markdown in Previews": "",
|
"Render Markdown in Previews": "",
|
||||||
|
"Render Markdown in User Messages": "",
|
||||||
"Reorder Models": "",
|
"Reorder Models": "",
|
||||||
"Repeats": "",
|
"Repeats": "",
|
||||||
"Reply": "",
|
"Reply": "",
|
||||||
|
|||||||
@@ -286,6 +286,7 @@
|
|||||||
"Calendar deleted": "",
|
"Calendar deleted": "",
|
||||||
"Calendar name": "",
|
"Calendar name": "",
|
||||||
"Calendars": "",
|
"Calendars": "",
|
||||||
|
"Calendars Public Sharing": "",
|
||||||
"Call": "Skambinti",
|
"Call": "Skambinti",
|
||||||
"Call feature is not supported when using Web STT engine": "Skambučio funkcionalumas neleidžiamas naudojant Web STT variklį",
|
"Call feature is not supported when using Web STT engine": "Skambučio funkcionalumas neleidžiamas naudojant Web STT variklį",
|
||||||
"Camera": "Kamera",
|
"Camera": "Kamera",
|
||||||
@@ -1698,7 +1699,9 @@
|
|||||||
"Remove Model": "Pašalinti modelį",
|
"Remove Model": "Pašalinti modelį",
|
||||||
"Rename": "Pervadinti",
|
"Rename": "Pervadinti",
|
||||||
"Renamed to {{name}}": "",
|
"Renamed to {{name}}": "",
|
||||||
|
"Render Markdown in Assistant Messages": "",
|
||||||
"Render Markdown in Previews": "",
|
"Render Markdown in Previews": "",
|
||||||
|
"Render Markdown in User Messages": "",
|
||||||
"Reorder Models": "",
|
"Reorder Models": "",
|
||||||
"Repeats": "",
|
"Repeats": "",
|
||||||
"Reply": "",
|
"Reply": "",
|
||||||
|
|||||||
@@ -285,6 +285,7 @@
|
|||||||
"Calendar deleted": "",
|
"Calendar deleted": "",
|
||||||
"Calendar name": "",
|
"Calendar name": "",
|
||||||
"Calendars": "",
|
"Calendars": "",
|
||||||
|
"Calendars Public Sharing": "",
|
||||||
"Call": "Zvans",
|
"Call": "Zvans",
|
||||||
"Call feature is not supported when using Web STT engine": "Zvana funkcija nav atbalstīta, izmantojot Web STT dzinēju",
|
"Call feature is not supported when using Web STT engine": "Zvana funkcija nav atbalstīta, izmantojot Web STT dzinēju",
|
||||||
"Camera": "Kamera",
|
"Camera": "Kamera",
|
||||||
@@ -1697,7 +1698,9 @@
|
|||||||
"Remove Model": "Noņemt modeli",
|
"Remove Model": "Noņemt modeli",
|
||||||
"Rename": "Pārdēvēt",
|
"Rename": "Pārdēvēt",
|
||||||
"Renamed to {{name}}": "",
|
"Renamed to {{name}}": "",
|
||||||
|
"Render Markdown in Assistant Messages": "",
|
||||||
"Render Markdown in Previews": "",
|
"Render Markdown in Previews": "",
|
||||||
|
"Render Markdown in User Messages": "",
|
||||||
"Reorder Models": "Pārkārtot modeļus",
|
"Reorder Models": "Pārkārtot modeļus",
|
||||||
"Repeats": "",
|
"Repeats": "",
|
||||||
"Reply": "Atbildēt",
|
"Reply": "Atbildēt",
|
||||||
|
|||||||
@@ -283,6 +283,7 @@
|
|||||||
"Calendar deleted": "",
|
"Calendar deleted": "",
|
||||||
"Calendar name": "",
|
"Calendar name": "",
|
||||||
"Calendars": "",
|
"Calendars": "",
|
||||||
|
"Calendars Public Sharing": "",
|
||||||
"Call": "Hubungi",
|
"Call": "Hubungi",
|
||||||
"Call feature is not supported when using Web STT engine": "Ciri panggilan tidak disokong apabila menggunakan enjin Web STT",
|
"Call feature is not supported when using Web STT engine": "Ciri panggilan tidak disokong apabila menggunakan enjin Web STT",
|
||||||
"Camera": "Kamera",
|
"Camera": "Kamera",
|
||||||
@@ -1695,7 +1696,9 @@
|
|||||||
"Remove Model": "Hapuskan Model",
|
"Remove Model": "Hapuskan Model",
|
||||||
"Rename": "Namakan Semula",
|
"Rename": "Namakan Semula",
|
||||||
"Renamed to {{name}}": "",
|
"Renamed to {{name}}": "",
|
||||||
|
"Render Markdown in Assistant Messages": "",
|
||||||
"Render Markdown in Previews": "Render Markdown dalam Pratonton",
|
"Render Markdown in Previews": "Render Markdown dalam Pratonton",
|
||||||
|
"Render Markdown in User Messages": "",
|
||||||
"Reorder Models": "Susun Semula Model",
|
"Reorder Models": "Susun Semula Model",
|
||||||
"Repeats": "",
|
"Repeats": "",
|
||||||
"Reply": "Balas",
|
"Reply": "Balas",
|
||||||
|
|||||||
@@ -284,6 +284,7 @@
|
|||||||
"Calendar deleted": "",
|
"Calendar deleted": "",
|
||||||
"Calendar name": "",
|
"Calendar name": "",
|
||||||
"Calendars": "",
|
"Calendars": "",
|
||||||
|
"Calendars Public Sharing": "",
|
||||||
"Call": "Ring",
|
"Call": "Ring",
|
||||||
"Call feature is not supported when using Web STT engine": "Ringefunksjonen støttes ikke når du bruker Web STT-motoren",
|
"Call feature is not supported when using Web STT engine": "Ringefunksjonen støttes ikke når du bruker Web STT-motoren",
|
||||||
"Camera": "Kamera",
|
"Camera": "Kamera",
|
||||||
@@ -1696,7 +1697,9 @@
|
|||||||
"Remove Model": "Fjern modell",
|
"Remove Model": "Fjern modell",
|
||||||
"Rename": "Gi nytt navn",
|
"Rename": "Gi nytt navn",
|
||||||
"Renamed to {{name}}": "",
|
"Renamed to {{name}}": "",
|
||||||
|
"Render Markdown in Assistant Messages": "",
|
||||||
"Render Markdown in Previews": "",
|
"Render Markdown in Previews": "",
|
||||||
|
"Render Markdown in User Messages": "",
|
||||||
"Reorder Models": "Sorter modeller på nytt",
|
"Reorder Models": "Sorter modeller på nytt",
|
||||||
"Repeats": "",
|
"Repeats": "",
|
||||||
"Reply": "",
|
"Reply": "",
|
||||||
|
|||||||
@@ -284,6 +284,7 @@
|
|||||||
"Calendar deleted": "Agenda verwijderd",
|
"Calendar deleted": "Agenda verwijderd",
|
||||||
"Calendar name": "",
|
"Calendar name": "",
|
||||||
"Calendars": "Agenda's",
|
"Calendars": "Agenda's",
|
||||||
|
"Calendars Public Sharing": "",
|
||||||
"Call": "Oproep",
|
"Call": "Oproep",
|
||||||
"Call feature is not supported when using Web STT engine": "Belfunctie wordt niet ondersteund bij gebruik van de Web STT engine",
|
"Call feature is not supported when using Web STT engine": "Belfunctie wordt niet ondersteund bij gebruik van de Web STT engine",
|
||||||
"Camera": "Camera",
|
"Camera": "Camera",
|
||||||
@@ -1696,7 +1697,9 @@
|
|||||||
"Remove Model": "Verwijder model",
|
"Remove Model": "Verwijder model",
|
||||||
"Rename": "Hernoemen",
|
"Rename": "Hernoemen",
|
||||||
"Renamed to {{name}}": "Hernoemd naar {{name}}",
|
"Renamed to {{name}}": "Hernoemd naar {{name}}",
|
||||||
|
"Render Markdown in Assistant Messages": "",
|
||||||
"Render Markdown in Previews": "Markdown renderen in voorvertoningen",
|
"Render Markdown in Previews": "Markdown renderen in voorvertoningen",
|
||||||
|
"Render Markdown in User Messages": "",
|
||||||
"Reorder Models": "Herschik modellen",
|
"Reorder Models": "Herschik modellen",
|
||||||
"Repeats": "Herhalingen",
|
"Repeats": "Herhalingen",
|
||||||
"Reply": "Antwoorden",
|
"Reply": "Antwoorden",
|
||||||
|
|||||||
@@ -284,6 +284,7 @@
|
|||||||
"Calendar deleted": "",
|
"Calendar deleted": "",
|
||||||
"Calendar name": "",
|
"Calendar name": "",
|
||||||
"Calendars": "",
|
"Calendars": "",
|
||||||
|
"Calendars Public Sharing": "",
|
||||||
"Call": "",
|
"Call": "",
|
||||||
"Call feature is not supported when using Web STT engine": "",
|
"Call feature is not supported when using Web STT engine": "",
|
||||||
"Camera": "",
|
"Camera": "",
|
||||||
@@ -1696,7 +1697,9 @@
|
|||||||
"Remove Model": "ਮਾਡਲ ਹਟਾਓ",
|
"Remove Model": "ਮਾਡਲ ਹਟਾਓ",
|
||||||
"Rename": "ਨਾਮ ਬਦਲੋ",
|
"Rename": "ਨਾਮ ਬਦਲੋ",
|
||||||
"Renamed to {{name}}": "",
|
"Renamed to {{name}}": "",
|
||||||
|
"Render Markdown in Assistant Messages": "",
|
||||||
"Render Markdown in Previews": "",
|
"Render Markdown in Previews": "",
|
||||||
|
"Render Markdown in User Messages": "",
|
||||||
"Reorder Models": "",
|
"Reorder Models": "",
|
||||||
"Repeats": "",
|
"Repeats": "",
|
||||||
"Reply": "",
|
"Reply": "",
|
||||||
|
|||||||
@@ -286,6 +286,7 @@
|
|||||||
"Calendar deleted": "",
|
"Calendar deleted": "",
|
||||||
"Calendar name": "",
|
"Calendar name": "",
|
||||||
"Calendars": "",
|
"Calendars": "",
|
||||||
|
"Calendars Public Sharing": "",
|
||||||
"Call": "Rozmowa",
|
"Call": "Rozmowa",
|
||||||
"Call feature is not supported when using Web STT engine": "Funkcja rozmowy nie jest obsługiwana przy użyciu przeglądarkowego silnika STT",
|
"Call feature is not supported when using Web STT engine": "Funkcja rozmowy nie jest obsługiwana przy użyciu przeglądarkowego silnika STT",
|
||||||
"Camera": "Kamera",
|
"Camera": "Kamera",
|
||||||
@@ -1698,7 +1699,9 @@
|
|||||||
"Remove Model": "Usuń model",
|
"Remove Model": "Usuń model",
|
||||||
"Rename": "Zmień nazwę",
|
"Rename": "Zmień nazwę",
|
||||||
"Renamed to {{name}}": "",
|
"Renamed to {{name}}": "",
|
||||||
|
"Render Markdown in Assistant Messages": "",
|
||||||
"Render Markdown in Previews": "",
|
"Render Markdown in Previews": "",
|
||||||
|
"Render Markdown in User Messages": "",
|
||||||
"Reorder Models": "Zmień kolejność modeli",
|
"Reorder Models": "Zmień kolejność modeli",
|
||||||
"Repeats": "",
|
"Repeats": "",
|
||||||
"Reply": "Odpowiedz",
|
"Reply": "Odpowiedz",
|
||||||
|
|||||||
@@ -281,10 +281,11 @@
|
|||||||
"Bypass Web Loader": "Ignorar carregador da Web",
|
"Bypass Web Loader": "Ignorar carregador da Web",
|
||||||
"Cache Base Model List": "Lista de modelos base de cache",
|
"Cache Base Model List": "Lista de modelos base de cache",
|
||||||
"Calendar": "Calendário",
|
"Calendar": "Calendário",
|
||||||
"Calendar created": "",
|
"Calendar created": "Calendário criado",
|
||||||
"Calendar deleted": "Calendário excluído",
|
"Calendar deleted": "Calendário excluído",
|
||||||
"Calendar name": "",
|
"Calendar name": "Nome do calendário",
|
||||||
"Calendars": "Calendários",
|
"Calendars": "Calendários",
|
||||||
|
"Calendars Public Sharing": "Compartilhamento Público de Calendários",
|
||||||
"Call": "Chamada",
|
"Call": "Chamada",
|
||||||
"Call feature is not supported when using Web STT engine": "O recurso de chamada não é suportado ao usar o mecanismo Web STT",
|
"Call feature is not supported when using Web STT engine": "O recurso de chamada não é suportado ao usar o mecanismo Web STT",
|
||||||
"Camera": "Câmera",
|
"Camera": "Câmera",
|
||||||
@@ -316,7 +317,7 @@
|
|||||||
"Chat Bubble UI": "Interface de Bolha de Chat",
|
"Chat Bubble UI": "Interface de Bolha de Chat",
|
||||||
"Chat Completions": "Gerar Resposta",
|
"Chat Completions": "Gerar Resposta",
|
||||||
"Chat Conversation": "Conversa do Chat",
|
"Chat Conversation": "Conversa do Chat",
|
||||||
"Chat deleted.": "",
|
"Chat deleted.": "Chat excluído.",
|
||||||
"Chat direction": "Direção do Chat",
|
"Chat direction": "Direção do Chat",
|
||||||
"Chat exported successfully": "Chat exportado com sucesso",
|
"Chat exported successfully": "Chat exportado com sucesso",
|
||||||
"Chat History": "Histórico de chat",
|
"Chat History": "Histórico de chat",
|
||||||
@@ -327,7 +328,7 @@
|
|||||||
"Chat unshared successfully.": "Compartilhamento do chat removido com sucesso.",
|
"Chat unshared successfully.": "Compartilhamento do chat removido com sucesso.",
|
||||||
"chats": "chats",
|
"chats": "chats",
|
||||||
"Chats": "Chats",
|
"Chats": "Chats",
|
||||||
"Chats Public Sharing": "",
|
"Chats Public Sharing": "Compartilhamento Público de Chats",
|
||||||
"Check Again": "Verificar Novamente",
|
"Check Again": "Verificar Novamente",
|
||||||
"Check for updates": "Verificar atualizações",
|
"Check for updates": "Verificar atualizações",
|
||||||
"Checking for updates...": "Verificando atualizações...",
|
"Checking for updates...": "Verificando atualizações...",
|
||||||
@@ -434,7 +435,7 @@
|
|||||||
"Content": "Conteúdo",
|
"Content": "Conteúdo",
|
||||||
"Content Extraction Engine": "Mecanismo de Extração de Conteúdo",
|
"Content Extraction Engine": "Mecanismo de Extração de Conteúdo",
|
||||||
"Content lengths (character counts only)": "Extensão do conteúdo (apenas em caracteres)",
|
"Content lengths (character counts only)": "Extensão do conteúdo (apenas em caracteres)",
|
||||||
"Context Tokens": "",
|
"Context Tokens": "Tokens de Contexto",
|
||||||
"Continue Response": "Continuar Resposta",
|
"Continue Response": "Continuar Resposta",
|
||||||
"Continue with {{provider}}": "Continuar com {{provider}}",
|
"Continue with {{provider}}": "Continuar com {{provider}}",
|
||||||
"Continue with Email": "Continuar com Email",
|
"Continue with Email": "Continuar com Email",
|
||||||
@@ -494,7 +495,7 @@
|
|||||||
"Current Model": "Modelo Atual",
|
"Current Model": "Modelo Atual",
|
||||||
"Current Password": "Senha Atual",
|
"Current Password": "Senha Atual",
|
||||||
"Custom": "Personalizado",
|
"Custom": "Personalizado",
|
||||||
"Custom color": "",
|
"Custom color": "Cor personalizada",
|
||||||
"Custom description enabled": "Descrição personalizada habilitada",
|
"Custom description enabled": "Descrição personalizada habilitada",
|
||||||
"Custom Gender": "Gênero personalizado",
|
"Custom Gender": "Gênero personalizado",
|
||||||
"Custom Parameter Name": "Nome do parâmetro personalizado",
|
"Custom Parameter Name": "Nome do parâmetro personalizado",
|
||||||
@@ -506,7 +507,7 @@
|
|||||||
"Data Controls": "Controle de Dados",
|
"Data Controls": "Controle de Dados",
|
||||||
"Database": "Banco de Dados",
|
"Database": "Banco de Dados",
|
||||||
"Datalab Marker API": "API do Marcador do Datalab",
|
"Datalab Marker API": "API do Marcador do Datalab",
|
||||||
"Date Modified": "",
|
"Date Modified": "Data de Modificação",
|
||||||
"Day": "Dia",
|
"Day": "Dia",
|
||||||
"DD/MM/YYYY": "DD/MM/AAAA",
|
"DD/MM/YYYY": "DD/MM/AAAA",
|
||||||
"DDGS Backend": "Backend DDGS",
|
"DDGS Backend": "Backend DDGS",
|
||||||
@@ -585,7 +586,7 @@
|
|||||||
"Disable Image Extraction": "Desativar extração de imagem",
|
"Disable Image Extraction": "Desativar extração de imagem",
|
||||||
"Disable image extraction from the PDF. If Use LLM is enabled, images will be automatically captioned. Defaults to False.": "Desabilite a extração de imagens do PDF. Se a opção Usar LLM estiver habilitada, as imagens serão legendadas automaticamente. O padrão é Falso.",
|
"Disable image extraction from the PDF. If Use LLM is enabled, images will be automatically captioned. Defaults to False.": "Desabilite a extração de imagens do PDF. Se a opção Usar LLM estiver habilitada, as imagens serão legendadas automaticamente. O padrão é Falso.",
|
||||||
"Disabled": "Desativado",
|
"Disabled": "Desativado",
|
||||||
"Disconnect OAuth": "",
|
"Disconnect OAuth": "Desconectar OAuth",
|
||||||
"Discover a function": "Descubra uma função",
|
"Discover a function": "Descubra uma função",
|
||||||
"Discover a model": "Descubra um modelo",
|
"Discover a model": "Descubra um modelo",
|
||||||
"Discover a prompt": "Descubra um prompt",
|
"Discover a prompt": "Descubra um prompt",
|
||||||
@@ -775,8 +776,8 @@
|
|||||||
"Enter New Password": "Digite uma nova senha",
|
"Enter New Password": "Digite uma nova senha",
|
||||||
"Enter Number of Steps (e.g. 50)": "Digite o Número de Passos (por exemplo, 50)",
|
"Enter Number of Steps (e.g. 50)": "Digite o Número de Passos (por exemplo, 50)",
|
||||||
"Enter Ollama Cloud API Key": "Insira a chave da API do Ollama Cloud",
|
"Enter Ollama Cloud API Key": "Insira a chave da API do Ollama Cloud",
|
||||||
"Enter PaddleOCR-vl API Base URL": "",
|
"Enter PaddleOCR-vl API Base URL": "Insira a URL base da API PaddleOCR-vl",
|
||||||
"Enter PaddleOCR-vl API Token": "",
|
"Enter PaddleOCR-vl API Token": "Insira o token da API PaddleOCR-vl",
|
||||||
"Enter Perplexity API Key": "Insira a chave da API Perplexity",
|
"Enter Perplexity API Key": "Insira a chave da API Perplexity",
|
||||||
"Enter Perplexity Search API URL": "Insira a URL da API de pesquisa Perplexity",
|
"Enter Perplexity Search API URL": "Insira a URL da API de pesquisa Perplexity",
|
||||||
"Enter Playwright Timeout": "Insira o tempo limite do Playwright",
|
"Enter Playwright Timeout": "Insira o tempo limite do Playwright",
|
||||||
@@ -907,7 +908,7 @@
|
|||||||
"Failed to create API Key.": "Falha ao criar a Chave API.",
|
"Failed to create API Key.": "Falha ao criar a Chave API.",
|
||||||
"Failed to delete calendar": "Falha ao excluir calendário",
|
"Failed to delete calendar": "Falha ao excluir calendário",
|
||||||
"Failed to delete note": "Falha ao excluir a nota",
|
"Failed to delete note": "Falha ao excluir a nota",
|
||||||
"Failed to disconnect": "",
|
"Failed to disconnect": "Falha ao desconectar",
|
||||||
"Failed to download image": "Falha ao baixar a imagem",
|
"Failed to download image": "Falha ao baixar a imagem",
|
||||||
"Failed to extract content from the file: {{error}}": "Falha ao extrair conteúdo do arquivo: {{error}}",
|
"Failed to extract content from the file: {{error}}": "Falha ao extrair conteúdo do arquivo: {{error}}",
|
||||||
"Failed to extract content from the file.": "Falha ao extrair conteúdo do arquivo.",
|
"Failed to extract content from the file.": "Falha ao extrair conteúdo do arquivo.",
|
||||||
@@ -1231,10 +1232,10 @@
|
|||||||
"List calendars, search, create, update, and delete calendar events": "Listar calendários, pesquisar, criar, atualizar e excluir eventos do calendário",
|
"List calendars, search, create, update, and delete calendar events": "Listar calendários, pesquisar, criar, atualizar e excluir eventos do calendário",
|
||||||
"Listening...": "Escutando...",
|
"Listening...": "Escutando...",
|
||||||
"Live": "Ao vivo",
|
"Live": "Ao vivo",
|
||||||
"llama.cpp": "",
|
"llama.cpp": "llama.cpp",
|
||||||
"Llama.cpp": "Llama.cpp",
|
"Llama.cpp": "Llama.cpp",
|
||||||
"LLMs can make mistakes. Verify important information.": "LLMs podem cometer erros. Verifique informações importantes.",
|
"LLMs can make mistakes. Verify important information.": "LLMs podem cometer erros. Verifique informações importantes.",
|
||||||
"Loaded": "",
|
"Loaded": "Carregado",
|
||||||
"Loader": "Carregador",
|
"Loader": "Carregador",
|
||||||
"Loading Kokoro.js...": "Carregando Kokoro.js...",
|
"Loading Kokoro.js...": "Carregando Kokoro.js...",
|
||||||
"Loading...": "Carregando...",
|
"Loading...": "Carregando...",
|
||||||
@@ -1265,7 +1266,7 @@
|
|||||||
"Markdown": "Markdown",
|
"Markdown": "Markdown",
|
||||||
"Markdown Header Text Splitter": "Separador de texto de cabeçalho Markdown",
|
"Markdown Header Text Splitter": "Separador de texto de cabeçalho Markdown",
|
||||||
"Max Speakers": "Máximo de locutores",
|
"Max Speakers": "Máximo de locutores",
|
||||||
"Max tokens to retrieve (1024-32768, default 8192)": "",
|
"Max tokens to retrieve (1024-32768, default 8192)": "Máximo de tokens para recuperar (1024-32768, padrão 8192)",
|
||||||
"Max Upload Count": "Quantidade máxima de anexos",
|
"Max Upload Count": "Quantidade máxima de anexos",
|
||||||
"Max Upload Size": "Tamanho máximo do arquivo",
|
"Max Upload Size": "Tamanho máximo do arquivo",
|
||||||
"Maximum characters to return from fetched URLs. Leave empty for no limit.": "Número máximo de caracteres a retornar de URLs buscadas. Deixe em branco para sem limite.",
|
"Maximum characters to return from fetched URLs. Leave empty for no limit.": "Número máximo de caracteres a retornar de URLs buscadas. Deixe em branco para sem limite.",
|
||||||
@@ -1294,7 +1295,7 @@
|
|||||||
"Message counts and response timestamps": "Contagem de mensagens e registros de data e hora de resposta",
|
"Message counts and response timestamps": "Contagem de mensagens e registros de data e hora de resposta",
|
||||||
"Message counts are based on assistant responses.": "A contagem de mensagens é baseada nas respostas do assistente.",
|
"Message counts are based on assistant responses.": "A contagem de mensagens é baseada nas respostas do assistente.",
|
||||||
"Message rating should be enabled to use this feature": "A avaliação de mensagens deve estar habilitada para usar este recurso",
|
"Message rating should be enabled to use this feature": "A avaliação de mensagens deve estar habilitada para usar este recurso",
|
||||||
"Message text...": "",
|
"Message text...": "Texto da mensagem...",
|
||||||
"messages": "mensagens",
|
"messages": "mensagens",
|
||||||
"Messages": "Mensagens",
|
"Messages": "Mensagens",
|
||||||
"Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "Mensagens enviadas após criar seu link não serão compartilhadas. Usuários com o URL poderão visualizar o chat compartilhado.",
|
"Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "Mensagens enviadas após criar seu link não serão compartilhadas. Usuários com o URL poderão visualizar o chat compartilhado.",
|
||||||
@@ -1361,12 +1362,12 @@
|
|||||||
"More Options": "Mais opções",
|
"More Options": "Mais opções",
|
||||||
"Move": "Mover",
|
"Move": "Mover",
|
||||||
"Moved {{name}}": "{{name}} movido",
|
"Moved {{name}}": "{{name}} movido",
|
||||||
"Mute": "",
|
"Mute": "Silenciar",
|
||||||
"Muted": "",
|
"Muted": "Silenciado",
|
||||||
"My Terminal": "Meu Terminal",
|
"My Terminal": "Meu Terminal",
|
||||||
"Name": "Nome",
|
"Name": "Nome",
|
||||||
"Name and ID are required, please fill them out": "Nome e ID são obrigatórios, por favor preencha-os",
|
"Name and ID are required, please fill them out": "Nome e ID são obrigatórios, por favor preencha-os",
|
||||||
"Name is required": "",
|
"Name is required": "O nome é obrigatório",
|
||||||
"Name your knowledge base": "Nome da sua base de conhecimento",
|
"Name your knowledge base": "Nome da sua base de conhecimento",
|
||||||
"Name, prompt, and model are required": "Nome, prompt e modelo são obrigatórios.",
|
"Name, prompt, and model are required": "Nome, prompt e modelo são obrigatórios.",
|
||||||
"Native": "Nativo",
|
"Native": "Nativo",
|
||||||
@@ -1374,8 +1375,8 @@
|
|||||||
"New": "Novo",
|
"New": "Novo",
|
||||||
"New Automation": "Nova Automação",
|
"New Automation": "Nova Automação",
|
||||||
"New Button": "Novo Botão",
|
"New Button": "Novo Botão",
|
||||||
"New calendar": "",
|
"New calendar": "Novo calendário",
|
||||||
"New Calendar": "",
|
"New Calendar": "Novo Calendário",
|
||||||
"New Chat": "Novo Chat",
|
"New Chat": "Novo Chat",
|
||||||
"New Event": "Novo Evento",
|
"New Event": "Novo Evento",
|
||||||
"New File": "Novo Arquivo",
|
"New File": "Novo Arquivo",
|
||||||
@@ -1433,7 +1434,7 @@
|
|||||||
"No Notes": "Sem Notas",
|
"No Notes": "Sem Notas",
|
||||||
"No notes found": "Notas não encontradas",
|
"No notes found": "Notas não encontradas",
|
||||||
"No one": "Ninguém",
|
"No one": "Ninguém",
|
||||||
"No output items": "",
|
"No output items": "Nenhum item de saída",
|
||||||
"No pinned messages": "Nenhuma mensagem fixada",
|
"No pinned messages": "Nenhuma mensagem fixada",
|
||||||
"No prompts found": "Nenhum prompt encontrado",
|
"No prompts found": "Nenhum prompt encontrado",
|
||||||
"No results": "Nenhum resultado encontrado",
|
"No results": "Nenhum resultado encontrado",
|
||||||
@@ -1472,8 +1473,8 @@
|
|||||||
"OAuth 2.1": "OAuth 2.1",
|
"OAuth 2.1": "OAuth 2.1",
|
||||||
"OAuth 2.1 (Static)": "OAuth 2.1 (Estático)",
|
"OAuth 2.1 (Static)": "OAuth 2.1 (Estático)",
|
||||||
"OAuth ID": "OAuth ID",
|
"OAuth ID": "OAuth ID",
|
||||||
"OAuth Server URL": "",
|
"OAuth Server URL": "URL do Servidor OAuth",
|
||||||
"OAuth session disconnected": "",
|
"OAuth session disconnected": "Sessão OAuth desconectada",
|
||||||
"October": "Outubro",
|
"October": "Outubro",
|
||||||
"Off": "Desligado",
|
"Off": "Desligado",
|
||||||
"Okay, Let's Go!": "Ok, Vamos Lá!",
|
"Okay, Let's Go!": "Ok, Vamos Lá!",
|
||||||
@@ -1540,8 +1541,8 @@
|
|||||||
"Output format": "Formato de saída",
|
"Output format": "Formato de saída",
|
||||||
"Output Format": "Formato de Saída",
|
"Output Format": "Formato de Saída",
|
||||||
"Overview": "Visão Geral",
|
"Overview": "Visão Geral",
|
||||||
"PaddleOCR-vl": "",
|
"PaddleOCR-vl": "PaddleOCR-vl",
|
||||||
"PaddleOCR-vl API URL required.": "",
|
"PaddleOCR-vl API URL required.": "URL da API PaddleOCR-vl é obrigatória.",
|
||||||
"page": "página",
|
"page": "página",
|
||||||
"Page": "Página",
|
"Page": "Página",
|
||||||
"Page mode creates one document per page. Single mode combines all pages into one document for better chunking across page boundaries.": "O modo de página cria um documento por página. O modo único combina todas as páginas em um único documento para melhor divisão entre páginas.",
|
"Page mode creates one document per page. Single mode combines all pages into one document for better chunking across page boundaries.": "O modo de página cria um documento por página. O modo único combina todas as páginas em um único documento para melhor divisão entre páginas.",
|
||||||
@@ -1631,13 +1632,13 @@
|
|||||||
"Prompt created successfully": "Prompt criado com sucesso",
|
"Prompt created successfully": "Prompt criado com sucesso",
|
||||||
"Prompt Name": "Nome do prompt",
|
"Prompt Name": "Nome do prompt",
|
||||||
"Prompt Suggestions": "Sugestões de Prompt",
|
"Prompt Suggestions": "Sugestões de Prompt",
|
||||||
"Prompt Template": "",
|
"Prompt Template": "Modelo de Prompt",
|
||||||
"Prompt updated successfully": "Prompt atualizado com sucesso",
|
"Prompt updated successfully": "Prompt atualizado com sucesso",
|
||||||
"Prompts": "Prompts",
|
"Prompts": "Prompts",
|
||||||
"Prompts Access": "Acesso aos Prompts",
|
"Prompts Access": "Acesso aos Prompts",
|
||||||
"Prompts Public Sharing": "Compartilhamento Público dos Prompts",
|
"Prompts Public Sharing": "Compartilhamento Público dos Prompts",
|
||||||
"Prompts Sharing": "Compartilhamento de Prompts",
|
"Prompts Sharing": "Compartilhamento de Prompts",
|
||||||
"Provider": "",
|
"Provider": "Provedor",
|
||||||
"Public": "Público",
|
"Public": "Público",
|
||||||
"Pull \"{{searchValue}}\" from Ollama.com": "Obter \"{{searchValue}}\" de Ollama.com",
|
"Pull \"{{searchValue}}\" from Ollama.com": "Obter \"{{searchValue}}\" de Ollama.com",
|
||||||
"Pull a model from Ollama.com": "Obter um modelo de Ollama.com",
|
"Pull a model from Ollama.com": "Obter um modelo de Ollama.com",
|
||||||
@@ -1660,7 +1661,7 @@
|
|||||||
"Reason": "Razão",
|
"Reason": "Razão",
|
||||||
"Reasoning Effort": "Esforço de raciocínio",
|
"Reasoning Effort": "Esforço de raciocínio",
|
||||||
"Reasoning Tags": "Tags de raciocínio",
|
"Reasoning Tags": "Tags de raciocínio",
|
||||||
"Reasoning text...": "",
|
"Reasoning text...": "Texto de raciocínio...",
|
||||||
"Recently Used": "Usado recentemente",
|
"Recently Used": "Usado recentemente",
|
||||||
"Reconnected": "Reconectado",
|
"Reconnected": "Reconectado",
|
||||||
"Record": "Gravar",
|
"Record": "Gravar",
|
||||||
@@ -1697,7 +1698,9 @@
|
|||||||
"Remove Model": "Remover Modelo",
|
"Remove Model": "Remover Modelo",
|
||||||
"Rename": "Renomear",
|
"Rename": "Renomear",
|
||||||
"Renamed to {{name}}": "Renomeado para {{name}}",
|
"Renamed to {{name}}": "Renomeado para {{name}}",
|
||||||
|
"Render Markdown in Assistant Messages": "",
|
||||||
"Render Markdown in Previews": "Renderizar Markdown nas Pré-visualizações",
|
"Render Markdown in Previews": "Renderizar Markdown nas Pré-visualizações",
|
||||||
|
"Render Markdown in User Messages": "",
|
||||||
"Reorder Models": "Reordenar modelos",
|
"Reorder Models": "Reordenar modelos",
|
||||||
"Repeats": "Repetições",
|
"Repeats": "Repetições",
|
||||||
"Reply": "Responder",
|
"Reply": "Responder",
|
||||||
@@ -1750,7 +1753,7 @@
|
|||||||
"Schedule": "Agendar",
|
"Schedule": "Agendar",
|
||||||
"Scheduled time must be in the future": "O horário agendado deve ser no futuro.",
|
"Scheduled time must be in the future": "O horário agendado deve ser no futuro.",
|
||||||
"Scroll On Branch Change": "Rolar na mudança de ramo",
|
"Scroll On Branch Change": "Rolar na mudança de ramo",
|
||||||
"Scroll to Top": "",
|
"Scroll to Top": "Rolar para o topo",
|
||||||
"Search": "Pesquisar",
|
"Search": "Pesquisar",
|
||||||
"Search a model": "Pesquisar um modelo",
|
"Search a model": "Pesquisar um modelo",
|
||||||
"Search all emojis": "Pesquisar todos os emojis",
|
"Search all emojis": "Pesquisar todos os emojis",
|
||||||
@@ -1974,8 +1977,8 @@
|
|||||||
"Support": "Suporte",
|
"Support": "Suporte",
|
||||||
"Support this plugin:": "Apoie este plugin:",
|
"Support this plugin:": "Apoie este plugin:",
|
||||||
"Supported MIME Types": "Tipos MIME suportados",
|
"Supported MIME Types": "Tipos MIME suportados",
|
||||||
"Switch to JSON editor": "",
|
"Switch to JSON editor": "Mudar para editor JSON",
|
||||||
"Switch to visual editor": "",
|
"Switch to visual editor": "Mudar para editor visual",
|
||||||
"Sync": "Sincronizar",
|
"Sync": "Sincronizar",
|
||||||
"Sync Complete!": "Sincronização concluída!",
|
"Sync Complete!": "Sincronização concluída!",
|
||||||
"Sync directory": "Sincronizar Diretório",
|
"Sync directory": "Sincronizar Diretório",
|
||||||
@@ -2051,7 +2054,7 @@
|
|||||||
"This option sets the maximum number of tokens the model can generate in its response. Increasing this limit allows the model to provide longer answers, but it may also increase the likelihood of unhelpful or irrelevant content being generated.": "Esta opção define o número máximo de tokens que o modelo pode gerar em sua resposta. Aumentar esse limite permite que o modelo forneça respostas mais longas, mas também pode aumentar a probabilidade de geração de conteúdo inútil ou irrelevante.",
|
"This option sets the maximum number of tokens the model can generate in its response. Increasing this limit allows the model to provide longer answers, but it may also increase the likelihood of unhelpful or irrelevant content being generated.": "Esta opção define o número máximo de tokens que o modelo pode gerar em sua resposta. Aumentar esse limite permite que o modelo forneça respostas mais longas, mas também pode aumentar a probabilidade de geração de conteúdo inútil ou irrelevante.",
|
||||||
"This option will delete all existing files in the collection and replace them with newly uploaded files.": "Essa opção deletará todos os arquivos existentes na coleção e todos eles serão substituídos.",
|
"This option will delete all existing files in the collection and replace them with newly uploaded files.": "Essa opção deletará todos os arquivos existentes na coleção e todos eles serão substituídos.",
|
||||||
"This response was generated by \"{{model}}\"": "Esta resposta foi gerada por \"{{model}}\"",
|
"This response was generated by \"{{model}}\"": "Esta resposta foi gerada por \"{{model}}\"",
|
||||||
"This template contains multiple context placeholders ([context] or {{CONTEXT}}). Context will be injected at each occurrence.": "",
|
"This template contains multiple context placeholders ([context] or {{CONTEXT}}). Context will be injected at each occurrence.": "Este template contém múltiplos marcadores de contexto ([context] ou {{CONTEXT}}). O contexto será injetado em cada ocorrência.",
|
||||||
"This will delete": "Isso vai excluir",
|
"This will delete": "Isso vai excluir",
|
||||||
"This will delete <strong>{{NAME}}</strong> and <strong>all its contents</strong>.": "Esta ação excluirá <strong>{{NAME}}</strong> e <strong>todos seus conteúdos</strong>.",
|
"This will delete <strong>{{NAME}}</strong> and <strong>all its contents</strong>.": "Esta ação excluirá <strong>{{NAME}}</strong> e <strong>todos seus conteúdos</strong>.",
|
||||||
"This will delete all models including custom models": "Isto vai excluir todos os modelos, incluindo personalizados",
|
"This will delete all models including custom models": "Isto vai excluir todos os modelos, incluindo personalizados",
|
||||||
@@ -2142,7 +2145,7 @@
|
|||||||
"Unknown User": "Usuário desconhecido",
|
"Unknown User": "Usuário desconhecido",
|
||||||
"Unloads {{FROM_NOW}}": "Descarrega {{FROM_NOW}}",
|
"Unloads {{FROM_NOW}}": "Descarrega {{FROM_NOW}}",
|
||||||
"Unlock mysteries": "Desvendar mistérios",
|
"Unlock mysteries": "Desvendar mistérios",
|
||||||
"Unmute": "",
|
"Unmute": "Reativar som",
|
||||||
"Unpin": "Desfixar",
|
"Unpin": "Desfixar",
|
||||||
"Unpin from Sidebar": "Desfixar da barra lateral",
|
"Unpin from Sidebar": "Desfixar da barra lateral",
|
||||||
"Unravel secrets": "Desvendar segredos",
|
"Unravel secrets": "Desvendar segredos",
|
||||||
@@ -2221,7 +2224,7 @@
|
|||||||
"Visible": "Visível",
|
"Visible": "Visível",
|
||||||
"Visible to all users": "Visível para todos os usuários",
|
"Visible to all users": "Visível para todos os usuários",
|
||||||
"Vision": "Visão",
|
"Vision": "Visão",
|
||||||
"Visual": "",
|
"Visual": "Visual",
|
||||||
"Voice": "Voz",
|
"Voice": "Voz",
|
||||||
"Voice Input": "Entrada de voz",
|
"Voice Input": "Entrada de voz",
|
||||||
"Voice mode": "Modo de voz",
|
"Voice mode": "Modo de voz",
|
||||||
|
|||||||
@@ -285,6 +285,7 @@
|
|||||||
"Calendar deleted": "",
|
"Calendar deleted": "",
|
||||||
"Calendar name": "",
|
"Calendar name": "",
|
||||||
"Calendars": "",
|
"Calendars": "",
|
||||||
|
"Calendars Public Sharing": "",
|
||||||
"Call": "Chamar",
|
"Call": "Chamar",
|
||||||
"Call feature is not supported when using Web STT engine": "A funcionalide de Chamar não é suportada quando usa um motor Web STT",
|
"Call feature is not supported when using Web STT engine": "A funcionalide de Chamar não é suportada quando usa um motor Web STT",
|
||||||
"Camera": "Câmara",
|
"Camera": "Câmara",
|
||||||
@@ -1697,7 +1698,9 @@
|
|||||||
"Remove Model": "Remover Modelo",
|
"Remove Model": "Remover Modelo",
|
||||||
"Rename": "Renomear",
|
"Rename": "Renomear",
|
||||||
"Renamed to {{name}}": "",
|
"Renamed to {{name}}": "",
|
||||||
|
"Render Markdown in Assistant Messages": "",
|
||||||
"Render Markdown in Previews": "Renderizar Markdown em Pré-visualizações",
|
"Render Markdown in Previews": "Renderizar Markdown em Pré-visualizações",
|
||||||
|
"Render Markdown in User Messages": "",
|
||||||
"Reorder Models": "Reordenar Modelos",
|
"Reorder Models": "Reordenar Modelos",
|
||||||
"Repeats": "",
|
"Repeats": "",
|
||||||
"Reply": "Responder",
|
"Reply": "Responder",
|
||||||
|
|||||||
@@ -285,6 +285,7 @@
|
|||||||
"Calendar deleted": "",
|
"Calendar deleted": "",
|
||||||
"Calendar name": "",
|
"Calendar name": "",
|
||||||
"Calendars": "",
|
"Calendars": "",
|
||||||
|
"Calendars Public Sharing": "",
|
||||||
"Call": "Apel",
|
"Call": "Apel",
|
||||||
"Call feature is not supported when using Web STT engine": "Funcția de apel nu este suportată când se utilizează motorul Web STT",
|
"Call feature is not supported when using Web STT engine": "Funcția de apel nu este suportată când se utilizează motorul Web STT",
|
||||||
"Camera": "Cameră",
|
"Camera": "Cameră",
|
||||||
@@ -1697,7 +1698,9 @@
|
|||||||
"Remove Model": "Înlătură Modelul",
|
"Remove Model": "Înlătură Modelul",
|
||||||
"Rename": "Redenumește",
|
"Rename": "Redenumește",
|
||||||
"Renamed to {{name}}": "",
|
"Renamed to {{name}}": "",
|
||||||
|
"Render Markdown in Assistant Messages": "",
|
||||||
"Render Markdown in Previews": "",
|
"Render Markdown in Previews": "",
|
||||||
|
"Render Markdown in User Messages": "",
|
||||||
"Reorder Models": "",
|
"Reorder Models": "",
|
||||||
"Repeats": "",
|
"Repeats": "",
|
||||||
"Reply": "",
|
"Reply": "",
|
||||||
|
|||||||
@@ -286,6 +286,7 @@
|
|||||||
"Calendar deleted": "",
|
"Calendar deleted": "",
|
||||||
"Calendar name": "",
|
"Calendar name": "",
|
||||||
"Calendars": "",
|
"Calendars": "",
|
||||||
|
"Calendars Public Sharing": "",
|
||||||
"Call": "Вызов",
|
"Call": "Вызов",
|
||||||
"Call feature is not supported when using Web STT engine": "Функция вызова не поддерживается при использовании Web STT (распознавание речи) движка",
|
"Call feature is not supported when using Web STT engine": "Функция вызова не поддерживается при использовании Web STT (распознавание речи) движка",
|
||||||
"Camera": "Камера",
|
"Camera": "Камера",
|
||||||
@@ -1698,7 +1699,9 @@
|
|||||||
"Remove Model": "Удалить модель",
|
"Remove Model": "Удалить модель",
|
||||||
"Rename": "Переименовать",
|
"Rename": "Переименовать",
|
||||||
"Renamed to {{name}}": "Переименовано в {{name}}",
|
"Renamed to {{name}}": "Переименовано в {{name}}",
|
||||||
|
"Render Markdown in Assistant Messages": "",
|
||||||
"Render Markdown in Previews": "Отображать Markdown в предпросмотре",
|
"Render Markdown in Previews": "Отображать Markdown в предпросмотре",
|
||||||
|
"Render Markdown in User Messages": "",
|
||||||
"Reorder Models": "Изменение порядка моделей",
|
"Reorder Models": "Изменение порядка моделей",
|
||||||
"Repeats": "",
|
"Repeats": "",
|
||||||
"Reply": "Ответить",
|
"Reply": "Ответить",
|
||||||
|
|||||||
@@ -286,6 +286,7 @@
|
|||||||
"Calendar deleted": "",
|
"Calendar deleted": "",
|
||||||
"Calendar name": "",
|
"Calendar name": "",
|
||||||
"Calendars": "",
|
"Calendars": "",
|
||||||
|
"Calendars Public Sharing": "",
|
||||||
"Call": "Volanie",
|
"Call": "Volanie",
|
||||||
"Call feature is not supported when using Web STT engine": "Funkcia volania nie je podporovaná pri použití Web STT engine.",
|
"Call feature is not supported when using Web STT engine": "Funkcia volania nie je podporovaná pri použití Web STT engine.",
|
||||||
"Camera": "Kamera",
|
"Camera": "Kamera",
|
||||||
@@ -1698,7 +1699,9 @@
|
|||||||
"Remove Model": "Odstrániť model",
|
"Remove Model": "Odstrániť model",
|
||||||
"Rename": "Premenovať",
|
"Rename": "Premenovať",
|
||||||
"Renamed to {{name}}": "",
|
"Renamed to {{name}}": "",
|
||||||
|
"Render Markdown in Assistant Messages": "",
|
||||||
"Render Markdown in Previews": "",
|
"Render Markdown in Previews": "",
|
||||||
|
"Render Markdown in User Messages": "",
|
||||||
"Reorder Models": "",
|
"Reorder Models": "",
|
||||||
"Repeats": "",
|
"Repeats": "",
|
||||||
"Reply": "",
|
"Reply": "",
|
||||||
|
|||||||
@@ -285,6 +285,7 @@
|
|||||||
"Calendar deleted": "",
|
"Calendar deleted": "",
|
||||||
"Calendar name": "",
|
"Calendar name": "",
|
||||||
"Calendars": "",
|
"Calendars": "",
|
||||||
|
"Calendars Public Sharing": "",
|
||||||
"Call": "Позив",
|
"Call": "Позив",
|
||||||
"Call feature is not supported when using Web STT engine": "",
|
"Call feature is not supported when using Web STT engine": "",
|
||||||
"Camera": "Камера",
|
"Camera": "Камера",
|
||||||
@@ -1697,7 +1698,9 @@
|
|||||||
"Remove Model": "Уклони модел",
|
"Remove Model": "Уклони модел",
|
||||||
"Rename": "Преименуј",
|
"Rename": "Преименуј",
|
||||||
"Renamed to {{name}}": "",
|
"Renamed to {{name}}": "",
|
||||||
|
"Render Markdown in Assistant Messages": "",
|
||||||
"Render Markdown in Previews": "",
|
"Render Markdown in Previews": "",
|
||||||
|
"Render Markdown in User Messages": "",
|
||||||
"Reorder Models": "",
|
"Reorder Models": "",
|
||||||
"Repeats": "",
|
"Repeats": "",
|
||||||
"Reply": "",
|
"Reply": "",
|
||||||
|
|||||||
@@ -284,6 +284,7 @@
|
|||||||
"Calendar deleted": "",
|
"Calendar deleted": "",
|
||||||
"Calendar name": "",
|
"Calendar name": "",
|
||||||
"Calendars": "",
|
"Calendars": "",
|
||||||
|
"Calendars Public Sharing": "",
|
||||||
"Call": "Samtal",
|
"Call": "Samtal",
|
||||||
"Call feature is not supported when using Web STT engine": "Samtalsfunktionen är inte kompatibel med Web Tal-till-text motor",
|
"Call feature is not supported when using Web STT engine": "Samtalsfunktionen är inte kompatibel med Web Tal-till-text motor",
|
||||||
"Camera": "Kamera",
|
"Camera": "Kamera",
|
||||||
@@ -1696,7 +1697,9 @@
|
|||||||
"Remove Model": "Ta bort modell",
|
"Remove Model": "Ta bort modell",
|
||||||
"Rename": "Byt namn",
|
"Rename": "Byt namn",
|
||||||
"Renamed to {{name}}": "",
|
"Renamed to {{name}}": "",
|
||||||
|
"Render Markdown in Assistant Messages": "",
|
||||||
"Render Markdown in Previews": "",
|
"Render Markdown in Previews": "",
|
||||||
|
"Render Markdown in User Messages": "",
|
||||||
"Reorder Models": "Omordna modeller",
|
"Reorder Models": "Omordna modeller",
|
||||||
"Repeats": "",
|
"Repeats": "",
|
||||||
"Reply": "Svara",
|
"Reply": "Svara",
|
||||||
|
|||||||
@@ -284,6 +284,7 @@
|
|||||||
"Calendar deleted": "",
|
"Calendar deleted": "",
|
||||||
"Calendar name": "",
|
"Calendar name": "",
|
||||||
"Calendars": "",
|
"Calendars": "",
|
||||||
|
"Calendars Public Sharing": "",
|
||||||
"Call": "அழைக்கவும்",
|
"Call": "அழைக்கவும்",
|
||||||
"Call feature is not supported when using Web STT engine": "Web STT இன்ஜினைப் பயன்படுத்தும் போது அழைப்பு அம்சம் ஆதரிக்கப்படாது",
|
"Call feature is not supported when using Web STT engine": "Web STT இன்ஜினைப் பயன்படுத்தும் போது அழைப்பு அம்சம் ஆதரிக்கப்படாது",
|
||||||
"Camera": "கேமரா",
|
"Camera": "கேமரா",
|
||||||
@@ -1696,7 +1697,9 @@
|
|||||||
"Remove Model": "மாதிரியை அகற்று",
|
"Remove Model": "மாதிரியை அகற்று",
|
||||||
"Rename": "மறுபெயரிடு",
|
"Rename": "மறுபெயரிடு",
|
||||||
"Renamed to {{name}}": "{{name}} என மறுபெயரிடப்பட்டது",
|
"Renamed to {{name}}": "{{name}} என மறுபெயரிடப்பட்டது",
|
||||||
|
"Render Markdown in Assistant Messages": "",
|
||||||
"Render Markdown in Previews": "முன்னோட்டங்களில் ரெண்டர் மார்க் டவுன்",
|
"Render Markdown in Previews": "முன்னோட்டங்களில் ரெண்டர் மார்க் டவுன்",
|
||||||
|
"Render Markdown in User Messages": "",
|
||||||
"Reorder Models": "மாதிரிகளை மறுவரிசைப்படுத்தவும்",
|
"Reorder Models": "மாதிரிகளை மறுவரிசைப்படுத்தவும்",
|
||||||
"Repeats": "மீண்டும் நிகழ்வுகள்",
|
"Repeats": "மீண்டும் நிகழ்வுகள்",
|
||||||
"Reply": "பதில்",
|
"Reply": "பதில்",
|
||||||
|
|||||||
@@ -283,6 +283,7 @@
|
|||||||
"Calendar deleted": "",
|
"Calendar deleted": "",
|
||||||
"Calendar name": "",
|
"Calendar name": "",
|
||||||
"Calendars": "",
|
"Calendars": "",
|
||||||
|
"Calendars Public Sharing": "",
|
||||||
"Call": "โทร",
|
"Call": "โทร",
|
||||||
"Call feature is not supported when using Web STT engine": "ไม่รองรับฟีเจอร์การโทรเมื่อใช้เอนจิน Web STT",
|
"Call feature is not supported when using Web STT engine": "ไม่รองรับฟีเจอร์การโทรเมื่อใช้เอนจิน Web STT",
|
||||||
"Camera": "กล้อง",
|
"Camera": "กล้อง",
|
||||||
@@ -1695,7 +1696,9 @@
|
|||||||
"Remove Model": "ลบโมเดล",
|
"Remove Model": "ลบโมเดล",
|
||||||
"Rename": "เปลี่ยนชื่อ",
|
"Rename": "เปลี่ยนชื่อ",
|
||||||
"Renamed to {{name}}": "",
|
"Renamed to {{name}}": "",
|
||||||
|
"Render Markdown in Assistant Messages": "",
|
||||||
"Render Markdown in Previews": "",
|
"Render Markdown in Previews": "",
|
||||||
|
"Render Markdown in User Messages": "",
|
||||||
"Reorder Models": "จัดลำดับโมเดลใหม่",
|
"Reorder Models": "จัดลำดับโมเดลใหม่",
|
||||||
"Repeats": "",
|
"Repeats": "",
|
||||||
"Reply": "ตอบกลับ",
|
"Reply": "ตอบกลับ",
|
||||||
|
|||||||
@@ -284,6 +284,7 @@
|
|||||||
"Calendar deleted": "",
|
"Calendar deleted": "",
|
||||||
"Calendar name": "",
|
"Calendar name": "",
|
||||||
"Calendars": "",
|
"Calendars": "",
|
||||||
|
"Calendars Public Sharing": "",
|
||||||
"Call": "",
|
"Call": "",
|
||||||
"Call feature is not supported when using Web STT engine": "",
|
"Call feature is not supported when using Web STT engine": "",
|
||||||
"Camera": "",
|
"Camera": "",
|
||||||
@@ -1696,7 +1697,9 @@
|
|||||||
"Remove Model": "Modeli Aýyr",
|
"Remove Model": "Modeli Aýyr",
|
||||||
"Rename": "Adyny Üýtget",
|
"Rename": "Adyny Üýtget",
|
||||||
"Renamed to {{name}}": "",
|
"Renamed to {{name}}": "",
|
||||||
|
"Render Markdown in Assistant Messages": "",
|
||||||
"Render Markdown in Previews": "",
|
"Render Markdown in Previews": "",
|
||||||
|
"Render Markdown in User Messages": "",
|
||||||
"Reorder Models": "",
|
"Reorder Models": "",
|
||||||
"Repeats": "",
|
"Repeats": "",
|
||||||
"Reply": "",
|
"Reply": "",
|
||||||
|
|||||||
@@ -284,6 +284,7 @@
|
|||||||
"Calendar deleted": "",
|
"Calendar deleted": "",
|
||||||
"Calendar name": "",
|
"Calendar name": "",
|
||||||
"Calendars": "",
|
"Calendars": "",
|
||||||
|
"Calendars Public Sharing": "",
|
||||||
"Call": "Arama",
|
"Call": "Arama",
|
||||||
"Call feature is not supported when using Web STT engine": "Web STT motoru kullanılırken arama özelliği desteklenmiyor",
|
"Call feature is not supported when using Web STT engine": "Web STT motoru kullanılırken arama özelliği desteklenmiyor",
|
||||||
"Camera": "Kamera",
|
"Camera": "Kamera",
|
||||||
@@ -1696,7 +1697,9 @@
|
|||||||
"Remove Model": "Modeli Kaldır",
|
"Remove Model": "Modeli Kaldır",
|
||||||
"Rename": "Yeniden Adlandır",
|
"Rename": "Yeniden Adlandır",
|
||||||
"Renamed to {{name}}": "",
|
"Renamed to {{name}}": "",
|
||||||
|
"Render Markdown in Assistant Messages": "",
|
||||||
"Render Markdown in Previews": "Önizlemelerde Markdown'u İşle",
|
"Render Markdown in Previews": "Önizlemelerde Markdown'u İşle",
|
||||||
|
"Render Markdown in User Messages": "",
|
||||||
"Reorder Models": "Modelleri Yeniden Sırala",
|
"Reorder Models": "Modelleri Yeniden Sırala",
|
||||||
"Repeats": "",
|
"Repeats": "",
|
||||||
"Reply": "",
|
"Reply": "",
|
||||||
|
|||||||
@@ -284,6 +284,7 @@
|
|||||||
"Calendar deleted": "",
|
"Calendar deleted": "",
|
||||||
"Calendar name": "",
|
"Calendar name": "",
|
||||||
"Calendars": "",
|
"Calendars": "",
|
||||||
|
"Calendars Public Sharing": "",
|
||||||
"Call": "چاقىرىش",
|
"Call": "چاقىرىش",
|
||||||
"Call feature is not supported when using Web STT engine": "تور STT ماتورى ئىشلىتىلگەندە چاقىرىش ئىقتىدارى قوللىنىلمايدۇ",
|
"Call feature is not supported when using Web STT engine": "تور STT ماتورى ئىشلىتىلگەندە چاقىرىش ئىقتىدارى قوللىنىلمايدۇ",
|
||||||
"Camera": "كامېرا",
|
"Camera": "كامېرا",
|
||||||
@@ -1696,7 +1697,9 @@
|
|||||||
"Remove Model": "مودېل چىقىرىۋېتىش",
|
"Remove Model": "مودېل چىقىرىۋېتىش",
|
||||||
"Rename": "ئات ئۆزگەرتىش",
|
"Rename": "ئات ئۆزگەرتىش",
|
||||||
"Renamed to {{name}}": "",
|
"Renamed to {{name}}": "",
|
||||||
|
"Render Markdown in Assistant Messages": "",
|
||||||
"Render Markdown in Previews": "",
|
"Render Markdown in Previews": "",
|
||||||
|
"Render Markdown in User Messages": "",
|
||||||
"Reorder Models": "مودېللارنى قايتا تەرتىپلەش",
|
"Reorder Models": "مودېللارنى قايتا تەرتىپلەش",
|
||||||
"Repeats": "",
|
"Repeats": "",
|
||||||
"Reply": "",
|
"Reply": "",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user