Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8112d72b9 | ||
|
|
71ccedd2bf | ||
|
|
4225791313 | ||
|
|
e5cd1b479b | ||
|
|
61d44aa773 | ||
|
|
ef036529b5 | ||
|
|
e0bdef85ab | ||
|
|
2ce935bdb1 | ||
|
|
23b1e2cca4 | ||
|
|
05b8768fb9 | ||
|
|
173d5631ca | ||
|
|
34cd3d79e8 | ||
|
|
aede1b7a08 | ||
|
|
15b893e651 | ||
|
|
4896d30281 | ||
|
|
9be45f49e4 | ||
|
|
f1053d94c7 | ||
|
|
10cfddccd7 | ||
|
|
e5e39be90f | ||
|
|
337109e99c | ||
|
|
8adf2b33b4 | ||
|
|
1984ce42aa | ||
|
|
656de56a3e | ||
|
|
0b2abe6cb8 | ||
|
|
f364b2d205 | ||
|
|
519ff40cb6 | ||
|
|
7c7fe44328 | ||
|
|
15b5f97f89 | ||
|
|
ca0983f76b | ||
|
|
ef04a704ce | ||
|
|
5fda814669 | ||
|
|
c26e8110af | ||
|
|
9e85055b8b | ||
|
|
6d17de6c67 | ||
|
|
f4e99c80f6 | ||
|
|
09dc28df1e | ||
|
|
c748c3ede7 | ||
|
|
33308022f0 | ||
|
|
f96e8f04fc | ||
|
|
38ae91ae23 | ||
|
|
88401e91c7 | ||
|
|
8c5cfa530d | ||
|
|
d215e46315 | ||
|
|
e10e7d056e | ||
|
|
7a7d902238 | ||
|
|
24179cde2f | ||
|
|
3ae4c618e1 | ||
|
|
4a0d893995 | ||
|
|
b780d5c556 | ||
|
|
911eecac85 | ||
|
|
319d3e8856 | ||
|
|
e1b3e7252c | ||
|
|
f20cc6d7e6 | ||
|
|
f1a1e64d2e | ||
|
|
58e923fe00 | ||
|
|
f2aca781c8 | ||
|
|
ce51c481b8 | ||
|
|
9a2595f070 | ||
|
|
393c0071dc | ||
|
|
7e224e4a53 | ||
|
|
883f1dda0f | ||
|
|
12bad452fa | ||
|
|
5de60dc922 | ||
|
|
a30b106ea3 | ||
|
|
d33ad462aa | ||
|
|
3b61562c82 | ||
|
|
e5d88be4f3 | ||
|
|
b36f8d9314 | ||
|
|
626d236d13 | ||
|
|
79ecbfc757 | ||
|
|
a9b8677cc0 | ||
|
|
0f3f68b0c4 | ||
|
|
abc9b63093 | ||
|
|
64fa26bd28 | ||
|
|
2487c84f1f | ||
|
|
163211a367 | ||
|
|
f027a01ab2 | ||
|
|
370a677a38 | ||
|
|
d1d1efe212 | ||
|
|
b7549d2f6c | ||
|
|
589c4e64c1 | ||
|
|
20de5a87da | ||
|
|
ca6b18ab5c | ||
|
|
97a3b1528d | ||
|
|
d01b1d4880 | ||
|
|
df6e38039f | ||
|
|
b4c3f54f96 | ||
|
|
73776d54b8 | ||
|
|
7bda6bf767 | ||
|
|
ddcec9842f | ||
|
|
4d5b7b3014 | ||
|
|
9886ebb97f | ||
|
|
0b05b2fc7e | ||
|
|
49e7eade15 |
@@ -5,6 +5,83 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.8.3] - 2026-02-17
|
||||
|
||||
### Added
|
||||
|
||||
- ✏️ **Model edit shortcut.** Users can now edit models directly from the model selector dropdown menu, making it faster to modify model settings without navigating to separate admin or workspace pages. [Commit](https://github.com/open-webui/open-webui/commit/519ff40cb69cdc1d215cee369e9db70ff7438153)
|
||||
- 🎨 **Image edit API background support.** The image edit API now supports the background parameter for OpenAI's gpt-image-1 model, enabling background transparency control ("transparent", "opaque", "auto") when the feature is exposed in the UI. [#21459](https://github.com/open-webui/open-webui/pull/21459)
|
||||
- ⚡ **Faster model filtering.** Model access control filtering no longer makes a redundant database query to re-fetch model info that is already available in memory, reducing latency when loading model lists for non-admin users. [Commit](https://github.com/open-webui/open-webui/commit/34cd3d79e8688f589e3dd2f03415f8a8f9a13115)
|
||||
- 🔧 **Tool call display improvements.** Tool call results now display arguments in a cleaner key-value format instead of raw JSON, with a responsive layout that shows only the tool name on narrow screens and the full label on wider screens, preventing text wrapping to multiple lines. [Commit](https://github.com/open-webui/open-webui/commit/2ce935bdb10d2b26b230cd54cb649f5c667ed96a)
|
||||
- 🔄 **General improvements.** Various improvements were implemented across the application to enhance performance, stability, and security.
|
||||
- 🌐 Translations for Portuguese (Brazil), Simplified Chinese, and Traditional Chinese were enhanced and expanded.
|
||||
|
||||
### Fixed
|
||||
|
||||
- 📧 **USER_EMAIL variable fix.** The {{USER_EMAIL}} template variable now correctly returns the user's email address instead of "Unknown" in prompts. [#21479](https://github.com/open-webui/open-webui/pull/21479), [#21465](https://github.com/open-webui/open-webui/issues/21465)
|
||||
- 🖼️ **Image and file attachment handling fixes.** Uploaded images are now correctly sent to vision-enabled models, and file attachments now work even when no user text is entered alongside a system prompt. This fixes two issues where the backend was not properly processing file attachments: images weren't converted to the expected format for API requests, and file context was dropped when the user sent only a file without accompanying text. [Commit](https://github.com/open-webui/open-webui/commit/f1053d94c7ef7b8b78682dd73586b65a84d202a1), [#21477](https://github.com/open-webui/open-webui/issues/21477), [#21457](https://github.com/open-webui/open-webui/issues/21457)
|
||||
- 🛡️ **Missing function error handling.** Models that reference deleted functions no longer cause the entire /api/models endpoint to crash; instead, the missing functions are skipped and logged, allowing the rest of the models to load successfully. [#21476](https://github.com/open-webui/open-webui/pull/21476), [#21464](https://github.com/open-webui/open-webui/issues/21464)
|
||||
- 🚀 **Startup model pre-fetch error handling.** If model pre-fetching fails during app startup, the application now logs a warning and continues instead of crashing entirely. [Commit](https://github.com/open-webui/open-webui/commit/337109e99ce390f55a9085d0a301853637923779)
|
||||
- ⚙️ **Function module loading error handling.** Function modules that fail to load during startup or model processing are now caught and logged, preventing crashes when models reference functions with loading errors. [Commit](https://github.com/open-webui/open-webui/commit/15b893e651de71b033408e1b713e0b51f6829ab8)
|
||||
- 🗄️ **PostgreSQL group query fix.** The '/api/v1/groups/' endpoint no longer fails with a GROUP BY error when using PostgreSQL; member counts are now calculated using correlated subqueries for better database compatibility. [#21458](https://github.com/open-webui/open-webui/pull/21458), [#21467](https://github.com/open-webui/open-webui/issues/21467)
|
||||
|
||||
## [0.8.2] - 2026-02-16
|
||||
|
||||
### Added
|
||||
|
||||
- 🧠 **Skill content handling.** User-selected skills now have their full content injected into the chat, while model-attached skills only display name and description in the available skills list. This allows users to override skill behavior while model-attached skills remain flexible. [Commit](https://github.com/open-webui/open-webui/commit/393c0071dc612c5ac982fb37dfc0288cb9911439)
|
||||
- ⚙️ **Chat toggles now control built-in tools.** Users can now disable web search, image generation, and code execution on a per-conversation basis, even when those tools are enabled as builtin tools on the model. [#20641](https://github.com/open-webui/open-webui/issues/20641), [#21318](https://github.com/open-webui/open-webui/discussions/21318), [Commit](https://github.com/open-webui/open-webui/commit/c46ef3b63bcc1e2e9adbdd18fab82c4bbe33ff6c), [Commit](https://github.com/open-webui/open-webui/commit/f1a1e64d2e9ad953b2bc2a9543e9a308b7c669c8)
|
||||
- 🖼️ **Image preview in file modal.** Images uploaded to chats can now be previewed directly in the file management modal, making it easier to identify and manage image files. [#21413](https://github.com/open-webui/open-webui/issues/21413), [Commit](https://github.com/open-webui/open-webui/commit/e1b3e7252c1896c04d498547908f0fce111434e1)
|
||||
- 🏷️ **Batch tag operations.** Tag creation, deletion, and orphan cleanup for chats now use batch database queries instead of per-tag loops, significantly reducing database round trips when updating, archiving, or deleting chats with multiple tags. [Commit](https://github.com/open-webui/open-webui/commit/c748c3ede)
|
||||
- 💨 **Faster group list loading.** Group lists and search results now load with a single database query that joins member counts, replacing the previous pattern of fetching groups first and then counting members in a separate batch query. [Commit](https://github.com/open-webui/open-webui/commit/33308022f)
|
||||
- 🔐 **Skills sharing permissions.** Administrators can now control skills sharing and public sharing permissions per-group, matching the existing capabilities for tools, knowledge, and prompts. [Commit](https://github.com/open-webui/open-webui/commit/88401e91c)
|
||||
- ⚡ **Long content truncation in preview modals.** Citation and file content modals now truncate markdown-rendered content at 10,000 characters with a "Show all" expansion button, preventing UI jank when previewing very large documents.
|
||||
- 🌐 **Translation updates.** Translations for Spanish and German were enhanced and expanded.
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🔐 **OAuth session error handling.** Corrupted OAuth sessions are now gracefully handled and automatically cleaned up instead of causing errors. [Commit](https://github.com/open-webui/open-webui/commit/7e224e4a536b07ec008613f06592e34050e7067c)
|
||||
- 🐛 **Task model selector validation.** The task model selector in admin settings now correctly accepts models based on the new access grants system instead of rejecting all models with an incorrect error. [Commit](https://github.com/open-webui/open-webui/commit/9a2595f0706d0c9d809ae7746001cf799f98db1d)
|
||||
- 🔗 **Tool call message preservation.** Models no longer hallucinate tool outputs in multi-turn conversations because tool call history is now properly preserved instead of being merged into assistant messages. [#21098](https://github.com/open-webui/open-webui/discussions/21098), [#20600](https://github.com/open-webui/open-webui/issues/20600), [Commit](https://github.com/open-webui/open-webui/commit/f2aca781c87244cffc130aa2722e700c19a81d66)
|
||||
- 🔧 **Tool server startup initialization.** External tool servers configured via the "TOOL_SERVER_CONNECTIONS" environment variable now initialize automatically on startup, eliminating the need to manually visit the Admin Panel and save for tools to become available. This enables proper GitOps and containerized deployments. [#18140](https://github.com/open-webui/open-webui/issues/18140), [#20914](https://github.com/open-webui/open-webui/pull/20914), [Commit](https://github.com/open-webui/open-webui/commit/f20cc6d7e6da493eb75ca1618f5cbd068fa57684)
|
||||
- ♻️ **Resource handle cleanup.** File handles are now properly closed during audio transcription and pipeline uploads, preventing resource leaks that could cause system instability over time. [#21411](https://github.com/open-webui/open-webui/issues/21411)
|
||||
- ⌨️ **Strikethrough shortcut conflict fix.** Pressing Ctrl+Shift+S to toggle the sidebar no longer causes text to become struck through in the chat input, by disabling the TipTap Strike extension's default keyboard shortcut when rich text mode is off. [Commit](https://github.com/open-webui/open-webui/commit/38ae91ae2)
|
||||
- 🔧 **Tool call finish_reason fix.** API responses now correctly set finish_reason to "tool_calls" instead of "stop" when tool calls are present, fixing an issue where external API clients (such as OpenCode) would halt prematurely after tool execution when routing Ollama models through the Open WebUI API. [#20896](https://github.com/open-webui/open-webui/issues/20896)
|
||||
|
||||
## [0.8.1] - 2026-02-14
|
||||
|
||||
### Added
|
||||
|
||||
- 🚀 **Channel user active status.** Checking user active status in channels is now faster thanks to optimized database queries. [Commit](https://github.com/open-webui/open-webui/commit/ca6b18ab5cb94153a9dae233f975d36bf6b19b76)
|
||||
- 🔗 **Responses API endpoint with model routing.** The OpenAI API proxy now supports a /responses endpoint that routes requests to the correct backend based on the model field in the request, instead of always using the first configured endpoint. This enables support for backends like vLLM that provide /skills and /v1/responses endpoints. [Commit](https://github.com/open-webui/open-webui/commit/abc9b63093d65f4d74342db85b7d5df1809aa0f0), [Commit](https://github.com/open-webui/open-webui/commit/79ecbfc757f0642740d0e44fab98263d84295490)
|
||||
- ⚡ **Model and prompt list optimization.** Improved performance when loading models and prompts by pre-fetching user group IDs once instead of making multiple database queries. [Commit](https://github.com/open-webui/open-webui/commit/20de5a87da0c12e4052b50887a42ddd7228c5ef5)
|
||||
- 🗄️ **Batch access control queries.** Improved performance when loading models, prompts, and knowledge bases by replacing multiple individual access checks with single batch queries, significantly reducing database load for large deployments. [Commit](https://github.com/open-webui/open-webui/commit/589c4e64c1b7bb7a7a5abc20382b92fb860e28c2)
|
||||
- 💨 **Faster user list loading.** User lists now load significantly faster by deferring profile image loading; images are fetched separately in parallel by the browser, improving caching and reducing database load. [Commit](https://github.com/open-webui/open-webui/commit/b7549d2f6ca2843661ec79a5a1e55da9e7553368)
|
||||
- 🔍 **Web search result count.** The built-in search_web tool now respects the admin-configured "Search Result Count" setting instead of always returning 5 results when using Native Function Calling mode. [#21373](https://github.com/open-webui/open-webui/pull/21373), [#21371](https://github.com/open-webui/open-webui/issues/21371)
|
||||
- 🔐 **SCIM externalId support.** SCIM-enabled deployments can now store and manage externalId for user provisioning, enabling better integration with identity providers like Microsoft Entra ID and Okta. [#21099](https://github.com/open-webui/open-webui/pull/21099), [#21280](https://github.com/open-webui/open-webui/issues/21280), [Commit](https://github.com/open-webui/open-webui/commit/d1d1efe212b16e0052359991d67fd813125077e8)
|
||||
- 🌐 **Translation updates.** Portuguese (Brazil) translations were updated.
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🛡️ **Public sharing security fix.** Fixed a security issue where users with write access could see the Public sharing option regardless of their actual public sharing permission, and direct API calls could bypass frontend sharing restrictions. [#21358](https://github.com/open-webui/open-webui/pull/21358), [#21356](https://github.com/open-webui/open-webui/issues/21356)
|
||||
- 🔒 **Direct model access control fix.** Model access control changes now persist correctly for direct Ollama and OpenAI models that don't have database entries, and error messages display properly instead of showing "[object Object]". [Commit](https://github.com/open-webui/open-webui/commit/f027a01ab2ff3b6175af3dd13a4478c265c0544a), [#21377](https://github.com/open-webui/open-webui/issues/21377)
|
||||
- 💭 **Reasoning trace rendering performance.** Reasoning traces from models now render properly without being split into many fragments, preventing browser slowdowns during streaming responses. [#21348](https://github.com/open-webui/open-webui/issues/21348), [Commit](https://github.com/open-webui/open-webui/commit/3b61562c82448cf83710d8b6ed29b797991aa83a)
|
||||
- 🖥️ **ARM device compatibility fix.** Fixed an issue where upgrading to 0.8.0 would fail to start on ARM devices (like Raspberry Pi 4) due to torch 2.10.0 causing SIGILL errors; now pinned to torch<=2.9.1. [#21385](https://github.com/open-webui/open-webui/pull/21385), [#21349](https://github.com/open-webui/open-webui/issues/21349)
|
||||
- 🗄️ **Skills PostgreSQL compatibility fix.** Fixed a PostgreSQL compatibility issue where creating or listing skills would fail with a TypeError, while SQLite worked correctly. [#21372](https://github.com/open-webui/open-webui/pull/21372), [Commit](https://github.com/open-webui/open-webui/commit/b4c3f54f9648c4232a0fd6557703ffa66fcf4caa), [#21365](https://github.com/open-webui/open-webui/issues/21365)
|
||||
- 🗄️ **PostgreSQL analytics query fix.** Fixed an issue where retrieving chat IDs by model ID would fail on PostgreSQL due to incompatible DISTINCT ordering, while SQLite worked correctly. [#21347](https://github.com/open-webui/open-webui/issues/21347), [Commit](https://github.com/open-webui/open-webui/commit/7bda6bf767d5d5c4dc1111465096a88e10b5030e)
|
||||
- 🗃️ **SQLite cascade delete fix.** Deleting chats now properly removes all associated messages in SQLite, matching PostgreSQL behavior and preventing orphaned data. [#21362](https://github.com/open-webui/open-webui/pull/21362)
|
||||
- ☁️ **Ollama Cloud model naming fix.** Fixed an issue where using Ollama Cloud models would fail with "Model not found" errors because ":latest" was incorrectly appended to model names. [#21386](https://github.com/open-webui/open-webui/issues/21386)
|
||||
- 🛠️ **Knowledge selector tooltip z-index.** Fixed an issue where tooltips in the "Select Knowledge" dropdown were hidden behind the menu, making it difficult to read knowledge item names and descriptions. [#21375](https://github.com/open-webui/open-webui/pull/21375)
|
||||
- 🎯 **Model selector scroll position.** The model selector dropdown now correctly scrolls to and centers the currently selected model when opened, and resets scroll position when reopened. [Commit](https://github.com/open-webui/open-webui/commit/0b05b2fc7ed4c38af158707438ff404d1beb7c91)
|
||||
- 🐛 **Sync modal unexpected appearance.** Fixed an issue where the Sync Modal would appear unexpectedly after enabling the "Community Sharing" feature if the user had previously visited the app with the sync parameter. [#21376](https://github.com/open-webui/open-webui/pull/21376)
|
||||
- 🎨 **Knowledge collection layout fix.** Fixed a layout issue in the Knowledge integration menu where long collection names caused indentation artifacts and now properly truncate with ellipsis. [#21374](https://github.com/open-webui/open-webui/pull/21374)
|
||||
- 📝 **Metadata processing crash fix.** Fixed a latent bug where processing document metadata containing certain keys (content, pages, tables, paragraphs, sections, figures) would cause a RuntimeError due to dictionary mutation during iteration. [#21105](https://github.com/open-webui/open-webui/pull/21105)
|
||||
- 🔑 **Password validation regex fix.** Fixed the password validation regex by adding the raw string prefix, ensuring escape sequences like \d and \w are interpreted correctly. [#21400](https://github.com/open-webui/open-webui/pull/21400), [#21399](https://github.com/open-webui/open-webui/issues/21399)
|
||||
|
||||
### Changed
|
||||
|
||||
- ⚠️ **Database Migrations:** This release includes database schema changes; we strongly recommend backing up your database and all associated data before upgrading in production environments. If you are running a multi-worker, multi-server, or load-balanced deployment, all instances must be updated simultaneously, rolling updates are not supported and will cause application failures due to schema incompatibility.
|
||||
|
||||
## [0.8.0] - 2026-02-12
|
||||
|
||||
### Added
|
||||
|
||||
+3
-2
@@ -137,7 +137,8 @@ COPY --chown=$UID:$GID ./backend/requirements.txt ./requirements.txt
|
||||
RUN pip3 install --no-cache-dir uv && \
|
||||
if [ "$USE_CUDA" = "true" ]; then \
|
||||
# If you use CUDA the whisper and embedding model will be downloaded on first use
|
||||
pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/$USE_CUDA_DOCKER_VER --no-cache-dir && \
|
||||
# fix: pin torch<=2.9.1 - torch 2.10.0 aarch64 wheels cause SIGILL on ARM devices (RPi 4 Cortex-A72) #21349
|
||||
pip3 install 'torch<=2.9.1' torchvision torchaudio --index-url https://download.pytorch.org/whl/$USE_CUDA_DOCKER_VER --no-cache-dir && \
|
||||
uv pip install --system -r requirements.txt --no-cache-dir && \
|
||||
python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \
|
||||
python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ.get('AUXILIARY_EMBEDDING_MODEL', 'TaylorAI/bge-micro-v2'), device='cpu')" && \
|
||||
@@ -145,7 +146,7 @@ RUN pip3 install --no-cache-dir uv && \
|
||||
python -c "import os; import tiktoken; tiktoken.get_encoding(os.environ['TIKTOKEN_ENCODING_NAME'])"; \
|
||||
python -c "import nltk; nltk.download('punkt_tab')"; \
|
||||
else \
|
||||
pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu --no-cache-dir && \
|
||||
pip3 install 'torch<=2.9.1' torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu --no-cache-dir && \
|
||||
uv pip install --system -r requirements.txt --no-cache-dir && \
|
||||
if [ "$USE_SLIM" != "true" ]; then \
|
||||
python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \
|
||||
|
||||
@@ -1385,6 +1385,18 @@ USER_PERMISSIONS_WORKSPACE_TOOLS_ALLOW_PUBLIC_SHARING = (
|
||||
== "true"
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_WORKSPACE_SKILLS_ALLOW_SHARING = (
|
||||
os.environ.get("USER_PERMISSIONS_WORKSPACE_SKILLS_ALLOW_SHARING", "False").lower()
|
||||
== "true"
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_WORKSPACE_SKILLS_ALLOW_PUBLIC_SHARING = (
|
||||
os.environ.get(
|
||||
"USER_PERMISSIONS_WORKSPACE_SKILLS_ALLOW_PUBLIC_SHARING", "False"
|
||||
).lower()
|
||||
== "true"
|
||||
)
|
||||
|
||||
|
||||
USER_PERMISSIONS_NOTES_ALLOW_SHARING = (
|
||||
os.environ.get("USER_PERMISSIONS_NOTES_ALLOW_SHARING", "False").lower() == "true"
|
||||
@@ -1543,6 +1555,8 @@ DEFAULT_USER_PERMISSIONS = {
|
||||
"public_prompts": USER_PERMISSIONS_WORKSPACE_PROMPTS_ALLOW_PUBLIC_SHARING,
|
||||
"tools": USER_PERMISSIONS_WORKSPACE_TOOLS_ALLOW_SHARING,
|
||||
"public_tools": USER_PERMISSIONS_WORKSPACE_TOOLS_ALLOW_PUBLIC_SHARING,
|
||||
"skills": USER_PERMISSIONS_WORKSPACE_SKILLS_ALLOW_SHARING,
|
||||
"public_skills": USER_PERMISSIONS_WORKSPACE_SKILLS_ALLOW_PUBLIC_SHARING,
|
||||
"notes": USER_PERMISSIONS_NOTES_ALLOW_SHARING,
|
||||
"public_notes": USER_PERMISSIONS_NOTES_ALLOW_PUBLIC_SHARING,
|
||||
},
|
||||
|
||||
@@ -478,7 +478,7 @@ ENABLE_PASSWORD_VALIDATION = (
|
||||
)
|
||||
PASSWORD_VALIDATION_REGEX_PATTERN = os.environ.get(
|
||||
"PASSWORD_VALIDATION_REGEX_PATTERN",
|
||||
"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\w\s]).{8,}$",
|
||||
r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\w\s]).{8,}$",
|
||||
)
|
||||
|
||||
|
||||
@@ -572,6 +572,14 @@ ENABLE_SCIM = (
|
||||
== "true"
|
||||
)
|
||||
SCIM_TOKEN = os.environ.get("SCIM_TOKEN", "")
|
||||
SCIM_AUTH_PROVIDER = os.environ.get("SCIM_AUTH_PROVIDER", "")
|
||||
|
||||
if ENABLE_SCIM and not SCIM_AUTH_PROVIDER:
|
||||
log.warning(
|
||||
"SCIM is enabled but SCIM_AUTH_PROVIDER is not set. "
|
||||
"Set SCIM_AUTH_PROVIDER to the OAuth provider name (e.g. 'microsoft', 'oidc') "
|
||||
"to enable externalId storage."
|
||||
)
|
||||
|
||||
####################################
|
||||
# LICENSE_KEY
|
||||
|
||||
@@ -64,6 +64,7 @@ from open_webui.socket.main import (
|
||||
MODELS,
|
||||
app as socket_app,
|
||||
periodic_usage_pool_cleanup,
|
||||
periodic_session_pool_cleanup,
|
||||
get_event_emitter,
|
||||
get_models_in_use,
|
||||
)
|
||||
@@ -517,6 +518,7 @@ from open_webui.utils.middleware import (
|
||||
process_chat_payload,
|
||||
process_chat_response,
|
||||
)
|
||||
from open_webui.utils.tools import set_tool_servers
|
||||
|
||||
from open_webui.utils.auth import (
|
||||
get_license_data,
|
||||
@@ -634,11 +636,37 @@ async def lifespan(app: FastAPI):
|
||||
limiter.total_tokens = THREAD_POOL_SIZE
|
||||
|
||||
asyncio.create_task(periodic_usage_pool_cleanup())
|
||||
asyncio.create_task(periodic_session_pool_cleanup())
|
||||
|
||||
if app.state.config.ENABLE_BASE_MODELS_CACHE:
|
||||
await get_all_models(
|
||||
Request(
|
||||
# Creating a mock request object to pass to get_all_models
|
||||
try:
|
||||
await get_all_models(
|
||||
Request(
|
||||
# Creating a mock request object to pass to get_all_models
|
||||
{
|
||||
"type": "http",
|
||||
"asgi.version": "3.0",
|
||||
"asgi.spec_version": "2.0",
|
||||
"method": "GET",
|
||||
"path": "/internal",
|
||||
"query_string": b"",
|
||||
"headers": Headers({}).raw,
|
||||
"client": ("127.0.0.1", 12345),
|
||||
"server": ("127.0.0.1", 80),
|
||||
"scheme": "http",
|
||||
"app": app,
|
||||
}
|
||||
),
|
||||
None,
|
||||
)
|
||||
except Exception as e:
|
||||
log.warning(f"Failed to pre-fetch models at startup: {e}")
|
||||
|
||||
# Pre-fetch tool server specs so the first request doesn't pay the latency cost
|
||||
if len(app.state.config.TOOL_SERVER_CONNECTIONS) > 0:
|
||||
log.info("Initializing tool servers...")
|
||||
try:
|
||||
mock_request = Request(
|
||||
{
|
||||
"type": "http",
|
||||
"asgi.version": "3.0",
|
||||
@@ -652,9 +680,11 @@ async def lifespan(app: FastAPI):
|
||||
"scheme": "http",
|
||||
"app": app,
|
||||
}
|
||||
),
|
||||
None,
|
||||
)
|
||||
)
|
||||
await set_tool_servers(mock_request)
|
||||
log.info(f"Initialized {len(app.state.TOOL_SERVERS)} tool server(s)")
|
||||
except Exception as e:
|
||||
log.warning(f"Failed to initialize tool servers at startup: {e}")
|
||||
|
||||
yield
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
"""add scim column to user table
|
||||
|
||||
Revision ID: b2c3d4e5f6a7
|
||||
Revises: a1b2c3d4e5f6
|
||||
Create Date: 2026-02-13 14:19:00.000000
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "b2c3d4e5f6a7"
|
||||
down_revision: Union[str, None] = "a1b2c3d4e5f6"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column("user", sa.Column("scim", sa.JSON(), nullable=True))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("user", "scim")
|
||||
@@ -515,6 +515,62 @@ class AccessGrantsTable:
|
||||
)
|
||||
return exists is not None
|
||||
|
||||
def get_accessible_resource_ids(
|
||||
self,
|
||||
user_id: str,
|
||||
resource_type: str,
|
||||
resource_ids: list[str],
|
||||
permission: str = "read",
|
||||
user_group_ids: Optional[set[str]] = None,
|
||||
db: Optional[Session] = None,
|
||||
) -> set[str]:
|
||||
"""
|
||||
Batch check: return the subset of resource_ids that the user can access.
|
||||
|
||||
This replaces calling has_access() in a loop (N+1) with a single query.
|
||||
"""
|
||||
if not resource_ids:
|
||||
return set()
|
||||
|
||||
with get_db_context(db) as db:
|
||||
conditions = [
|
||||
and_(
|
||||
AccessGrant.principal_type == "user",
|
||||
AccessGrant.principal_id == "*",
|
||||
),
|
||||
and_(
|
||||
AccessGrant.principal_type == "user",
|
||||
AccessGrant.principal_id == user_id,
|
||||
),
|
||||
]
|
||||
|
||||
if user_group_ids is None:
|
||||
from open_webui.models.groups import Groups
|
||||
|
||||
user_groups = Groups.get_groups_by_member_id(user_id, db=db)
|
||||
user_group_ids = {group.id for group in user_groups}
|
||||
|
||||
if user_group_ids:
|
||||
conditions.append(
|
||||
and_(
|
||||
AccessGrant.principal_type == "group",
|
||||
AccessGrant.principal_id.in_(user_group_ids),
|
||||
)
|
||||
)
|
||||
|
||||
rows = (
|
||||
db.query(AccessGrant.resource_id)
|
||||
.filter(
|
||||
AccessGrant.resource_type == resource_type,
|
||||
AccessGrant.resource_id.in_(resource_ids),
|
||||
AccessGrant.permission == permission,
|
||||
or_(*conditions),
|
||||
)
|
||||
.distinct()
|
||||
.all()
|
||||
)
|
||||
return {row[0] for row in rows}
|
||||
|
||||
def get_users_with_access(
|
||||
self,
|
||||
resource_type: str,
|
||||
|
||||
@@ -15,6 +15,7 @@ from sqlalchemy import (
|
||||
Text,
|
||||
JSON,
|
||||
Index,
|
||||
func,
|
||||
)
|
||||
|
||||
####################
|
||||
@@ -279,25 +280,26 @@ class ChatMessageTable:
|
||||
db: Optional[Session] = None,
|
||||
) -> list[str]:
|
||||
"""Get distinct chat_ids that used a specific model."""
|
||||
from sqlalchemy import distinct
|
||||
|
||||
with get_db_context(db) as db:
|
||||
query = db.query(distinct(ChatMessage.chat_id)).filter(
|
||||
ChatMessage.model_id == model_id
|
||||
)
|
||||
query = db.query(
|
||||
ChatMessage.chat_id,
|
||||
func.max(ChatMessage.created_at).label("last_message_at"),
|
||||
).filter(ChatMessage.model_id == model_id)
|
||||
if start_date:
|
||||
query = query.filter(ChatMessage.created_at >= start_date)
|
||||
if end_date:
|
||||
query = query.filter(ChatMessage.created_at <= end_date)
|
||||
|
||||
# Order by most recent message in each chat
|
||||
# Group by chat_id and order by most recent message in each chat
|
||||
chat_ids = (
|
||||
query.order_by(ChatMessage.created_at.desc())
|
||||
query.group_by(ChatMessage.chat_id)
|
||||
.order_by(func.max(ChatMessage.created_at).desc())
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
.all()
|
||||
)
|
||||
return [chat_id for (chat_id,) in chat_ids]
|
||||
return [chat_id for chat_id, _ in chat_ids]
|
||||
|
||||
def delete_messages_by_chat_id(
|
||||
self, chat_id: str, db: Optional[Session] = None
|
||||
|
||||
@@ -8,7 +8,7 @@ from sqlalchemy.orm import Session
|
||||
from open_webui.internal.db import Base, JSONField, get_db, get_db_context
|
||||
from open_webui.models.tags import TagModel, Tag, Tags
|
||||
from open_webui.models.folders import Folders
|
||||
from open_webui.models.chat_messages import ChatMessages
|
||||
from open_webui.models.chat_messages import ChatMessage, ChatMessages
|
||||
from open_webui.utils.misc import sanitize_data_for_db, sanitize_text_for_db
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
@@ -431,22 +431,29 @@ class ChatTable:
|
||||
def update_chat_tags_by_id(
|
||||
self, id: str, tags: list[str], user
|
||||
) -> Optional[ChatModel]:
|
||||
chat = self.get_chat_by_id(id)
|
||||
if chat is None:
|
||||
return None
|
||||
with get_db_context() as db:
|
||||
chat = db.get(Chat, id)
|
||||
if chat is None:
|
||||
return None
|
||||
|
||||
self.delete_all_tags_by_id_and_user_id(id, user.id)
|
||||
old_tags = chat.meta.get("tags", [])
|
||||
new_tags = [t for t in tags if t.replace(" ", "_").lower() != "none"]
|
||||
new_tag_ids = [t.replace(" ", "_").lower() for t in new_tags]
|
||||
|
||||
for tag in chat.meta.get("tags", []):
|
||||
if self.count_chats_by_tag_name_and_user_id(tag, user.id) == 0:
|
||||
Tags.delete_tag_by_name_and_user_id(tag, user.id)
|
||||
# Single meta update
|
||||
chat.meta = {**chat.meta, "tags": new_tag_ids}
|
||||
db.commit()
|
||||
db.refresh(chat)
|
||||
|
||||
for tag_name in tags:
|
||||
if tag_name.lower() == "none":
|
||||
continue
|
||||
# Batch-create any missing tag rows
|
||||
Tags.ensure_tags_exist(new_tags, user.id, db=db)
|
||||
|
||||
self.add_chat_tag_by_id_and_user_id_and_tag_name(id, user.id, tag_name)
|
||||
return self.get_chat_by_id(id)
|
||||
# Clean up orphaned old tags in one query
|
||||
removed = set(old_tags) - set(new_tag_ids)
|
||||
if removed:
|
||||
self.delete_orphan_tags_for_user(list(removed), user.id, db=db)
|
||||
|
||||
return ChatModel.model_validate(chat)
|
||||
|
||||
def get_chat_title_by_id(self, id: str) -> Optional[str]:
|
||||
chat = self.get_chat_by_id(id)
|
||||
@@ -621,6 +628,13 @@ class ChatTable:
|
||||
) -> bool:
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
# Use subquery to delete chat_messages for shared chats
|
||||
shared_chat_id_subquery = (
|
||||
db.query(Chat.id).filter_by(user_id=f"shared-{chat_id}").subquery()
|
||||
)
|
||||
db.query(ChatMessage).filter(
|
||||
ChatMessage.chat_id.in_(shared_chat_id_subquery)
|
||||
).delete(synchronize_session=False)
|
||||
db.query(Chat).filter_by(user_id=f"shared-{chat_id}").delete()
|
||||
db.commit()
|
||||
|
||||
@@ -1260,8 +1274,8 @@ class ChatTable:
|
||||
) -> list[TagModel]:
|
||||
with get_db_context(db) as db:
|
||||
chat = db.get(Chat, id)
|
||||
tags = chat.meta.get("tags", [])
|
||||
return [Tags.get_tag_by_name_and_user_id(tag, user_id) for tag in tags]
|
||||
tag_ids = chat.meta.get("tags", [])
|
||||
return Tags.get_tags_by_ids_and_user_id(tag_ids, user_id, db=db)
|
||||
|
||||
def get_chat_list_by_user_id_and_tag_name(
|
||||
self,
|
||||
@@ -1302,20 +1316,16 @@ class ChatTable:
|
||||
def add_chat_tag_by_id_and_user_id_and_tag_name(
|
||||
self, id: str, user_id: str, tag_name: str, db: Optional[Session] = None
|
||||
) -> Optional[ChatModel]:
|
||||
tag = Tags.get_tag_by_name_and_user_id(tag_name, user_id)
|
||||
if tag is None:
|
||||
tag = Tags.insert_new_tag(tag_name, user_id)
|
||||
tag_id = tag_name.replace(" ", "_").lower()
|
||||
Tags.ensure_tags_exist([tag_name], user_id, db=db)
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
chat = db.get(Chat, id)
|
||||
|
||||
tag_id = tag.id
|
||||
if tag_id not in chat.meta.get("tags", []):
|
||||
chat.meta = {
|
||||
**chat.meta,
|
||||
"tags": list(set(chat.meta.get("tags", []) + [tag_id])),
|
||||
}
|
||||
|
||||
db.commit()
|
||||
db.refresh(chat)
|
||||
return ChatModel.model_validate(chat)
|
||||
@@ -1325,40 +1335,53 @@ class ChatTable:
|
||||
def count_chats_by_tag_name_and_user_id(
|
||||
self, tag_name: str, user_id: str, db: Optional[Session] = None
|
||||
) -> int:
|
||||
with get_db_context(db) as db: # Assuming `get_db()` returns a session object
|
||||
with get_db_context(db) as db:
|
||||
query = db.query(Chat).filter_by(user_id=user_id, archived=False)
|
||||
|
||||
# Normalize the tag_name for consistency
|
||||
tag_id = tag_name.replace(" ", "_").lower()
|
||||
|
||||
if db.bind.dialect.name == "sqlite":
|
||||
# SQLite JSON1 support for querying the tags inside the `meta` JSON field
|
||||
query = query.filter(
|
||||
text(
|
||||
f"EXISTS (SELECT 1 FROM json_each(Chat.meta, '$.tags') WHERE json_each.value = :tag_id)"
|
||||
"EXISTS (SELECT 1 FROM json_each(Chat.meta, '$.tags') WHERE json_each.value = :tag_id)"
|
||||
)
|
||||
).params(tag_id=tag_id)
|
||||
|
||||
elif db.bind.dialect.name == "postgresql":
|
||||
# PostgreSQL JSONB support for querying the tags inside the `meta` JSON field
|
||||
query = query.filter(
|
||||
text(
|
||||
"EXISTS (SELECT 1 FROM json_array_elements_text(Chat.meta->'tags') elem WHERE elem = :tag_id)"
|
||||
)
|
||||
).params(tag_id=tag_id)
|
||||
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
f"Unsupported dialect: {db.bind.dialect.name}"
|
||||
)
|
||||
|
||||
# Get the count of matching records
|
||||
count = query.count()
|
||||
return query.count()
|
||||
|
||||
# Debugging output for inspection
|
||||
log.info(f"Count of chats for tag '{tag_name}': {count}")
|
||||
def delete_orphan_tags_for_user(
|
||||
self,
|
||||
tag_ids: list[str],
|
||||
user_id: str,
|
||||
threshold: int = 0,
|
||||
db: Optional[Session] = None,
|
||||
) -> None:
|
||||
"""Delete tag rows from *tag_ids* that appear in at most *threshold*
|
||||
non-archived chats for *user_id*. One query to find orphans, one to
|
||||
delete them.
|
||||
|
||||
return count
|
||||
Use threshold=0 after a tag is already removed from a chat's meta.
|
||||
Use threshold=1 when the chat itself is about to be deleted (the
|
||||
referencing chat still exists at query time).
|
||||
"""
|
||||
if not tag_ids:
|
||||
return
|
||||
with get_db_context(db) as db:
|
||||
orphans = []
|
||||
for tag_id in tag_ids:
|
||||
count = self.count_chats_by_tag_name_and_user_id(tag_id, user_id, db=db)
|
||||
if count <= threshold:
|
||||
orphans.append(tag_id)
|
||||
Tags.delete_tags_by_ids_and_user_id(orphans, user_id, db=db)
|
||||
|
||||
def count_chats_by_folder_id_and_user_id(
|
||||
self, folder_id: str, user_id: str, db: Optional[Session] = None
|
||||
@@ -1410,6 +1433,7 @@ class ChatTable:
|
||||
def delete_chat_by_id(self, id: str, db: Optional[Session] = None) -> bool:
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
db.query(ChatMessage).filter_by(chat_id=id).delete()
|
||||
db.query(Chat).filter_by(id=id).delete()
|
||||
db.commit()
|
||||
|
||||
@@ -1422,6 +1446,7 @@ class ChatTable:
|
||||
) -> bool:
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
db.query(ChatMessage).filter_by(chat_id=id).delete()
|
||||
db.query(Chat).filter_by(id=id, user_id=user_id).delete()
|
||||
db.commit()
|
||||
|
||||
@@ -1436,6 +1461,12 @@ class ChatTable:
|
||||
with get_db_context(db) as db:
|
||||
self.delete_shared_chats_by_user_id(user_id, db=db)
|
||||
|
||||
chat_id_subquery = (
|
||||
db.query(Chat.id).filter_by(user_id=user_id).subquery()
|
||||
)
|
||||
db.query(ChatMessage).filter(
|
||||
ChatMessage.chat_id.in_(chat_id_subquery)
|
||||
).delete(synchronize_session=False)
|
||||
db.query(Chat).filter_by(user_id=user_id).delete()
|
||||
db.commit()
|
||||
|
||||
@@ -1448,6 +1479,14 @@ class ChatTable:
|
||||
) -> bool:
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
chat_id_subquery = (
|
||||
db.query(Chat.id)
|
||||
.filter_by(user_id=user_id, folder_id=folder_id)
|
||||
.subquery()
|
||||
)
|
||||
db.query(ChatMessage).filter(
|
||||
ChatMessage.chat_id.in_(chat_id_subquery)
|
||||
).delete(synchronize_session=False)
|
||||
db.query(Chat).filter_by(user_id=user_id, folder_id=folder_id).delete()
|
||||
db.commit()
|
||||
|
||||
@@ -1481,6 +1520,15 @@ class ChatTable:
|
||||
chats_by_user = db.query(Chat).filter_by(user_id=user_id).all()
|
||||
shared_chat_ids = [f"shared-{chat.id}" for chat in chats_by_user]
|
||||
|
||||
# Use subquery to delete chat_messages for shared chats
|
||||
shared_id_subq = (
|
||||
db.query(Chat.id)
|
||||
.filter(Chat.user_id.in_(shared_chat_ids))
|
||||
.subquery()
|
||||
)
|
||||
db.query(ChatMessage).filter(
|
||||
ChatMessage.chat_id.in_(shared_id_subq)
|
||||
).delete(synchronize_session=False)
|
||||
db.query(Chat).filter(Chat.user_id.in_(shared_chat_ids)).delete()
|
||||
db.commit()
|
||||
|
||||
|
||||
@@ -164,7 +164,14 @@ class GroupTable:
|
||||
|
||||
def get_groups(self, filter, db: Optional[Session] = None) -> list[GroupResponse]:
|
||||
with get_db_context(db) as db:
|
||||
query = db.query(Group)
|
||||
member_count = (
|
||||
select(func.count(GroupMember.user_id))
|
||||
.where(GroupMember.group_id == Group.id)
|
||||
.correlate(Group)
|
||||
.scalar_subquery()
|
||||
.label("member_count")
|
||||
)
|
||||
query = db.query(Group, member_count)
|
||||
|
||||
if filter:
|
||||
if "query" in filter:
|
||||
@@ -179,9 +186,6 @@ class GroupTable:
|
||||
json_share_lower = func.lower(json_share_str)
|
||||
|
||||
if share_value:
|
||||
# Groups open to anyone: data is null, config.share is null, or share is true
|
||||
# Use case-insensitive string comparison to handle variations like "True", "TRUE"
|
||||
# Handle potential JSON boolean to string casting issues by checking for both string 'true' and boolean equivalence if possible,
|
||||
anyone_can_share = or_(
|
||||
Group.data.is_(None),
|
||||
json_share_str.is_(None),
|
||||
@@ -190,7 +194,6 @@ class GroupTable:
|
||||
)
|
||||
|
||||
if member_id:
|
||||
# Also include member-only groups where user is a member
|
||||
member_groups_select = select(GroupMember.group_id).where(
|
||||
GroupMember.user_id == member_id
|
||||
)
|
||||
@@ -211,21 +214,24 @@ class GroupTable:
|
||||
else:
|
||||
# Only apply member_id filter when share filter is NOT present
|
||||
if "member_id" in filter:
|
||||
query = query.join(
|
||||
GroupMember, GroupMember.group_id == Group.id
|
||||
).filter(GroupMember.user_id == filter["member_id"])
|
||||
query = query.filter(
|
||||
Group.id.in_(
|
||||
select(GroupMember.group_id).where(
|
||||
GroupMember.user_id == filter["member_id"]
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
results = query.order_by(Group.updated_at.desc()).all()
|
||||
|
||||
groups = query.order_by(Group.updated_at.desc()).all()
|
||||
group_ids = [group.id for group in groups]
|
||||
member_counts = self.get_group_member_counts_by_ids(group_ids, db=db)
|
||||
return [
|
||||
GroupResponse.model_validate(
|
||||
{
|
||||
**GroupModel.model_validate(group).model_dump(),
|
||||
"member_count": member_counts.get(group.id, 0),
|
||||
"member_count": count or 0,
|
||||
}
|
||||
)
|
||||
for group in groups
|
||||
for group, count in results
|
||||
]
|
||||
|
||||
def search_groups(
|
||||
@@ -242,31 +248,46 @@ class GroupTable:
|
||||
if "query" in filter:
|
||||
query = query.filter(Group.name.ilike(f"%{filter['query']}%"))
|
||||
if "member_id" in filter:
|
||||
query = query.join(
|
||||
GroupMember, GroupMember.group_id == Group.id
|
||||
).filter(GroupMember.user_id == filter["member_id"])
|
||||
query = query.filter(
|
||||
Group.id.in_(
|
||||
select(GroupMember.group_id).where(
|
||||
GroupMember.user_id == filter["member_id"]
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if "share" in filter:
|
||||
# 'share' is stored in data JSON, support both sqlite and postgres
|
||||
share_value = filter["share"]
|
||||
print("Filtering by share:", share_value)
|
||||
query = query.filter(
|
||||
Group.data.op("->>")("share") == str(share_value)
|
||||
)
|
||||
|
||||
total = query.count()
|
||||
query = query.order_by(Group.updated_at.desc())
|
||||
groups = query.offset(skip).limit(limit).all()
|
||||
group_ids = [group.id for group in groups]
|
||||
member_counts = self.get_group_member_counts_by_ids(group_ids, db=db)
|
||||
|
||||
member_count = (
|
||||
select(func.count(GroupMember.user_id))
|
||||
.where(GroupMember.group_id == Group.id)
|
||||
.correlate(Group)
|
||||
.scalar_subquery()
|
||||
.label("member_count")
|
||||
)
|
||||
results = (
|
||||
query.add_columns(member_count)
|
||||
.order_by(Group.updated_at.desc())
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
.all()
|
||||
)
|
||||
|
||||
return {
|
||||
"items": [
|
||||
GroupResponse.model_validate(
|
||||
**GroupModel.model_validate(group).model_dump(),
|
||||
member_count=member_counts.get(group.id, 0),
|
||||
{
|
||||
**GroupModel.model_validate(group).model_dump(),
|
||||
"member_count": count or 0,
|
||||
}
|
||||
)
|
||||
for group in groups
|
||||
for group, count in results
|
||||
],
|
||||
"total": total,
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ class OAuthSessionTable:
|
||||
decrypted = self.fernet.decrypt(token.encode()).decode()
|
||||
return json.loads(decrypted)
|
||||
except Exception as e:
|
||||
log.error(f"Error decrypting tokens: {e}")
|
||||
log.error(f"Error decrypting tokens: {type(e).__name__}: {e}")
|
||||
raise
|
||||
|
||||
def create_session(
|
||||
@@ -209,8 +209,15 @@ class OAuthSessionTable:
|
||||
|
||||
results = []
|
||||
for session in sessions:
|
||||
session.token = self._decrypt_token(session.token)
|
||||
results.append(OAuthSessionModel.model_validate(session))
|
||||
try:
|
||||
session.token = self._decrypt_token(session.token)
|
||||
results.append(OAuthSessionModel.model_validate(session))
|
||||
except Exception as e:
|
||||
log.warning(
|
||||
f"Skipping OAuth session {session.id} due to decryption failure, deleting corrupted session: {type(e).__name__}: {e}"
|
||||
)
|
||||
db.query(OAuthSession).filter_by(id=session.id).delete()
|
||||
db.commit()
|
||||
|
||||
return results
|
||||
|
||||
|
||||
@@ -3,13 +3,13 @@ import time
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
from open_webui.internal.db import Base, JSONField, get_db, get_db_context
|
||||
from open_webui.internal.db import Base, get_db, get_db_context
|
||||
from open_webui.models.users import Users, UserResponse
|
||||
from open_webui.models.groups import Groups
|
||||
from open_webui.models.access_grants import AccessGrantModel, AccessGrants
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from sqlalchemy import BigInteger, Boolean, Column, String, Text, or_
|
||||
from sqlalchemy import JSON, BigInteger, Boolean, Column, String, Text, or_
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -26,7 +26,7 @@ class Skill(Base):
|
||||
name = Column(Text, unique=True)
|
||||
description = Column(Text, nullable=True)
|
||||
content = Column(Text)
|
||||
meta = Column(JSONField)
|
||||
meta = Column(JSON)
|
||||
is_active = Column(Boolean, default=True)
|
||||
|
||||
updated_at = Column(BigInteger)
|
||||
|
||||
@@ -115,5 +115,45 @@ class TagTable:
|
||||
log.error(f"delete_tag: {e}")
|
||||
return False
|
||||
|
||||
def delete_tags_by_ids_and_user_id(
|
||||
self, ids: list[str], user_id: str, db: Optional[Session] = None
|
||||
) -> bool:
|
||||
"""Delete all tags whose id is in *ids* for the given user, in one query."""
|
||||
if not ids:
|
||||
return True
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
db.query(Tag).filter(Tag.id.in_(ids), Tag.user_id == user_id).delete(
|
||||
synchronize_session=False
|
||||
)
|
||||
db.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
log.error(f"delete_tags_by_ids: {e}")
|
||||
return False
|
||||
|
||||
def ensure_tags_exist(
|
||||
self, names: list[str], user_id: str, db: Optional[Session] = None
|
||||
) -> None:
|
||||
"""Create tag rows for any *names* that don't already exist for *user_id*."""
|
||||
if not names:
|
||||
return
|
||||
ids = [n.replace(" ", "_").lower() for n in names]
|
||||
with get_db_context(db) as db:
|
||||
existing = {
|
||||
t.id
|
||||
for t in db.query(Tag.id)
|
||||
.filter(Tag.id.in_(ids), Tag.user_id == user_id)
|
||||
.all()
|
||||
}
|
||||
new_tags = [
|
||||
Tag(id=tag_id, name=name, user_id=user_id)
|
||||
for tag_id, name in zip(ids, names)
|
||||
if tag_id not in existing
|
||||
]
|
||||
if new_tags:
|
||||
db.add_all(new_tags)
|
||||
db.commit()
|
||||
|
||||
|
||||
Tags = TagTable()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.orm import Session, defer
|
||||
from open_webui.internal.db import Base, JSONField, get_db, get_db_context
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ from open_webui.utils.misc import throttle
|
||||
from open_webui.utils.validate import validate_profile_image_url
|
||||
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, field_validator
|
||||
from pydantic import BaseModel, ConfigDict, field_validator, model_validator
|
||||
from sqlalchemy import (
|
||||
BigInteger,
|
||||
JSON,
|
||||
@@ -28,7 +28,7 @@ from sqlalchemy import (
|
||||
select,
|
||||
cast,
|
||||
)
|
||||
from sqlalchemy import or_, case
|
||||
from sqlalchemy import or_, case, func
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
|
||||
import datetime
|
||||
@@ -71,6 +71,7 @@ class User(Base):
|
||||
settings = Column(JSON, nullable=True)
|
||||
|
||||
oauth = Column(JSON, nullable=True)
|
||||
scim = Column(JSON, nullable=True)
|
||||
|
||||
last_active_at = Column(BigInteger)
|
||||
updated_at = Column(BigInteger)
|
||||
@@ -86,7 +87,7 @@ class UserModel(BaseModel):
|
||||
|
||||
name: str
|
||||
|
||||
profile_image_url: str
|
||||
profile_image_url: Optional[str] = None
|
||||
profile_banner_image_url: Optional[str] = None
|
||||
|
||||
bio: Optional[str] = None
|
||||
@@ -103,6 +104,7 @@ class UserModel(BaseModel):
|
||||
settings: Optional[UserSettings] = None
|
||||
|
||||
oauth: Optional[dict] = None
|
||||
scim: Optional[dict] = None
|
||||
|
||||
last_active_at: int # timestamp in epoch
|
||||
updated_at: int # timestamp in epoch
|
||||
@@ -110,6 +112,12 @@ class UserModel(BaseModel):
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def set_profile_image_url(self):
|
||||
if not self.profile_image_url:
|
||||
self.profile_image_url = f"/api/v1/users/{self.id}/profile/image"
|
||||
return self
|
||||
|
||||
|
||||
class UserStatusModel(UserModel):
|
||||
is_active: bool = False
|
||||
@@ -315,8 +323,12 @@ class UsersTable:
|
||||
) -> Optional[UserModel]:
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
user = db.query(User).filter_by(email=email).first()
|
||||
return UserModel.model_validate(user)
|
||||
user = (
|
||||
db.query(User)
|
||||
.filter(func.lower(User.email) == email.lower())
|
||||
.first()
|
||||
)
|
||||
return UserModel.model_validate(user) if user else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@@ -341,6 +353,29 @@ class UsersTable:
|
||||
# You may want to log the exception here
|
||||
return None
|
||||
|
||||
def get_user_by_scim_external_id(
|
||||
self, provider: str, external_id: str, db: Optional[Session] = None
|
||||
) -> Optional[UserModel]:
|
||||
try:
|
||||
with get_db_context(db) as db: # type: Session
|
||||
dialect_name = db.bind.dialect.name
|
||||
|
||||
query = db.query(User)
|
||||
if dialect_name == "sqlite":
|
||||
query = query.filter(
|
||||
User.scim.contains({provider: {"external_id": external_id}})
|
||||
)
|
||||
elif dialect_name == "postgresql":
|
||||
query = query.filter(
|
||||
User.scim[provider].cast(JSONB)["external_id"].astext
|
||||
== external_id
|
||||
)
|
||||
|
||||
user = query.first()
|
||||
return UserModel.model_validate(user) if user else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_users(
|
||||
self,
|
||||
filter: Optional[dict] = None,
|
||||
@@ -350,7 +385,7 @@ class UsersTable:
|
||||
) -> dict:
|
||||
with get_db_context(db) as db:
|
||||
# Join GroupMember so we can order by group_id when requested
|
||||
query = db.query(User)
|
||||
query = db.query(User).options(defer(User.profile_image_url))
|
||||
|
||||
if filter:
|
||||
query_key = filter.get("query")
|
||||
@@ -485,6 +520,7 @@ class UsersTable:
|
||||
with get_db_context(db) as db:
|
||||
users = (
|
||||
db.query(User)
|
||||
.options(defer(User.profile_image_url))
|
||||
.join(GroupMember, User.id == GroupMember.user_id)
|
||||
.filter(GroupMember.group_id == group_id)
|
||||
.all()
|
||||
@@ -495,7 +531,12 @@ class UsersTable:
|
||||
self, user_ids: list[str], db: Optional[Session] = None
|
||||
) -> list[UserStatusModel]:
|
||||
with get_db_context(db) as db:
|
||||
users = db.query(User).filter(User.id.in_(user_ids)).all()
|
||||
users = (
|
||||
db.query(User)
|
||||
.options(defer(User.profile_image_url))
|
||||
.filter(User.id.in_(user_ids))
|
||||
.all()
|
||||
)
|
||||
return [UserModel.model_validate(user) for user in users]
|
||||
|
||||
def get_num_users(self, db: Optional[Session] = None) -> Optional[int]:
|
||||
@@ -635,6 +676,38 @@ class UsersTable:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def update_user_scim_by_id(
|
||||
self,
|
||||
id: str,
|
||||
provider: str,
|
||||
external_id: str,
|
||||
db: Optional[Session] = None,
|
||||
) -> Optional[UserModel]:
|
||||
"""
|
||||
Update or insert a SCIM provider/external_id pair into the user's scim JSON field.
|
||||
Example resulting structure:
|
||||
{
|
||||
"microsoft": { "external_id": "abc" },
|
||||
"okta": { "external_id": "def" }
|
||||
}
|
||||
"""
|
||||
try:
|
||||
with get_db_context(db) as db:
|
||||
user = db.query(User).filter_by(id=id).first()
|
||||
if not user:
|
||||
return None
|
||||
|
||||
scim = user.scim or {}
|
||||
scim[provider] = {"external_id": external_id}
|
||||
|
||||
db.query(User).filter_by(id=id).update({"scim": scim})
|
||||
db.commit()
|
||||
|
||||
return UserModel.model_validate(user)
|
||||
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def update_user_by_id(
|
||||
self, id: str, updated: dict, db: Optional[Session] = None
|
||||
) -> Optional[UserModel]:
|
||||
@@ -762,6 +835,14 @@ class UsersTable:
|
||||
)
|
||||
return count
|
||||
|
||||
@staticmethod
|
||||
def is_active(user: UserModel) -> bool:
|
||||
"""Compute active status from an already-loaded UserModel (no DB hit)."""
|
||||
if user.last_active_at:
|
||||
three_minutes_ago = int(time.time()) - 180
|
||||
return user.last_active_at >= three_minutes_ago
|
||||
return False
|
||||
|
||||
def is_user_active(self, user_id: str, db: Optional[Session] = None) -> bool:
|
||||
with get_db_context(db) as db:
|
||||
user = db.query(User).filter_by(id=user_id).first()
|
||||
|
||||
@@ -4,6 +4,7 @@ KEYS_TO_EXCLUDE = ["content", "pages", "tables", "paragraphs", "sections", "figu
|
||||
|
||||
|
||||
def filter_metadata(metadata: dict[str, any]) -> dict[str, any]:
|
||||
# Removes large/redundant fields from metadata dict.
|
||||
metadata = {
|
||||
key: value for key, value in metadata.items() if key not in KEYS_TO_EXCLUDE
|
||||
}
|
||||
@@ -13,16 +14,15 @@ def filter_metadata(metadata: dict[str, any]) -> dict[str, any]:
|
||||
def process_metadata(
|
||||
metadata: dict[str, any],
|
||||
) -> dict[str, any]:
|
||||
# Removes large fields and converts non-serializable types (datetime, list, dict) to strings.
|
||||
result = {}
|
||||
for key, value in metadata.items():
|
||||
# Remove large fields
|
||||
# Skip large fields
|
||||
if key in KEYS_TO_EXCLUDE:
|
||||
del metadata[key]
|
||||
|
||||
continue
|
||||
# Convert non-serializable fields to strings
|
||||
if (
|
||||
isinstance(value, datetime)
|
||||
or isinstance(value, list)
|
||||
or isinstance(value, dict)
|
||||
):
|
||||
metadata[key] = str(value)
|
||||
return metadata
|
||||
if isinstance(value, (datetime, list, dict)):
|
||||
result[key] = str(value)
|
||||
else:
|
||||
result[key] = value
|
||||
return result
|
||||
|
||||
@@ -639,13 +639,14 @@ def transcription_handler(request, file_path, metadata, user=None):
|
||||
if user and ENABLE_FORWARD_USER_INFO_HEADERS:
|
||||
headers = include_user_info_headers(headers, user)
|
||||
|
||||
r = requests.post(
|
||||
url=f"{request.app.state.config.STT_OPENAI_API_BASE_URL}/audio/transcriptions",
|
||||
headers=headers,
|
||||
files={"file": (filename, open(file_path, "rb"))},
|
||||
data=payload,
|
||||
timeout=AIOHTTP_CLIENT_TIMEOUT,
|
||||
)
|
||||
with open(file_path, "rb") as audio_file:
|
||||
r = requests.post(
|
||||
url=f"{request.app.state.config.STT_OPENAI_API_BASE_URL}/audio/transcriptions",
|
||||
headers=headers,
|
||||
files={"file": (filename, audio_file)},
|
||||
data=payload,
|
||||
timeout=AIOHTTP_CLIENT_TIMEOUT,
|
||||
)
|
||||
|
||||
if r.status_code == 200:
|
||||
# Successful transcription
|
||||
|
||||
@@ -145,7 +145,7 @@ def create_session_response(
|
||||
"email": user.email,
|
||||
"name": user.name,
|
||||
"role": user.role,
|
||||
"profile_image_url": user.profile_image_url,
|
||||
"profile_image_url": f"/api/v1/users/{user.id}/profile/image",
|
||||
"permissions": user_permissions,
|
||||
}
|
||||
|
||||
@@ -926,7 +926,7 @@ async def add_user(
|
||||
"email": user.email,
|
||||
"name": user.name,
|
||||
"role": user.role,
|
||||
"profile_image_url": user.profile_image_url,
|
||||
"profile_image_url": f"/api/v1/users/{user.id}/profile/image",
|
||||
}
|
||||
else:
|
||||
raise HTTPException(500, detail=ERROR_MESSAGES.CREATE_USER_ERROR)
|
||||
|
||||
@@ -204,7 +204,7 @@ async def get_channels(
|
||||
UserIdNameStatusResponse(
|
||||
**{
|
||||
**user.model_dump(),
|
||||
"is_active": Users.is_user_active(user.id, db=db),
|
||||
"is_active": Users.is_active(user),
|
||||
}
|
||||
)
|
||||
for user in Users.get_users_by_user_ids(user_ids, db=db)
|
||||
@@ -424,7 +424,7 @@ async def get_channel_by_id(
|
||||
UserIdNameStatusResponse(
|
||||
**{
|
||||
**user.model_dump(),
|
||||
"is_active": Users.is_user_active(user.id, db=db),
|
||||
"is_active": Users.is_active(user),
|
||||
}
|
||||
)
|
||||
for user in Users.get_users_by_user_ids(user_ids, db=db)
|
||||
@@ -540,9 +540,7 @@ async def get_channel_members_by_id(
|
||||
|
||||
return {
|
||||
"users": [
|
||||
UserModelResponse(
|
||||
**user.model_dump(), is_active=Users.is_user_active(user.id, db=db)
|
||||
)
|
||||
UserModelResponse(**user.model_dump(), is_active=Users.is_active(user))
|
||||
for user in users
|
||||
],
|
||||
"total": total,
|
||||
@@ -575,9 +573,7 @@ async def get_channel_members_by_id(
|
||||
|
||||
return {
|
||||
"users": [
|
||||
UserModelResponse(
|
||||
**user.model_dump(), is_active=Users.is_user_active(user.id, db=db)
|
||||
)
|
||||
UserModelResponse(**user.model_dump(), is_active=Users.is_active(user))
|
||||
for user in users
|
||||
],
|
||||
"total": total,
|
||||
|
||||
@@ -1131,9 +1131,9 @@ async def delete_chat_by_id(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
for tag in chat.meta.get("tags", []):
|
||||
if Chats.count_chats_by_tag_name_and_user_id(tag, user.id, db=db) == 1:
|
||||
Tags.delete_tag_by_name_and_user_id(tag, user.id, db=db)
|
||||
Chats.delete_orphan_tags_for_user(
|
||||
chat.meta.get("tags", []), user.id, threshold=1, db=db
|
||||
)
|
||||
|
||||
result = Chats.delete_chat_by_id(id, db=db)
|
||||
|
||||
@@ -1153,9 +1153,9 @@ async def delete_chat_by_id(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
for tag in chat.meta.get("tags", []):
|
||||
if Chats.count_chats_by_tag_name_and_user_id(tag, user.id, db=db) == 1:
|
||||
Tags.delete_tag_by_name_and_user_id(tag, user.id, db=db)
|
||||
Chats.delete_orphan_tags_for_user(
|
||||
chat.meta.get("tags", []), user.id, threshold=1, db=db
|
||||
)
|
||||
|
||||
result = Chats.delete_chat_by_id_and_user_id(id, user.id, db=db)
|
||||
return result
|
||||
@@ -1317,21 +1317,13 @@ async def archive_chat_by_id(
|
||||
if chat:
|
||||
chat = Chats.toggle_chat_archive_by_id(id, db=db)
|
||||
|
||||
# Delete tags if chat is archived
|
||||
tag_ids = chat.meta.get("tags", [])
|
||||
if chat.archived:
|
||||
for tag_id in chat.meta.get("tags", []):
|
||||
if (
|
||||
Chats.count_chats_by_tag_name_and_user_id(tag_id, user.id, db=db)
|
||||
== 0
|
||||
):
|
||||
log.debug(f"deleting tag: {tag_id}")
|
||||
Tags.delete_tag_by_name_and_user_id(tag_id, user.id, db=db)
|
||||
# Archived chats are excluded from count — clean up orphans
|
||||
Chats.delete_orphan_tags_for_user(tag_ids, user.id, db=db)
|
||||
else:
|
||||
for tag_id in chat.meta.get("tags", []):
|
||||
tag = Tags.get_tag_by_name_and_user_id(tag_id, user.id, db=db)
|
||||
if tag is None:
|
||||
log.debug(f"inserting tag: {tag_id}")
|
||||
tag = Tags.insert_new_tag(tag_id, user.id, db=db)
|
||||
# Unarchived — ensure tag rows exist
|
||||
Tags.ensure_tags_exist(tag_ids, user.id, db=db)
|
||||
|
||||
return ChatResponse(**chat.model_dump())
|
||||
else:
|
||||
@@ -1537,11 +1529,9 @@ async def delete_all_tags_by_id(
|
||||
):
|
||||
chat = Chats.get_chat_by_id_and_user_id(id, user.id, db=db)
|
||||
if chat:
|
||||
old_tags = chat.meta.get("tags", [])
|
||||
Chats.delete_all_tags_by_id_and_user_id(id, user.id, db=db)
|
||||
|
||||
for tag in chat.meta.get("tags", []):
|
||||
if Chats.count_chats_by_tag_name_and_user_id(tag, user.id, db=db) == 0:
|
||||
Tags.delete_tag_by_name_and_user_id(tag, user.id, db=db)
|
||||
Chats.delete_orphan_tags_for_user(old_tags, user.id, db=db)
|
||||
|
||||
return True
|
||||
else:
|
||||
|
||||
@@ -849,6 +849,7 @@ class EditImageForm(BaseModel):
|
||||
size: Optional[str] = None
|
||||
n: Optional[int] = None
|
||||
negative_prompt: Optional[str] = None
|
||||
background: Optional[str] = None
|
||||
|
||||
|
||||
@router.post("/edit")
|
||||
@@ -953,6 +954,9 @@ async def image_edits(
|
||||
"prompt": form_data.prompt,
|
||||
**({"n": form_data.n} if form_data.n else {}),
|
||||
**({"size": size} if size else {}),
|
||||
**(
|
||||
{"background": form_data.background} if form_data.background else {}
|
||||
),
|
||||
**(
|
||||
{}
|
||||
if re.match(
|
||||
|
||||
@@ -115,8 +115,10 @@ async def get_knowledge_bases(
|
||||
skip = (page - 1) * limit
|
||||
|
||||
filter = {}
|
||||
groups = Groups.get_groups_by_member_id(user.id, db=db)
|
||||
user_group_ids = {group.id for group in groups}
|
||||
|
||||
if not user.role == "admin" or not BYPASS_ADMIN_ACCESS_CONTROL:
|
||||
groups = Groups.get_groups_by_member_id(user.id, db=db)
|
||||
if groups:
|
||||
filter["group_ids"] = [group.id for group in groups]
|
||||
|
||||
@@ -126,6 +128,17 @@ async def get_knowledge_bases(
|
||||
user.id, filter=filter, skip=skip, limit=limit, db=db
|
||||
)
|
||||
|
||||
# Batch-fetch writable knowledge IDs in a single query instead of N has_access calls
|
||||
knowledge_base_ids = [knowledge_base.id for knowledge_base in result.items]
|
||||
writable_knowledge_base_ids = AccessGrants.get_accessible_resource_ids(
|
||||
user_id=user.id,
|
||||
resource_type="knowledge",
|
||||
resource_ids=knowledge_base_ids,
|
||||
permission="write",
|
||||
user_group_ids=user_group_ids,
|
||||
db=db,
|
||||
)
|
||||
|
||||
return KnowledgeAccessListResponse(
|
||||
items=[
|
||||
KnowledgeAccessResponse(
|
||||
@@ -133,13 +146,7 @@ async def get_knowledge_bases(
|
||||
write_access=(
|
||||
user.id == knowledge_base.user_id
|
||||
or (user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL)
|
||||
or AccessGrants.has_access(
|
||||
user_id=user.id,
|
||||
resource_type="knowledge",
|
||||
resource_id=knowledge_base.id,
|
||||
permission="write",
|
||||
db=db,
|
||||
)
|
||||
or knowledge_base.id in writable_knowledge_base_ids
|
||||
),
|
||||
)
|
||||
for knowledge_base in result.items
|
||||
@@ -166,8 +173,10 @@ async def search_knowledge_bases(
|
||||
if view_option:
|
||||
filter["view_option"] = view_option
|
||||
|
||||
groups = Groups.get_groups_by_member_id(user.id, db=db)
|
||||
user_group_ids = {group.id for group in groups}
|
||||
|
||||
if not user.role == "admin" or not BYPASS_ADMIN_ACCESS_CONTROL:
|
||||
groups = Groups.get_groups_by_member_id(user.id, db=db)
|
||||
if groups:
|
||||
filter["group_ids"] = [group.id for group in groups]
|
||||
|
||||
@@ -177,6 +186,17 @@ async def search_knowledge_bases(
|
||||
user.id, filter=filter, skip=skip, limit=limit, db=db
|
||||
)
|
||||
|
||||
# Batch-fetch writable knowledge IDs in a single query instead of N has_access calls
|
||||
knowledge_base_ids = [knowledge_base.id for knowledge_base in result.items]
|
||||
writable_knowledge_base_ids = AccessGrants.get_accessible_resource_ids(
|
||||
user_id=user.id,
|
||||
resource_type="knowledge",
|
||||
resource_ids=knowledge_base_ids,
|
||||
permission="write",
|
||||
user_group_ids=user_group_ids,
|
||||
db=db,
|
||||
)
|
||||
|
||||
return KnowledgeAccessListResponse(
|
||||
items=[
|
||||
KnowledgeAccessResponse(
|
||||
@@ -184,13 +204,7 @@ async def search_knowledge_bases(
|
||||
write_access=(
|
||||
user.id == knowledge_base.user_id
|
||||
or (user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL)
|
||||
or AccessGrants.has_access(
|
||||
user_id=user.id,
|
||||
resource_type="knowledge",
|
||||
resource_id=knowledge_base.id,
|
||||
permission="write",
|
||||
db=db,
|
||||
)
|
||||
or knowledge_base.id in writable_knowledge_base_ids
|
||||
),
|
||||
)
|
||||
for knowledge_base in result.items
|
||||
@@ -511,6 +525,7 @@ class KnowledgeAccessGrantsForm(BaseModel):
|
||||
|
||||
@router.post("/{id}/access/update", response_model=Optional[KnowledgeFilesResponse])
|
||||
async def update_knowledge_access_by_id(
|
||||
request: Request,
|
||||
id: str,
|
||||
form_data: KnowledgeAccessGrantsForm,
|
||||
user=Depends(get_verified_user),
|
||||
@@ -539,6 +554,25 @@ async def update_knowledge_access_by_id(
|
||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
)
|
||||
|
||||
# Strip public sharing if user lacks permission
|
||||
if (
|
||||
user.role != "admin"
|
||||
and has_public_read_access_grant(form_data.access_grants)
|
||||
and not has_permission(
|
||||
user.id,
|
||||
"sharing.public_knowledge",
|
||||
request.app.state.config.USER_PERMISSIONS,
|
||||
)
|
||||
):
|
||||
form_data.access_grants = [
|
||||
grant
|
||||
for grant in form_data.access_grants
|
||||
if not (
|
||||
grant.get("principal_type") == "user"
|
||||
and grant.get("principal_id") == "*"
|
||||
)
|
||||
]
|
||||
|
||||
AccessGrants.set_access_grants("knowledge", id, form_data.access_grants, db=db)
|
||||
|
||||
return KnowledgeFilesResponse(
|
||||
|
||||
@@ -8,14 +8,16 @@ import logging
|
||||
from open_webui.models.groups import Groups
|
||||
from open_webui.models.models import (
|
||||
ModelForm,
|
||||
ModelMeta,
|
||||
ModelModel,
|
||||
ModelParams,
|
||||
ModelResponse,
|
||||
ModelListResponse,
|
||||
ModelAccessListResponse,
|
||||
ModelAccessResponse,
|
||||
Models,
|
||||
)
|
||||
from open_webui.models.access_grants import AccessGrants
|
||||
from open_webui.models.access_grants import AccessGrants, has_public_read_access_grant
|
||||
|
||||
from pydantic import BaseModel
|
||||
from open_webui.constants import ERROR_MESSAGES
|
||||
@@ -84,14 +86,29 @@ async def get_models(
|
||||
if direction:
|
||||
filter["direction"] = direction
|
||||
|
||||
# Pre-fetch user group IDs once - used for both filter and write_access check
|
||||
groups = Groups.get_groups_by_member_id(user.id, db=db)
|
||||
user_group_ids = {group.id for group in groups}
|
||||
|
||||
if not user.role == "admin" or not BYPASS_ADMIN_ACCESS_CONTROL:
|
||||
groups = Groups.get_groups_by_member_id(user.id, db=db)
|
||||
if groups:
|
||||
filter["group_ids"] = [group.id for group in groups]
|
||||
|
||||
filter["user_id"] = user.id
|
||||
|
||||
result = Models.search_models(user.id, filter=filter, skip=skip, limit=limit, db=db)
|
||||
|
||||
# Batch-fetch writable model IDs in a single query instead of N has_access calls
|
||||
model_ids = [model.id for model in result.items]
|
||||
writable_model_ids = AccessGrants.get_accessible_resource_ids(
|
||||
user_id=user.id,
|
||||
resource_type="model",
|
||||
resource_ids=model_ids,
|
||||
permission="write",
|
||||
user_group_ids=user_group_ids,
|
||||
db=db,
|
||||
)
|
||||
|
||||
return ModelAccessListResponse(
|
||||
items=[
|
||||
ModelAccessResponse(
|
||||
@@ -99,13 +116,7 @@ async def get_models(
|
||||
write_access=(
|
||||
(user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL)
|
||||
or user.id == model.user_id
|
||||
or AccessGrants.has_access(
|
||||
user_id=user.id,
|
||||
resource_type="model",
|
||||
resource_id=model.id,
|
||||
permission="write",
|
||||
db=db,
|
||||
)
|
||||
or model.id in writable_model_ids
|
||||
),
|
||||
)
|
||||
for model in result.items
|
||||
@@ -506,16 +517,36 @@ class ModelAccessGrantsForm(BaseModel):
|
||||
|
||||
@router.post("/model/access/update", response_model=Optional[ModelModel])
|
||||
async def update_model_access_by_id(
|
||||
request: Request,
|
||||
form_data: ModelAccessGrantsForm,
|
||||
user=Depends(get_verified_user),
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
model = Models.get_model_by_id(form_data.id, db=db)
|
||||
|
||||
# Non-preset models (e.g. direct Ollama/OpenAI models) may not have a DB
|
||||
# entry yet. Create a minimal one so access grants can be stored.
|
||||
if not model:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
if user.role != "admin":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
)
|
||||
model = Models.insert_new_model(
|
||||
ModelForm(
|
||||
id=form_data.id,
|
||||
name=form_data.id,
|
||||
meta=ModelMeta(),
|
||||
params=ModelParams(),
|
||||
),
|
||||
user.id,
|
||||
db=db,
|
||||
)
|
||||
if not model:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=ERROR_MESSAGES.DEFAULT("Error creating model entry"),
|
||||
)
|
||||
|
||||
if (
|
||||
model.user_id != user.id
|
||||
@@ -533,6 +564,25 @@ async def update_model_access_by_id(
|
||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
)
|
||||
|
||||
# Strip public sharing if user lacks permission
|
||||
if (
|
||||
user.role != "admin"
|
||||
and has_public_read_access_grant(form_data.access_grants)
|
||||
and not has_permission(
|
||||
user.id,
|
||||
"sharing.public_models",
|
||||
request.app.state.config.USER_PERMISSIONS,
|
||||
)
|
||||
):
|
||||
form_data.access_grants = [
|
||||
grant
|
||||
for grant in form_data.access_grants
|
||||
if not (
|
||||
grant.get("principal_type") == "user"
|
||||
and grant.get("principal_id") == "*"
|
||||
)
|
||||
]
|
||||
|
||||
AccessGrants.set_access_grants(
|
||||
"model", form_data.id, form_data.access_grants, db=db
|
||||
)
|
||||
|
||||
@@ -345,6 +345,25 @@ async def update_note_access_by_id(
|
||||
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
|
||||
)
|
||||
|
||||
# Strip public sharing if user lacks permission
|
||||
if (
|
||||
user.role != "admin"
|
||||
and has_public_read_access_grant(form_data.access_grants)
|
||||
and not has_permission(
|
||||
user.id,
|
||||
"sharing.public_notes",
|
||||
request.app.state.config.USER_PERMISSIONS,
|
||||
)
|
||||
):
|
||||
form_data.access_grants = [
|
||||
grant
|
||||
for grant in form_data.access_grants
|
||||
if not (
|
||||
grant.get("principal_type") == "user"
|
||||
and grant.get("principal_id") == "*"
|
||||
)
|
||||
]
|
||||
|
||||
AccessGrants.set_access_grants("note", id, form_data.access_grants, db=db)
|
||||
|
||||
return Notes.get_note_by_id(id, db=db)
|
||||
|
||||
@@ -418,21 +418,29 @@ async def get_all_models(request: Request, user: UserModel = None):
|
||||
async def get_filtered_models(models, user, db=None):
|
||||
# Filter models based on user access control
|
||||
model_ids = [model["model"] for model in models.get("models", [])]
|
||||
model_infos = {m.id: m for m in Models.get_models_by_ids(model_ids, db=db)}
|
||||
user_group_ids = {g.id for g in Groups.get_groups_by_member_id(user.id, db=db)}
|
||||
model_infos = {
|
||||
model_info.id: model_info
|
||||
for model_info in Models.get_models_by_ids(model_ids, db=db)
|
||||
}
|
||||
user_group_ids = {
|
||||
group.id for group in Groups.get_groups_by_member_id(user.id, db=db)
|
||||
}
|
||||
|
||||
# Batch-fetch accessible resource IDs in a single query instead of N has_access calls
|
||||
accessible_model_ids = AccessGrants.get_accessible_resource_ids(
|
||||
user_id=user.id,
|
||||
resource_type="model",
|
||||
resource_ids=list(model_infos.keys()),
|
||||
permission="read",
|
||||
user_group_ids=user_group_ids,
|
||||
db=db,
|
||||
)
|
||||
|
||||
filtered_models = []
|
||||
for model in models.get("models", []):
|
||||
model_info = model_infos.get(model["model"])
|
||||
if model_info:
|
||||
if user.id == model_info.user_id or AccessGrants.has_access(
|
||||
user_id=user.id,
|
||||
resource_type="model",
|
||||
resource_id=model_info.id,
|
||||
permission="read",
|
||||
user_group_ids=user_group_ids,
|
||||
db=db,
|
||||
):
|
||||
if user.id == model_info.user_id or model_info.id in accessible_model_ids:
|
||||
filtered_models.append(model)
|
||||
return filtered_models
|
||||
|
||||
@@ -653,10 +661,6 @@ async def unload_model(
|
||||
await get_all_models(request, user=user)
|
||||
models = request.app.state.OLLAMA_MODELS
|
||||
|
||||
# Canonicalize model name (if not supplied with version)
|
||||
if ":" not in model_name:
|
||||
model_name = f"{model_name}:latest"
|
||||
|
||||
if model_name not in models:
|
||||
raise HTTPException(
|
||||
status_code=400, detail=ERROR_MESSAGES.MODEL_NOT_FOUND(model_name)
|
||||
@@ -1333,6 +1337,9 @@ async def generate_chat_completion(
|
||||
|
||||
# Check if user has access to the model
|
||||
if not bypass_filter and user.role == "user":
|
||||
user_group_ids = {
|
||||
group.id for group in Groups.get_groups_by_member_id(user.id)
|
||||
}
|
||||
if not (
|
||||
user.id == model_info.user_id
|
||||
or AccessGrants.has_access(
|
||||
@@ -1340,6 +1347,7 @@ async def generate_chat_completion(
|
||||
resource_type="model",
|
||||
resource_id=model_info.id,
|
||||
permission="read",
|
||||
user_group_ids=user_group_ids,
|
||||
)
|
||||
):
|
||||
raise HTTPException(
|
||||
@@ -1353,9 +1361,6 @@ async def generate_chat_completion(
|
||||
detail="Model not found",
|
||||
)
|
||||
|
||||
if ":" not in payload["model"]:
|
||||
payload["model"] = f"{payload['model']}:latest"
|
||||
|
||||
url, url_idx = await get_ollama_url(request, payload["model"], url_idx)
|
||||
api_config = request.app.state.config.OLLAMA_API_CONFIGS.get(
|
||||
str(url_idx),
|
||||
@@ -1432,9 +1437,6 @@ async def generate_openai_completion(
|
||||
del payload["metadata"]
|
||||
|
||||
model_id = form_data.model
|
||||
if ":" not in model_id:
|
||||
model_id = f"{model_id}:latest"
|
||||
|
||||
model_info = Models.get_model_by_id(model_id)
|
||||
if model_info:
|
||||
if model_info.base_model_id:
|
||||
@@ -1446,6 +1448,9 @@ async def generate_openai_completion(
|
||||
|
||||
# Check if user has access to the model
|
||||
if user.role == "user":
|
||||
user_group_ids = {
|
||||
group.id for group in Groups.get_groups_by_member_id(user.id)
|
||||
}
|
||||
if not (
|
||||
user.id == model_info.user_id
|
||||
or AccessGrants.has_access(
|
||||
@@ -1453,6 +1458,7 @@ async def generate_openai_completion(
|
||||
resource_type="model",
|
||||
resource_id=model_info.id,
|
||||
permission="read",
|
||||
user_group_ids=user_group_ids,
|
||||
)
|
||||
):
|
||||
raise HTTPException(
|
||||
@@ -1466,9 +1472,6 @@ async def generate_openai_completion(
|
||||
detail="Model not found",
|
||||
)
|
||||
|
||||
if ":" not in payload["model"]:
|
||||
payload["model"] = f"{payload['model']}:latest"
|
||||
|
||||
url, url_idx = await get_ollama_url(request, payload["model"], url_idx)
|
||||
api_config = request.app.state.config.OLLAMA_API_CONFIGS.get(
|
||||
str(url_idx),
|
||||
@@ -1518,9 +1521,6 @@ async def generate_openai_chat_completion(
|
||||
del payload["metadata"]
|
||||
|
||||
model_id = completion_form.model
|
||||
if ":" not in model_id:
|
||||
model_id = f"{model_id}:latest"
|
||||
|
||||
model_info = Models.get_model_by_id(model_id)
|
||||
if model_info:
|
||||
if model_info.base_model_id:
|
||||
@@ -1536,6 +1536,9 @@ async def generate_openai_chat_completion(
|
||||
|
||||
# Check if user has access to the model
|
||||
if user.role == "user":
|
||||
user_group_ids = {
|
||||
group.id for group in Groups.get_groups_by_member_id(user.id)
|
||||
}
|
||||
if not (
|
||||
user.id == model_info.user_id
|
||||
or AccessGrants.has_access(
|
||||
@@ -1543,6 +1546,7 @@ async def generate_openai_chat_completion(
|
||||
resource_type="model",
|
||||
resource_id=model_info.id,
|
||||
permission="read",
|
||||
user_group_ids=user_group_ids,
|
||||
)
|
||||
):
|
||||
raise HTTPException(
|
||||
@@ -1556,9 +1560,6 @@ async def generate_openai_chat_completion(
|
||||
detail="Model not found",
|
||||
)
|
||||
|
||||
if ":" not in payload["model"]:
|
||||
payload["model"] = f"{payload['model']}:latest"
|
||||
|
||||
url, url_idx = await get_ollama_url(request, payload["model"], url_idx)
|
||||
api_config = request.app.state.config.OLLAMA_API_CONFIGS.get(
|
||||
str(url_idx),
|
||||
@@ -1637,20 +1638,31 @@ async def get_openai_models(
|
||||
if user.role == "user" and not BYPASS_MODEL_ACCESS_CONTROL:
|
||||
# Filter models based on user access control
|
||||
model_ids = [model["id"] for model in models]
|
||||
model_infos = {m.id: m for m in Models.get_models_by_ids(model_ids, db=db)}
|
||||
user_group_ids = {g.id for g in Groups.get_groups_by_member_id(user.id, db=db)}
|
||||
model_infos = {
|
||||
model_info.id: model_info
|
||||
for model_info in Models.get_models_by_ids(model_ids, db=db)
|
||||
}
|
||||
user_group_ids = {
|
||||
group.id for group in Groups.get_groups_by_member_id(user.id, db=db)
|
||||
}
|
||||
|
||||
# Batch-fetch accessible resource IDs in a single query instead of N has_access calls
|
||||
accessible_model_ids = AccessGrants.get_accessible_resource_ids(
|
||||
user_id=user.id,
|
||||
resource_type="model",
|
||||
resource_ids=list(model_infos.keys()),
|
||||
permission="read",
|
||||
user_group_ids=user_group_ids,
|
||||
db=db,
|
||||
)
|
||||
|
||||
filtered_models = []
|
||||
for model in models:
|
||||
model_info = model_infos.get(model["id"])
|
||||
if model_info:
|
||||
if user.id == model_info.user_id or AccessGrants.has_access(
|
||||
user_id=user.id,
|
||||
resource_type="model",
|
||||
resource_id=model_info.id,
|
||||
permission="read",
|
||||
user_group_ids=user_group_ids,
|
||||
db=db,
|
||||
if (
|
||||
user.id == model_info.user_id
|
||||
or model_info.id in accessible_model_ids
|
||||
):
|
||||
filtered_models.append(model)
|
||||
models = filtered_models
|
||||
|
||||
@@ -18,7 +18,7 @@ from fastapi.responses import (
|
||||
JSONResponse,
|
||||
PlainTextResponse,
|
||||
)
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -455,21 +455,29 @@ async def get_all_models_responses(request: Request, user: UserModel) -> list:
|
||||
async def get_filtered_models(models, user, db=None):
|
||||
# Filter models based on user access control
|
||||
model_ids = [model["id"] for model in models.get("data", [])]
|
||||
model_infos = {m.id: m for m in Models.get_models_by_ids(model_ids, db=db)}
|
||||
user_group_ids = {g.id for g in Groups.get_groups_by_member_id(user.id, db=db)}
|
||||
model_infos = {
|
||||
model_info.id: model_info
|
||||
for model_info in Models.get_models_by_ids(model_ids, db=db)
|
||||
}
|
||||
user_group_ids = {
|
||||
group.id for group in Groups.get_groups_by_member_id(user.id, db=db)
|
||||
}
|
||||
|
||||
# Batch-fetch accessible resource IDs in a single query instead of N has_access calls
|
||||
accessible_model_ids = AccessGrants.get_accessible_resource_ids(
|
||||
user_id=user.id,
|
||||
resource_type="model",
|
||||
resource_ids=list(model_infos.keys()),
|
||||
permission="read",
|
||||
user_group_ids=user_group_ids,
|
||||
db=db,
|
||||
)
|
||||
|
||||
filtered_models = []
|
||||
for model in models.get("data", []):
|
||||
model_info = model_infos.get(model["id"])
|
||||
if model_info:
|
||||
if user.id == model_info.user_id or AccessGrants.has_access(
|
||||
user_id=user.id,
|
||||
resource_type="model",
|
||||
resource_id=model_info.id,
|
||||
permission="read",
|
||||
user_group_ids=user_group_ids,
|
||||
db=db,
|
||||
):
|
||||
if user.id == model_info.user_id or model_info.id in accessible_model_ids:
|
||||
filtered_models.append(model)
|
||||
return filtered_models
|
||||
|
||||
@@ -596,9 +604,12 @@ async def get_models(
|
||||
if r.status != 200:
|
||||
# Extract response error details if available
|
||||
error_detail = f"HTTP Error: {r.status}"
|
||||
res = await r.json()
|
||||
if "error" in res:
|
||||
error_detail = f"External Error: {res['error']}"
|
||||
try:
|
||||
res = await r.json()
|
||||
if "error" in res:
|
||||
error_detail = f"External Error: {res['error']}"
|
||||
except Exception:
|
||||
pass
|
||||
raise Exception(error_detail)
|
||||
|
||||
response_data = await r.json()
|
||||
@@ -960,6 +971,9 @@ async def generate_chat_completion(
|
||||
|
||||
# Check if user has access to the model
|
||||
if not bypass_filter and user.role == "user":
|
||||
user_group_ids = {
|
||||
group.id for group in Groups.get_groups_by_member_id(user.id)
|
||||
}
|
||||
if not (
|
||||
user.id == model_info.user_id
|
||||
or AccessGrants.has_access(
|
||||
@@ -967,6 +981,7 @@ async def generate_chat_completion(
|
||||
resource_type="model",
|
||||
resource_id=model_info.id,
|
||||
permission="read",
|
||||
user_group_ids=user_group_ids,
|
||||
)
|
||||
):
|
||||
raise HTTPException(
|
||||
@@ -1208,6 +1223,129 @@ async def embeddings(request: Request, form_data: dict, user):
|
||||
await cleanup_response(r, session)
|
||||
|
||||
|
||||
class ResponsesForm(BaseModel):
|
||||
model_config = ConfigDict(extra="allow")
|
||||
|
||||
model: str
|
||||
input: Optional[list | str] = None
|
||||
instructions: Optional[str] = None
|
||||
stream: Optional[bool] = None
|
||||
temperature: Optional[float] = None
|
||||
max_output_tokens: Optional[int] = None
|
||||
top_p: Optional[float] = None
|
||||
tools: Optional[list] = None
|
||||
tool_choice: Optional[str | dict] = None
|
||||
text: Optional[dict] = None
|
||||
truncation: Optional[str] = None
|
||||
metadata: Optional[dict] = None
|
||||
store: Optional[bool] = None
|
||||
reasoning: Optional[dict] = None
|
||||
previous_response_id: Optional[str] = None
|
||||
|
||||
|
||||
@router.post("/responses")
|
||||
async def responses(
|
||||
request: Request,
|
||||
form_data: ResponsesForm,
|
||||
user=Depends(get_verified_user),
|
||||
):
|
||||
"""
|
||||
Forward requests to the OpenAI Responses API endpoint.
|
||||
Routes to the correct upstream backend based on the model field.
|
||||
"""
|
||||
payload = form_data.model_dump(exclude_none=True)
|
||||
body = json.dumps(payload)
|
||||
|
||||
idx = 0
|
||||
model_id = form_data.model
|
||||
if model_id:
|
||||
models = request.app.state.OPENAI_MODELS
|
||||
if not models or model_id not in models:
|
||||
await get_all_models(request, user=user)
|
||||
models = request.app.state.OPENAI_MODELS
|
||||
if model_id in models:
|
||||
idx = models[model_id]["urlIdx"]
|
||||
|
||||
url = request.app.state.config.OPENAI_API_BASE_URLS[idx]
|
||||
key = request.app.state.config.OPENAI_API_KEYS[idx]
|
||||
api_config = request.app.state.config.OPENAI_API_CONFIGS.get(
|
||||
str(idx),
|
||||
request.app.state.config.OPENAI_API_CONFIGS.get(url, {}), # Legacy support
|
||||
)
|
||||
|
||||
r = None
|
||||
session = None
|
||||
streaming = False
|
||||
|
||||
try:
|
||||
headers, cookies = await get_headers_and_cookies(
|
||||
request, url, key, api_config, user=user
|
||||
)
|
||||
|
||||
if api_config.get("azure", False):
|
||||
api_version = api_config.get("api_version", "2023-03-15-preview")
|
||||
|
||||
auth_type = api_config.get("auth_type", "bearer")
|
||||
if auth_type not in ("azure_ad", "microsoft_entra_id"):
|
||||
headers["api-key"] = key
|
||||
|
||||
headers["api-version"] = api_version
|
||||
|
||||
model = payload.get("model", "")
|
||||
request_url = (
|
||||
f"{url}/openai/deployments/{model}/responses?api-version={api_version}"
|
||||
)
|
||||
else:
|
||||
request_url = f"{url}/responses"
|
||||
|
||||
session = aiohttp.ClientSession(
|
||||
trust_env=True,
|
||||
timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT),
|
||||
)
|
||||
r = await session.request(
|
||||
method="POST",
|
||||
url=request_url,
|
||||
data=body,
|
||||
headers=headers,
|
||||
cookies=cookies,
|
||||
ssl=AIOHTTP_CLIENT_SESSION_SSL,
|
||||
)
|
||||
|
||||
# Check if response is SSE
|
||||
if "text/event-stream" in r.headers.get("Content-Type", ""):
|
||||
streaming = True
|
||||
return StreamingResponse(
|
||||
stream_wrapper(r, session),
|
||||
status_code=r.status,
|
||||
headers=dict(r.headers),
|
||||
)
|
||||
else:
|
||||
try:
|
||||
response_data = await r.json()
|
||||
except Exception:
|
||||
response_data = await r.text()
|
||||
|
||||
if r.status >= 400:
|
||||
if isinstance(response_data, (dict, list)):
|
||||
return JSONResponse(status_code=r.status, content=response_data)
|
||||
else:
|
||||
return PlainTextResponse(
|
||||
status_code=r.status, content=response_data
|
||||
)
|
||||
|
||||
return response_data
|
||||
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
raise HTTPException(
|
||||
status_code=r.status if r else 500,
|
||||
detail="Open WebUI: Server Connection Error",
|
||||
)
|
||||
finally:
|
||||
if not streaming:
|
||||
await cleanup_response(r, session)
|
||||
|
||||
|
||||
@router.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
|
||||
async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
|
||||
"""
|
||||
@@ -1216,7 +1354,24 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
|
||||
|
||||
body = await request.body()
|
||||
|
||||
# Parse JSON body to resolve model-based routing
|
||||
payload = None
|
||||
if body:
|
||||
try:
|
||||
payload = json.loads(body)
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
payload = None
|
||||
|
||||
idx = 0
|
||||
model_id = payload.get("model") if isinstance(payload, dict) else None
|
||||
if model_id:
|
||||
models = request.app.state.OPENAI_MODELS
|
||||
if not models or model_id not in models:
|
||||
await get_all_models(request, user=user)
|
||||
models = request.app.state.OPENAI_MODELS
|
||||
if model_id in models:
|
||||
idx = models[model_id]["urlIdx"]
|
||||
|
||||
url = request.app.state.config.OPENAI_API_BASE_URLS[idx]
|
||||
key = request.app.state.config.OPENAI_API_KEYS[idx]
|
||||
api_config = request.app.state.config.OPENAI_API_CONFIGS.get(
|
||||
|
||||
@@ -228,22 +228,23 @@ async def upload_pipeline(
|
||||
headers = {"Authorization": f"Bearer {key}"}
|
||||
|
||||
async with aiohttp.ClientSession(trust_env=True) as session:
|
||||
form_data = aiohttp.FormData()
|
||||
form_data.add_field(
|
||||
"file",
|
||||
open(file_path, "rb"),
|
||||
filename=filename,
|
||||
content_type="application/octet-stream",
|
||||
)
|
||||
with open(file_path, "rb") as f:
|
||||
form_data = aiohttp.FormData()
|
||||
form_data.add_field(
|
||||
"file",
|
||||
f,
|
||||
filename=filename,
|
||||
content_type="application/octet-stream",
|
||||
)
|
||||
|
||||
async with session.post(
|
||||
f"{url}/pipelines/upload",
|
||||
headers=headers,
|
||||
data=form_data,
|
||||
ssl=AIOHTTP_CLIENT_SESSION_SSL,
|
||||
) as response:
|
||||
response.raise_for_status()
|
||||
data = await response.json()
|
||||
async with session.post(
|
||||
f"{url}/pipelines/upload",
|
||||
headers=headers,
|
||||
data=form_data,
|
||||
ssl=AIOHTTP_CLIENT_SESSION_SSL,
|
||||
) as response:
|
||||
response.raise_for_status()
|
||||
data = await response.json()
|
||||
|
||||
return {**data}
|
||||
except Exception as e:
|
||||
|
||||
@@ -9,7 +9,7 @@ from open_webui.models.prompts import (
|
||||
PromptModel,
|
||||
Prompts,
|
||||
)
|
||||
from open_webui.models.access_grants import AccessGrants
|
||||
from open_webui.models.access_grants import AccessGrants, has_public_read_access_grant
|
||||
from open_webui.models.groups import Groups
|
||||
from open_webui.models.prompt_history import (
|
||||
PromptHistories,
|
||||
@@ -100,8 +100,11 @@ async def get_prompt_list(
|
||||
if direction:
|
||||
filter["direction"] = direction
|
||||
|
||||
# Pre-fetch user group IDs once - used for both filter and write_access check
|
||||
groups = Groups.get_groups_by_member_id(user.id, db=db)
|
||||
user_group_ids = {group.id for group in groups}
|
||||
|
||||
if not (user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL):
|
||||
groups = Groups.get_groups_by_member_id(user.id, db=db)
|
||||
if groups:
|
||||
filter["group_ids"] = [group.id for group in groups]
|
||||
|
||||
@@ -111,6 +114,17 @@ async def get_prompt_list(
|
||||
user.id, filter=filter, skip=skip, limit=limit, db=db
|
||||
)
|
||||
|
||||
# Batch-fetch writable prompt IDs in a single query instead of N has_access calls
|
||||
prompt_ids = [prompt.id for prompt in result.items]
|
||||
writable_prompt_ids = AccessGrants.get_accessible_resource_ids(
|
||||
user_id=user.id,
|
||||
resource_type="prompt",
|
||||
resource_ids=prompt_ids,
|
||||
permission="write",
|
||||
user_group_ids=user_group_ids,
|
||||
db=db,
|
||||
)
|
||||
|
||||
return PromptAccessListResponse(
|
||||
items=[
|
||||
PromptAccessResponse(
|
||||
@@ -118,13 +132,7 @@ async def get_prompt_list(
|
||||
write_access=(
|
||||
(user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL)
|
||||
or user.id == prompt.user_id
|
||||
or AccessGrants.has_access(
|
||||
user_id=user.id,
|
||||
resource_type="prompt",
|
||||
resource_id=prompt.id,
|
||||
permission="write",
|
||||
db=db,
|
||||
)
|
||||
or prompt.id in writable_prompt_ids
|
||||
),
|
||||
)
|
||||
for prompt in result.items
|
||||
@@ -436,6 +444,7 @@ class PromptAccessGrantsForm(BaseModel):
|
||||
|
||||
@router.post("/id/{prompt_id}/access/update", response_model=Optional[PromptModel])
|
||||
async def update_prompt_access_by_id(
|
||||
request: Request,
|
||||
prompt_id: str,
|
||||
form_data: PromptAccessGrantsForm,
|
||||
user=Depends(get_verified_user),
|
||||
@@ -464,6 +473,25 @@ async def update_prompt_access_by_id(
|
||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
)
|
||||
|
||||
# Strip public sharing if user lacks permission
|
||||
if (
|
||||
user.role != "admin"
|
||||
and has_public_read_access_grant(form_data.access_grants)
|
||||
and not has_permission(
|
||||
user.id,
|
||||
"sharing.public_prompts",
|
||||
request.app.state.config.USER_PERMISSIONS,
|
||||
)
|
||||
):
|
||||
form_data.access_grants = [
|
||||
grant
|
||||
for grant in form_data.access_grants
|
||||
if not (
|
||||
grant.get("principal_type") == "user"
|
||||
and grant.get("principal_id") == "*"
|
||||
)
|
||||
]
|
||||
|
||||
AccessGrants.set_access_grants("prompt", prompt_id, form_data.access_grants, db=db)
|
||||
|
||||
return Prompts.get_prompt_by_id(prompt_id, db=db)
|
||||
|
||||
@@ -25,6 +25,9 @@ from open_webui.utils.auth import (
|
||||
)
|
||||
from open_webui.constants import ERROR_MESSAGES
|
||||
|
||||
from open_webui.config import OAUTH_PROVIDERS
|
||||
from open_webui.env import SCIM_AUTH_PROVIDER
|
||||
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
from open_webui.internal.db import get_session
|
||||
@@ -300,6 +303,43 @@ def get_scim_auth(
|
||||
)
|
||||
|
||||
|
||||
def get_external_id(user: UserModel) -> Optional[str]:
|
||||
"""Extract externalId from a user's scim data.
|
||||
|
||||
Checks all stored provider entries and returns the first external_id found.
|
||||
"""
|
||||
if not user.scim:
|
||||
return None
|
||||
for provider_data in user.scim.values():
|
||||
if isinstance(provider_data, dict) and "external_id" in provider_data:
|
||||
return provider_data["external_id"]
|
||||
return None
|
||||
|
||||
|
||||
def get_scim_provider() -> str:
|
||||
"""Return the configured SCIM auth provider.
|
||||
|
||||
Requires SCIM_AUTH_PROVIDER env var to be set (e.g. 'microsoft', 'oidc').
|
||||
"""
|
||||
if not SCIM_AUTH_PROVIDER:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="SCIM_AUTH_PROVIDER environment variable is required when SCIM is enabled",
|
||||
)
|
||||
return SCIM_AUTH_PROVIDER
|
||||
|
||||
|
||||
def find_user_by_external_id(external_id: str, db=None) -> Optional[UserModel]:
|
||||
"""Find a user by SCIM externalId, falling back to OAuth sub match."""
|
||||
provider = get_scim_provider()
|
||||
user = Users.get_user_by_scim_external_id(provider, external_id, db=db)
|
||||
if user:
|
||||
return user
|
||||
|
||||
# Fallback: check if externalId matches an existing OAuth sub (account linking)
|
||||
return Users.get_user_by_oauth_sub(provider, external_id, db=db)
|
||||
|
||||
|
||||
def user_to_scim(user: UserModel, request: Request, db=None) -> SCIMUser:
|
||||
"""Convert internal User model to SCIM User"""
|
||||
# Parse display name into name components
|
||||
@@ -321,6 +361,7 @@ def user_to_scim(user: UserModel, request: Request, db=None) -> SCIMUser:
|
||||
|
||||
return SCIMUser(
|
||||
id=user.id,
|
||||
externalId=get_external_id(user),
|
||||
userName=user.email,
|
||||
name=SCIMName(
|
||||
formatted=user.name,
|
||||
@@ -494,13 +535,17 @@ async def get_users(
|
||||
|
||||
# Get users from database
|
||||
if filter:
|
||||
# Simple filter parsing - supports userName eq "email"
|
||||
# In production, you'd want a more robust filter parser
|
||||
# Simple filter parsing - supports userName eq, externalId eq
|
||||
if "userName eq" in filter:
|
||||
email = filter.split('"')[1]
|
||||
user = Users.get_user_by_email(email, db=db)
|
||||
users_list = [user] if user else []
|
||||
total = 1 if user else 0
|
||||
elif "externalId eq" in filter:
|
||||
external_id = filter.split('"')[1]
|
||||
user = find_user_by_external_id(external_id, db=db)
|
||||
users_list = [user] if user else []
|
||||
total = 1 if user else 0
|
||||
else:
|
||||
response = Users.get_users(skip=skip, limit=limit, db=db)
|
||||
users_list = response["users"]
|
||||
@@ -546,17 +591,33 @@ async def create_user(
|
||||
db: Session = Depends(get_session),
|
||||
):
|
||||
"""Create SCIM User"""
|
||||
# Check if user already exists
|
||||
existing_user = Users.get_user_by_email(user_data.userName, db=db)
|
||||
# Check for duplicate by externalId
|
||||
if user_data.externalId:
|
||||
existing_user = find_user_by_external_id(user_data.externalId, db=db)
|
||||
if existing_user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=f"User with externalId {user_data.externalId} already exists",
|
||||
)
|
||||
|
||||
# Determine primary email (lowercased per RFC 5321)
|
||||
email = user_data.userName
|
||||
for entry in user_data.emails:
|
||||
if entry.primary:
|
||||
email = entry.value
|
||||
break
|
||||
email = email.lower()
|
||||
|
||||
# Check for duplicate by email
|
||||
existing_user = Users.get_user_by_email(email, db=db)
|
||||
if existing_user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=f"User with email {user_data.userName} already exists",
|
||||
detail=f"User with email {email} already exists",
|
||||
)
|
||||
|
||||
# Create user
|
||||
user_id = str(uuid.uuid4())
|
||||
email = user_data.emails[0].value if user_data.emails else user_data.userName
|
||||
|
||||
# Parse name if provided
|
||||
name = user_data.displayName
|
||||
@@ -571,7 +632,6 @@ async def create_user(
|
||||
if user_data.photos and len(user_data.photos) > 0:
|
||||
profile_image = user_data.photos[0].value
|
||||
|
||||
# Create user
|
||||
new_user = Users.insert_new_user(
|
||||
id=user_id,
|
||||
name=name,
|
||||
@@ -587,6 +647,12 @@ async def create_user(
|
||||
detail="Failed to create user",
|
||||
)
|
||||
|
||||
# Store externalId in the scim field
|
||||
if user_data.externalId:
|
||||
provider = get_scim_provider()
|
||||
Users.update_user_scim_by_id(user_id, provider, user_data.externalId, db=db)
|
||||
new_user = Users.get_user_by_id(user_id, db=db)
|
||||
|
||||
return user_to_scim(new_user, request, db=db)
|
||||
|
||||
|
||||
@@ -631,7 +697,6 @@ async def update_user(
|
||||
if user_data.photos and len(user_data.photos) > 0:
|
||||
update_data["profile_image_url"] = user_data.photos[0].value
|
||||
|
||||
# Update user
|
||||
updated_user = Users.update_user_by_id(user_id, update_data, db=db)
|
||||
if not updated_user:
|
||||
raise HTTPException(
|
||||
@@ -639,6 +704,12 @@ async def update_user(
|
||||
detail="Failed to update user",
|
||||
)
|
||||
|
||||
# Update externalId in the scim field
|
||||
if user_data.externalId:
|
||||
provider = get_scim_provider()
|
||||
Users.update_user_scim_by_id(user_id, provider, user_data.externalId, db=db)
|
||||
updated_user = Users.get_user_by_id(user_id, db=db)
|
||||
|
||||
return user_to_scim(updated_user, request, db=db)
|
||||
|
||||
|
||||
@@ -676,6 +747,9 @@ async def patch_user(
|
||||
update_data["email"] = value
|
||||
elif path == "name.formatted":
|
||||
update_data["name"] = value
|
||||
elif path == "externalId":
|
||||
provider = get_scim_provider()
|
||||
Users.update_user_scim_by_id(user_id, provider, value, db=db)
|
||||
|
||||
# Update user
|
||||
if update_data:
|
||||
|
||||
@@ -17,7 +17,7 @@ from open_webui.models.skills import (
|
||||
SkillAccessListResponse,
|
||||
Skills,
|
||||
)
|
||||
from open_webui.models.access_grants import AccessGrants
|
||||
from open_webui.models.access_grants import AccessGrants, has_public_read_access_grant
|
||||
from open_webui.utils.auth import get_admin_user, get_verified_user
|
||||
from open_webui.utils.access_control import has_access, has_permission
|
||||
|
||||
@@ -312,6 +312,7 @@ class SkillAccessGrantsForm(BaseModel):
|
||||
|
||||
@router.post("/id/{id}/access/update", response_model=Optional[SkillModel])
|
||||
async def update_skill_access_by_id(
|
||||
request: Request,
|
||||
id: str,
|
||||
form_data: SkillAccessGrantsForm,
|
||||
user=Depends(get_verified_user),
|
||||
@@ -340,6 +341,25 @@ async def update_skill_access_by_id(
|
||||
detail=ERROR_MESSAGES.UNAUTHORIZED,
|
||||
)
|
||||
|
||||
# Strip public sharing if user lacks permission
|
||||
if (
|
||||
user.role != "admin"
|
||||
and has_public_read_access_grant(form_data.access_grants)
|
||||
and not has_permission(
|
||||
user.id,
|
||||
"sharing.public_skills",
|
||||
request.app.state.config.USER_PERMISSIONS,
|
||||
)
|
||||
):
|
||||
form_data.access_grants = [
|
||||
grant
|
||||
for grant in form_data.access_grants
|
||||
if not (
|
||||
grant.get("principal_type") == "user"
|
||||
and grant.get("principal_id") == "*"
|
||||
)
|
||||
]
|
||||
|
||||
AccessGrants.set_access_grants("skill", id, form_data.access_grants, db=db)
|
||||
|
||||
return Skills.get_skill_by_id(id, db=db)
|
||||
|
||||
@@ -21,7 +21,7 @@ from open_webui.models.tools import (
|
||||
ToolAccessResponse,
|
||||
Tools,
|
||||
)
|
||||
from open_webui.models.access_grants import AccessGrants
|
||||
from open_webui.models.access_grants import AccessGrants, has_public_read_access_grant
|
||||
from open_webui.utils.plugin import (
|
||||
load_tool_module_by_id,
|
||||
replace_imports,
|
||||
@@ -107,7 +107,9 @@ async def get_tools(
|
||||
|
||||
# MCP Tool Servers
|
||||
for server in request.app.state.config.TOOL_SERVER_CONNECTIONS:
|
||||
if server.get("type", "openapi") == "mcp":
|
||||
if server.get("type", "openapi") == "mcp" and server.get("config", {}).get(
|
||||
"enable"
|
||||
):
|
||||
server_id = server.get("info", {}).get("id")
|
||||
auth_type = server.get("auth_type", "none")
|
||||
|
||||
@@ -526,6 +528,7 @@ class ToolAccessGrantsForm(BaseModel):
|
||||
|
||||
@router.post("/id/{id}/access/update", response_model=Optional[ToolModel])
|
||||
async def update_tool_access_by_id(
|
||||
request: Request,
|
||||
id: str,
|
||||
form_data: ToolAccessGrantsForm,
|
||||
user=Depends(get_verified_user),
|
||||
@@ -554,6 +557,25 @@ async def update_tool_access_by_id(
|
||||
detail=ERROR_MESSAGES.UNAUTHORIZED,
|
||||
)
|
||||
|
||||
# Strip public sharing if user lacks permission
|
||||
if (
|
||||
user.role != "admin"
|
||||
and has_public_read_access_grant(form_data.access_grants)
|
||||
and not has_permission(
|
||||
user.id,
|
||||
"sharing.public_tools",
|
||||
request.app.state.config.USER_PERMISSIONS,
|
||||
)
|
||||
):
|
||||
form_data.access_grants = [
|
||||
grant
|
||||
for grant in form_data.access_grants
|
||||
if not (
|
||||
grant.get("principal_type") == "user"
|
||||
and grant.get("principal_id") == "*"
|
||||
)
|
||||
]
|
||||
|
||||
AccessGrants.set_access_grants("tool", id, form_data.access_grants, db=db)
|
||||
|
||||
return Tools.get_tool_by_id(id, db=db)
|
||||
|
||||
@@ -190,6 +190,8 @@ class SharingPermissions(BaseModel):
|
||||
public_prompts: bool = False
|
||||
tools: bool = False
|
||||
public_tools: bool = True
|
||||
skills: bool = False
|
||||
public_skills: bool = False
|
||||
notes: bool = False
|
||||
public_notes: bool = True
|
||||
|
||||
|
||||
@@ -99,6 +99,7 @@ else:
|
||||
|
||||
# Timeout duration in seconds
|
||||
TIMEOUT_DURATION = 3
|
||||
SESSION_POOL_TIMEOUT = 120 # seconds without heartbeat before session is reaped
|
||||
|
||||
# Dictionary to maintain the user pool
|
||||
|
||||
@@ -147,6 +148,17 @@ if WEBSOCKET_MANAGER == "redis":
|
||||
aquire_func = clean_up_lock.aquire_lock
|
||||
renew_func = clean_up_lock.renew_lock
|
||||
release_func = clean_up_lock.release_lock
|
||||
|
||||
session_cleanup_lock = RedisLock(
|
||||
redis_url=WEBSOCKET_REDIS_URL,
|
||||
lock_name=f"{REDIS_KEY_PREFIX}:session_cleanup_lock",
|
||||
timeout_secs=WEBSOCKET_REDIS_LOCK_TIMEOUT,
|
||||
redis_sentinels=redis_sentinels,
|
||||
redis_cluster=WEBSOCKET_REDIS_CLUSTER,
|
||||
)
|
||||
session_aquire_func = session_cleanup_lock.aquire_lock
|
||||
session_renew_func = session_cleanup_lock.renew_lock
|
||||
session_release_func = session_cleanup_lock.release_lock
|
||||
else:
|
||||
MODELS = {}
|
||||
|
||||
@@ -154,6 +166,7 @@ else:
|
||||
USAGE_POOL = {}
|
||||
|
||||
aquire_func = release_func = renew_func = lambda: True
|
||||
session_aquire_func = session_release_func = session_renew_func = lambda: True
|
||||
|
||||
|
||||
YDOC_MANAGER = YdocManager(
|
||||
@@ -162,6 +175,31 @@ YDOC_MANAGER = YdocManager(
|
||||
)
|
||||
|
||||
|
||||
async def periodic_session_pool_cleanup():
|
||||
"""Reap orphaned SESSION_POOL entries that missed heartbeats (e.g. crashed instance)."""
|
||||
if not session_aquire_func():
|
||||
log.debug("Session cleanup lock held by another node. Skipping.")
|
||||
return
|
||||
|
||||
try:
|
||||
while True:
|
||||
if not session_renew_func():
|
||||
log.error("Unable to renew session cleanup lock. Exiting.")
|
||||
return
|
||||
|
||||
now = int(time.time())
|
||||
for sid in list(SESSION_POOL.keys()):
|
||||
entry = SESSION_POOL.get(sid)
|
||||
if entry and now - entry.get("last_seen_at", 0) > SESSION_POOL_TIMEOUT:
|
||||
log.warning(
|
||||
f"Reaping orphaned session {sid} (user {entry.get('id')})"
|
||||
)
|
||||
del SESSION_POOL[sid]
|
||||
await asyncio.sleep(SESSION_POOL_TIMEOUT)
|
||||
finally:
|
||||
session_release_func()
|
||||
|
||||
|
||||
async def periodic_usage_pool_cleanup():
|
||||
max_retries = 2
|
||||
retry_delay = random.uniform(
|
||||
@@ -313,9 +351,18 @@ async def connect(sid, environ, auth):
|
||||
user = Users.get_user_by_id(data["id"])
|
||||
|
||||
if user:
|
||||
SESSION_POOL[sid] = user.model_dump(
|
||||
exclude=["date_of_birth", "bio", "gender"]
|
||||
)
|
||||
SESSION_POOL[sid] = {
|
||||
**user.model_dump(
|
||||
exclude=[
|
||||
"profile_image_url",
|
||||
"profile_banner_image_url",
|
||||
"date_of_birth",
|
||||
"bio",
|
||||
"gender",
|
||||
]
|
||||
),
|
||||
"last_seen_at": int(time.time()),
|
||||
}
|
||||
await sio.enter_room(sid, f"user:{user.id}")
|
||||
|
||||
|
||||
@@ -334,15 +381,18 @@ async def user_join(sid, data):
|
||||
if not user:
|
||||
return
|
||||
|
||||
SESSION_POOL[sid] = user.model_dump(
|
||||
exclude=[
|
||||
"profile_image_url",
|
||||
"profile_banner_image_url",
|
||||
"date_of_birth",
|
||||
"bio",
|
||||
"gender",
|
||||
]
|
||||
)
|
||||
SESSION_POOL[sid] = {
|
||||
**user.model_dump(
|
||||
exclude=[
|
||||
"profile_image_url",
|
||||
"profile_banner_image_url",
|
||||
"date_of_birth",
|
||||
"bio",
|
||||
"gender",
|
||||
]
|
||||
),
|
||||
"last_seen_at": int(time.time()),
|
||||
}
|
||||
|
||||
await sio.enter_room(sid, f"user:{user.id}")
|
||||
|
||||
@@ -360,6 +410,7 @@ async def user_join(sid, data):
|
||||
async def heartbeat(sid, data):
|
||||
user = SESSION_POOL.get(sid)
|
||||
if user:
|
||||
SESSION_POOL[sid] = {**user, "last_seen_at": int(time.time())}
|
||||
Users.update_last_active_by_id(user["id"])
|
||||
|
||||
|
||||
@@ -703,6 +754,17 @@ async def disconnect(sid):
|
||||
if sid in SESSION_POOL:
|
||||
user = SESSION_POOL[sid]
|
||||
del SESSION_POOL[sid]
|
||||
|
||||
# Clean up USAGE_POOL entries for this session
|
||||
for model_id in list(USAGE_POOL.keys()):
|
||||
connections = USAGE_POOL.get(model_id)
|
||||
if connections and sid in connections:
|
||||
del connections[sid]
|
||||
if not connections:
|
||||
del USAGE_POOL[model_id]
|
||||
else:
|
||||
USAGE_POOL[model_id] = connections
|
||||
|
||||
await YDOC_MANAGER.remove_user_from_all_documents(sid)
|
||||
else:
|
||||
pass
|
||||
|
||||
@@ -118,6 +118,8 @@ class RedisDict:
|
||||
|
||||
|
||||
class YdocManager:
|
||||
COMPACTION_THRESHOLD = 500
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
redis=None,
|
||||
@@ -133,10 +135,42 @@ class YdocManager:
|
||||
if self._redis:
|
||||
redis_key = f"{self._redis_key_prefix}:{document_id}:updates"
|
||||
await self._redis.rpush(redis_key, json.dumps(list(update)))
|
||||
list_len = await self._redis.llen(redis_key)
|
||||
if list_len >= self.COMPACTION_THRESHOLD:
|
||||
await self._compact_updates_redis(document_id)
|
||||
else:
|
||||
if document_id not in self._updates:
|
||||
self._updates[document_id] = []
|
||||
self._updates[document_id].append(update)
|
||||
if len(self._updates[document_id]) >= self.COMPACTION_THRESHOLD:
|
||||
self._compact_updates_memory(document_id)
|
||||
|
||||
async def _compact_updates_redis(self, document_id: str):
|
||||
"""Rolling compaction: squash oldest half into one snapshot."""
|
||||
redis_key = f"{self._redis_key_prefix}:{document_id}:updates"
|
||||
all_updates = await self._redis.lrange(redis_key, 0, -1)
|
||||
if len(all_updates) <= 1:
|
||||
return
|
||||
mid = len(all_updates) // 2
|
||||
ydoc = Y.Doc()
|
||||
for raw in all_updates[:mid]:
|
||||
ydoc.apply_update(bytes(json.loads(raw)))
|
||||
snapshot = json.dumps(list(ydoc.get_update()))
|
||||
pipe = self._redis.pipeline()
|
||||
pipe.delete(redis_key)
|
||||
pipe.rpush(redis_key, snapshot, *all_updates[mid:])
|
||||
await pipe.execute()
|
||||
|
||||
def _compact_updates_memory(self, document_id: str):
|
||||
"""Rolling compaction: squash oldest half into one snapshot."""
|
||||
updates = self._updates.get(document_id, [])
|
||||
if len(updates) <= 1:
|
||||
return
|
||||
mid = len(updates) // 2
|
||||
ydoc = Y.Doc()
|
||||
for update in updates[:mid]:
|
||||
ydoc.apply_update(bytes(update))
|
||||
self._updates[document_id] = [ydoc.get_update()] + updates[mid:]
|
||||
|
||||
async def get_updates(self, document_id: str) -> List[bytes]:
|
||||
document_id = document_id.replace(":", "_")
|
||||
|
||||
@@ -12,7 +12,7 @@ def _assert_user(data, id, **kwargs):
|
||||
comparison_data = {
|
||||
"name": f"user {id}",
|
||||
"email": f"user{id}@openwebui.com",
|
||||
"profile_image_url": f"/user{id}.png",
|
||||
"profile_image_url": f"/api/v1/users/{id}/profile/image",
|
||||
"role": "user",
|
||||
**kwargs,
|
||||
}
|
||||
@@ -150,7 +150,7 @@ class TestUsers(AbstractPostgresTest):
|
||||
role="admin",
|
||||
name="user 2 updated",
|
||||
email="user2-updated@openwebui.com",
|
||||
profile_image_url="/user2-updated.png",
|
||||
profile_image_url=f"/api/v1/users/2/profile/image",
|
||||
)
|
||||
|
||||
# Delete user by id
|
||||
|
||||
@@ -167,6 +167,9 @@ async def search_web(
|
||||
engine = __request__.app.state.config.WEB_SEARCH_ENGINE
|
||||
user = UserModel(**__user__) if __user__ else None
|
||||
|
||||
# Use admin-configured result count if configured, falling back to model-provided count of provided, else default to 5
|
||||
count = __request__.app.state.config.WEB_SEARCH_RESULT_COUNT or count
|
||||
|
||||
results = await asyncio.to_thread(_search_web, __request__, engine, query, user)
|
||||
|
||||
# Limit results
|
||||
@@ -1529,6 +1532,69 @@ async def search_knowledge_files(
|
||||
return json.dumps({"error": str(e)})
|
||||
|
||||
|
||||
async def view_file(
|
||||
file_id: str,
|
||||
__request__: Request = None,
|
||||
__user__: dict = None,
|
||||
__model_knowledge__: Optional[list[dict]] = None,
|
||||
) -> str:
|
||||
"""
|
||||
Get the full content of a file by its ID.
|
||||
|
||||
:param file_id: The ID of the file to retrieve
|
||||
:return: JSON with the file's id, filename, and full text content
|
||||
"""
|
||||
if __request__ is None:
|
||||
return json.dumps({"error": "Request context not available"})
|
||||
|
||||
if not __user__:
|
||||
return json.dumps({"error": "User context not available"})
|
||||
|
||||
try:
|
||||
from open_webui.models.files import Files
|
||||
from open_webui.routers.files import has_access_to_file
|
||||
|
||||
user_id = __user__.get("id")
|
||||
user_role = __user__.get("role", "user")
|
||||
|
||||
file = Files.get_file_by_id(file_id)
|
||||
if not file:
|
||||
return json.dumps({"error": "File not found"})
|
||||
|
||||
if (
|
||||
file.user_id != user_id
|
||||
and user_role != "admin"
|
||||
and not any(
|
||||
item.get("type") == "file" and item.get("id") == file_id
|
||||
for item in (__model_knowledge__ or [])
|
||||
)
|
||||
and not has_access_to_file(
|
||||
file_id=file_id,
|
||||
access_type="read",
|
||||
user=UserModel(**__user__),
|
||||
)
|
||||
):
|
||||
return json.dumps({"error": "File not found"})
|
||||
|
||||
content = ""
|
||||
if file.data:
|
||||
content = file.data.get("content", "")
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
"id": file.id,
|
||||
"filename": file.filename,
|
||||
"content": content,
|
||||
"updated_at": file.updated_at,
|
||||
"created_at": file.created_at,
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
log.exception(f"view_file error: {e}")
|
||||
return json.dumps({"error": str(e)})
|
||||
|
||||
|
||||
async def view_knowledge_file(
|
||||
file_id: str,
|
||||
__request__: Request = None,
|
||||
|
||||
@@ -39,11 +39,6 @@ from open_webui.routers.pipelines import (
|
||||
from open_webui.models.functions import Functions
|
||||
from open_webui.models.models import Models
|
||||
|
||||
|
||||
from open_webui.utils.plugin import (
|
||||
load_function_module_by_id,
|
||||
get_function_module_from_cache,
|
||||
)
|
||||
from open_webui.utils.models import get_all_models, check_model_access
|
||||
from open_webui.utils.payload import convert_payload_openai_to_ollama
|
||||
from open_webui.utils.response import (
|
||||
|
||||
@@ -411,6 +411,36 @@ def serialize_output(output: list) -> str:
|
||||
if content and not content.endswith("\n"):
|
||||
content += "\n"
|
||||
|
||||
# Render the code_interpreter item as a <details> block
|
||||
# so the frontend Collapsible renders "Analyzing..."/"Analyzed".
|
||||
code = item.get("code", "").strip()
|
||||
lang = item.get("lang", "python")
|
||||
status = item.get("status", "in_progress")
|
||||
duration = item.get("duration")
|
||||
is_last_item = idx == len(output) - 1
|
||||
|
||||
# Build inner content: code block
|
||||
display = ""
|
||||
if code:
|
||||
display = f"```{lang}\n{code}\n```"
|
||||
|
||||
# Build output attribute as HTML-escaped JSON for CodeBlock.svelte
|
||||
ci_output = item.get("output")
|
||||
output_attr = ""
|
||||
if ci_output:
|
||||
if isinstance(ci_output, dict):
|
||||
output_json = json.dumps(ci_output, ensure_ascii=False)
|
||||
else:
|
||||
output_json = json.dumps(
|
||||
{"result": str(ci_output)}, ensure_ascii=False
|
||||
)
|
||||
output_attr = f' output="{html.escape(output_json)}"'
|
||||
|
||||
if status == "completed" or duration is not None or not is_last_item:
|
||||
content += f'<details type="code_interpreter" done="true" duration="{duration or 0}"{output_attr}>\n<summary>Analyzed</summary>\n{display}\n</details>\n'
|
||||
else:
|
||||
content += f'<details type="code_interpreter" done="false"{output_attr}>\n<summary>Analyzing…</summary>\n{display}\n</details>\n'
|
||||
|
||||
return content.strip()
|
||||
|
||||
|
||||
@@ -1888,6 +1918,25 @@ async def convert_url_images_to_base64(form_data):
|
||||
return form_data
|
||||
|
||||
|
||||
def load_messages_from_db(chat_id: str, message_id: str) -> Optional[list[dict]]:
|
||||
"""
|
||||
Load the message chain from DB up to message_id,
|
||||
keeping only LLM-relevant fields (role, content, output).
|
||||
"""
|
||||
messages_map = Chats.get_messages_map_by_chat_id(chat_id)
|
||||
if not messages_map:
|
||||
return None
|
||||
|
||||
db_messages = get_message_list(messages_map, message_id)
|
||||
if not db_messages:
|
||||
return None
|
||||
|
||||
return [
|
||||
{k: v for k, v in msg.items() if k in ("role", "content", "output", "files")}
|
||||
for msg in db_messages
|
||||
]
|
||||
|
||||
|
||||
def process_messages_with_output(messages: list[dict]) -> list[dict]:
|
||||
"""
|
||||
Process messages with OR-aligned output items for LLM consumption.
|
||||
@@ -1920,6 +1969,44 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
||||
form_data = apply_params_to_form_data(form_data, model)
|
||||
log.debug(f"form_data: {form_data}")
|
||||
|
||||
# Load messages from DB when available — DB preserves structured 'output' items
|
||||
# which the frontend strips, causing tool calls to be merged into content.
|
||||
chat_id = metadata.get("chat_id")
|
||||
parent_message_id = metadata.get("parent_message_id")
|
||||
|
||||
if chat_id and parent_message_id and not chat_id.startswith("local:"):
|
||||
db_messages = load_messages_from_db(chat_id, parent_message_id)
|
||||
if db_messages:
|
||||
system_message = get_system_message(form_data.get("messages", []))
|
||||
form_data["messages"] = (
|
||||
[system_message, *db_messages] if system_message else db_messages
|
||||
)
|
||||
|
||||
# Inject image files into content as image_url parts (mirrors frontend logic)
|
||||
for message in form_data["messages"]:
|
||||
image_files = [
|
||||
f
|
||||
for f in message.get("files", [])
|
||||
if f.get("type") == "image"
|
||||
or (f.get("content_type") or "").startswith("image/")
|
||||
]
|
||||
if message.get("role") == "user" and image_files:
|
||||
text_content = message.get("content", "")
|
||||
if isinstance(text_content, str):
|
||||
message["content"] = [
|
||||
{"type": "text", "text": text_content},
|
||||
*[
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {"url": f["url"]},
|
||||
}
|
||||
for f in image_files
|
||||
if f.get("url")
|
||||
],
|
||||
]
|
||||
# Strip files field — it's been incorporated into content
|
||||
message.pop("files", None)
|
||||
|
||||
# Process messages with OR-aligned output items for clean LLM messages
|
||||
form_data["messages"] = process_messages_with_output(form_data.get("messages", []))
|
||||
|
||||
@@ -2094,23 +2181,27 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
||||
)
|
||||
|
||||
if "code_interpreter" in features and features["code_interpreter"]:
|
||||
form_data["messages"] = add_or_update_user_message(
|
||||
(
|
||||
request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE
|
||||
if request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE != ""
|
||||
else DEFAULT_CODE_INTERPRETER_PROMPT
|
||||
),
|
||||
form_data["messages"],
|
||||
)
|
||||
# Skip XML-tag prompt injection when native FC is enabled —
|
||||
# execute_code will be injected as a builtin tool instead
|
||||
if metadata.get("params", {}).get("function_calling") != "native":
|
||||
form_data["messages"] = add_or_update_user_message(
|
||||
(
|
||||
request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE
|
||||
if request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE
|
||||
!= ""
|
||||
else DEFAULT_CODE_INTERPRETER_PROMPT
|
||||
),
|
||||
form_data["messages"],
|
||||
)
|
||||
|
||||
tool_ids = form_data.pop("tool_ids", None)
|
||||
files = form_data.pop("files", None)
|
||||
|
||||
# Skills: inject manifest only — model uses view_skill tool to load full content on-demand
|
||||
user_skill_ids = form_data.pop("skill_ids", None) or []
|
||||
model_skill_ids = model.get("info", {}).get("meta", {}).get("skillIds", [])
|
||||
# Skills
|
||||
user_skill_ids = set(form_data.pop("skill_ids", None) or [])
|
||||
model_skill_ids = set(model.get("info", {}).get("meta", {}).get("skillIds", []))
|
||||
|
||||
all_skill_ids = list(set(user_skill_ids + model_skill_ids))
|
||||
all_skill_ids = user_skill_ids | model_skill_ids
|
||||
available_skills = []
|
||||
if all_skill_ids:
|
||||
from open_webui.models.skills import Skills as SkillsModel
|
||||
@@ -2126,13 +2217,24 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
||||
and s.is_active
|
||||
]
|
||||
|
||||
if available_skills:
|
||||
manifest = "<available_skills>\n"
|
||||
for skill in available_skills:
|
||||
manifest += f"<skill>\n<name>{skill.name}</name>\n<description>{skill.description or ''}</description>\n</skill>\n"
|
||||
manifest += "</available_skills>"
|
||||
skill_descriptions = ""
|
||||
for skill in available_skills:
|
||||
if skill.id in user_skill_ids:
|
||||
# User-selected: inject full content
|
||||
form_data["messages"] = add_or_update_system_message(
|
||||
f'<skill name="{skill.name}">\n{skill.content}\n</skill>',
|
||||
form_data["messages"],
|
||||
append=True,
|
||||
)
|
||||
else:
|
||||
# Model-attached: name+description only
|
||||
skill_descriptions += f"<skill>\n<name>{skill.name}</name>\n<description>{skill.description or ''}</description>\n</skill>\n"
|
||||
|
||||
if skill_descriptions:
|
||||
form_data["messages"] = add_or_update_system_message(
|
||||
manifest, form_data["messages"], append=True
|
||||
f"<available_skills>\n{skill_descriptions}</available_skills>",
|
||||
form_data["messages"],
|
||||
append=True,
|
||||
)
|
||||
|
||||
prompt = get_last_user_message(form_data["messages"])
|
||||
@@ -2369,7 +2471,9 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
||||
{
|
||||
**extra_params,
|
||||
"__event_emitter__": event_emitter,
|
||||
"__skill_ids__": [s.id for s in available_skills],
|
||||
"__skill_ids__": [
|
||||
s.id for s in available_skills if s.id not in user_skill_ids
|
||||
],
|
||||
},
|
||||
features,
|
||||
model,
|
||||
@@ -2930,11 +3034,14 @@ async def streaming_chat_response_handler(response, ctx):
|
||||
|
||||
# Handle as a background task
|
||||
async def response_handler(response, events):
|
||||
def tag_output_handler(content_type, tags, content, output):
|
||||
def tag_output_handler(content_type, tags, output):
|
||||
"""
|
||||
Detect special tags (reasoning, solution, code_interpreter) in streaming
|
||||
content and create corresponding OR-aligned output items directly.
|
||||
Operates on output items instead of content_blocks.
|
||||
|
||||
Uses the text from the output items themselves for tag detection,
|
||||
eliminating state divergence between accumulated content and items.
|
||||
"""
|
||||
end_flag = False
|
||||
|
||||
@@ -2974,6 +3081,8 @@ async def streaming_chat_response_handler(response, ctx):
|
||||
last_type = output[-1].get("type", "") if output else ""
|
||||
|
||||
if last_type == "message":
|
||||
# Use the output item's own text for tag detection
|
||||
item_text = get_last_text(output)
|
||||
for start_tag, end_tag in tags:
|
||||
|
||||
start_tag_pattern = rf"{re.escape(start_tag)}"
|
||||
@@ -2982,7 +3091,7 @@ async def streaming_chat_response_handler(response, ctx):
|
||||
rf"<{re.escape(start_tag[1:-1])}(\s.*?)?>"
|
||||
)
|
||||
|
||||
match = re.search(start_tag_pattern, content)
|
||||
match = re.search(start_tag_pattern, item_text)
|
||||
if match:
|
||||
try:
|
||||
attr_content = match.group(1) if match.group(1) else ""
|
||||
@@ -2991,20 +3100,13 @@ async def streaming_chat_response_handler(response, ctx):
|
||||
|
||||
attributes = extract_attributes(attr_content)
|
||||
|
||||
before_tag = content[: match.start()]
|
||||
after_tag = content[match.end() :]
|
||||
before_tag = item_text[: match.start()]
|
||||
after_tag = item_text[match.end() :]
|
||||
|
||||
# Remove the start tag and everything after from last message
|
||||
current_text = get_last_text(output)
|
||||
set_last_text(
|
||||
output,
|
||||
current_text.replace(match.group(0) + after_tag, ""),
|
||||
)
|
||||
# Keep only text before the tag in the message
|
||||
set_last_text(output, before_tag)
|
||||
|
||||
if before_tag:
|
||||
set_last_text(output, before_tag)
|
||||
|
||||
if not get_last_text(output).strip():
|
||||
if not before_tag.strip():
|
||||
# Remove empty message item
|
||||
if output and output[-1].get("type") == "message":
|
||||
output.pop()
|
||||
@@ -3069,9 +3171,11 @@ async def streaming_chat_response_handler(response, ctx):
|
||||
else:
|
||||
set_last_text(output, after_tag)
|
||||
|
||||
tag_output_handler(
|
||||
content_type, tags, after_tag, output
|
||||
_, recursive_end = tag_output_handler(
|
||||
content_type, tags, output
|
||||
)
|
||||
if recursive_end:
|
||||
end_flag = True
|
||||
|
||||
break
|
||||
|
||||
@@ -3090,25 +3194,22 @@ async def streaming_chat_response_handler(response, ctx):
|
||||
start_tag = item.get("start_tag", "")
|
||||
end_tag = item.get("end_tag", "")
|
||||
|
||||
if end_tag.startswith("<") and end_tag.endswith(">"):
|
||||
end_tag_pattern = rf"{re.escape(end_tag)}"
|
||||
end_tag_pattern = rf"{re.escape(end_tag)}"
|
||||
|
||||
# Get the block content from the item itself
|
||||
if last_type == "reasoning":
|
||||
parts = item.get("content", [])
|
||||
block_content = ""
|
||||
if parts and parts[-1].get("type") == "output_text":
|
||||
block_content = parts[-1].get("text", "")
|
||||
elif last_type == "open_webui:code_interpreter":
|
||||
block_content = item.get("code", "")
|
||||
else:
|
||||
end_tag_pattern = rf"{re.escape(end_tag)}"
|
||||
block_content = get_last_text(output)
|
||||
|
||||
if re.search(end_tag_pattern, content):
|
||||
if re.search(end_tag_pattern, block_content):
|
||||
end_flag = True
|
||||
|
||||
# Get the block content
|
||||
if last_type == "reasoning":
|
||||
parts = item.get("content", [])
|
||||
block_content = ""
|
||||
if parts and parts[-1].get("type") == "output_text":
|
||||
block_content = parts[-1].get("text", "")
|
||||
elif last_type == "open_webui:code_interpreter":
|
||||
block_content = item.get("code", "")
|
||||
else:
|
||||
block_content = get_last_text(output)
|
||||
|
||||
# Strip start and end tags from content
|
||||
start_tag_pattern = rf"{re.escape(start_tag)}"
|
||||
if start_tag.startswith("<") and start_tag.endswith(">"):
|
||||
@@ -3151,36 +3252,20 @@ async def streaming_chat_response_handler(response, ctx):
|
||||
item["ended_at"] = time.time()
|
||||
|
||||
# Reset by appending a new message item for leftover
|
||||
if content_type != "code_interpreter":
|
||||
output.append(
|
||||
{
|
||||
"type": "message",
|
||||
"id": output_id("msg"),
|
||||
"status": "in_progress",
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{
|
||||
"type": "output_text",
|
||||
"text": leftover_content,
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
else:
|
||||
output.append(
|
||||
{
|
||||
"type": "message",
|
||||
"id": output_id("msg"),
|
||||
"status": "in_progress",
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{
|
||||
"type": "output_text",
|
||||
"text": leftover_content,
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
output.append(
|
||||
{
|
||||
"type": "message",
|
||||
"id": output_id("msg"),
|
||||
"status": "in_progress",
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{
|
||||
"type": "output_text",
|
||||
"text": leftover_content,
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
else:
|
||||
# Remove the block if content is empty
|
||||
output.pop()
|
||||
@@ -3199,19 +3284,7 @@ async def streaming_chat_response_handler(response, ctx):
|
||||
}
|
||||
)
|
||||
|
||||
# Clean processed content
|
||||
start_tag_clean = rf"{re.escape(start_tag)}"
|
||||
if start_tag.startswith("<") and start_tag.endswith(">"):
|
||||
start_tag_clean = rf"<{re.escape(start_tag[1:-1])}(\s.*?)?>"
|
||||
|
||||
content = re.sub(
|
||||
rf"{start_tag_clean}(.|\n)*?{re.escape(end_tag)}",
|
||||
"",
|
||||
content,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
|
||||
return content, output, end_flag
|
||||
return output, end_flag
|
||||
|
||||
message = Chats.get_message_by_id_and_message_id(
|
||||
metadata["chat_id"], metadata["message_id"]
|
||||
@@ -3674,58 +3747,132 @@ async def streaming_chat_response_handler(response, ctx):
|
||||
)
|
||||
|
||||
content = f"{content}{value}"
|
||||
if (
|
||||
not output
|
||||
or output[-1].get("type") != "message"
|
||||
):
|
||||
output.append(
|
||||
{
|
||||
"type": "message",
|
||||
"id": output_id("msg"),
|
||||
"status": "in_progress",
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
|
||||
# Check if we're inside a tag-based block
|
||||
# (reasoning, code_interpreter, or solution).
|
||||
# If so, append to the existing in-progress
|
||||
# item instead of creating a new message —
|
||||
# otherwise tag_output_handler re-detects the
|
||||
# start tag on every chunk and fragments the
|
||||
# output.
|
||||
last_item = output[-1] if output else None
|
||||
last_item_type = (
|
||||
last_item.get("type", "")
|
||||
if last_item
|
||||
else ""
|
||||
)
|
||||
inside_tag_block = (
|
||||
last_item is not None
|
||||
and last_item.get("status") == "in_progress"
|
||||
and last_item.get("attributes", {}).get(
|
||||
"type"
|
||||
)
|
||||
!= "reasoning_content"
|
||||
and (
|
||||
last_item_type == "reasoning"
|
||||
or last_item_type
|
||||
== "open_webui:code_interpreter"
|
||||
or (
|
||||
last_item_type == "message"
|
||||
and last_item.get("_tag_type")
|
||||
is not None
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if inside_tag_block:
|
||||
# Append to the existing tag-based item
|
||||
if (
|
||||
last_item_type
|
||||
== "open_webui:code_interpreter"
|
||||
):
|
||||
last_item["code"] = (
|
||||
last_item.get("code", "") + value
|
||||
)
|
||||
elif last_item_type == "reasoning":
|
||||
parts = last_item.get("content", [])
|
||||
if (
|
||||
parts
|
||||
and parts[-1].get("type")
|
||||
== "output_text"
|
||||
):
|
||||
parts[-1]["text"] += value
|
||||
else:
|
||||
last_item["content"] = [
|
||||
{
|
||||
"type": "output_text",
|
||||
"text": "",
|
||||
"text": value,
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
# Append value to last message item's text
|
||||
msg_parts = output[-1].get("content", [])
|
||||
if (
|
||||
msg_parts
|
||||
and msg_parts[-1].get("type")
|
||||
== "output_text"
|
||||
):
|
||||
msg_parts[-1]["text"] += value
|
||||
]
|
||||
else:
|
||||
# solution or other _tag_type message
|
||||
msg_parts = last_item.get("content", [])
|
||||
if (
|
||||
msg_parts
|
||||
and msg_parts[-1].get("type")
|
||||
== "output_text"
|
||||
):
|
||||
msg_parts[-1]["text"] += value
|
||||
else:
|
||||
last_item["content"] = [
|
||||
{
|
||||
"type": "output_text",
|
||||
"text": value,
|
||||
}
|
||||
]
|
||||
else:
|
||||
output[-1]["content"] = [
|
||||
{"type": "output_text", "text": value}
|
||||
]
|
||||
if (
|
||||
not output
|
||||
or output[-1].get("type") != "message"
|
||||
):
|
||||
output.append(
|
||||
{
|
||||
"type": "message",
|
||||
"id": output_id("msg"),
|
||||
"status": "in_progress",
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{
|
||||
"type": "output_text",
|
||||
"text": "",
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
# Append value to last message item's text
|
||||
msg_parts = output[-1].get("content", [])
|
||||
if (
|
||||
msg_parts
|
||||
and msg_parts[-1].get("type")
|
||||
== "output_text"
|
||||
):
|
||||
msg_parts[-1]["text"] += value
|
||||
else:
|
||||
output[-1]["content"] = [
|
||||
{
|
||||
"type": "output_text",
|
||||
"text": value,
|
||||
}
|
||||
]
|
||||
|
||||
if DETECT_REASONING_TAGS:
|
||||
content, output, _ = tag_output_handler(
|
||||
output, _ = tag_output_handler(
|
||||
"reasoning",
|
||||
reasoning_tags,
|
||||
content,
|
||||
output,
|
||||
)
|
||||
|
||||
content, output, _ = tag_output_handler(
|
||||
output, _ = tag_output_handler(
|
||||
"solution",
|
||||
DEFAULT_SOLUTION_TAGS,
|
||||
content,
|
||||
output,
|
||||
)
|
||||
|
||||
if DETECT_CODE_INTERPRETER:
|
||||
content, output, end = tag_output_handler(
|
||||
output, end = tag_output_handler(
|
||||
"code_interpreter",
|
||||
DEFAULT_CODE_INTERPRETER_TAGS,
|
||||
content,
|
||||
output,
|
||||
)
|
||||
|
||||
|
||||
@@ -230,25 +230,27 @@ def convert_output_to_messages(output: list, raw: bool = False) -> list[dict]:
|
||||
# else: skip reasoning blocks for normal LLM messages
|
||||
|
||||
elif item_type == "open_webui:code_interpreter":
|
||||
if raw:
|
||||
# Include code interpreter content for LLM re-processing
|
||||
code = item.get("code", "")
|
||||
code_output = item.get("output", "")
|
||||
# Always include code interpreter content so the LLM knows
|
||||
# the code was already executed and doesn't retry.
|
||||
code = item.get("code", "")
|
||||
code_output = item.get("output", "")
|
||||
|
||||
if code:
|
||||
lang = item.get("lang", "python")
|
||||
pending_content.append(f"```{lang}\n{code}\n```")
|
||||
if code:
|
||||
pending_content.append(
|
||||
f"<code_interpreter>\n{code}\n</code_interpreter>"
|
||||
)
|
||||
|
||||
if code_output:
|
||||
if isinstance(code_output, dict):
|
||||
stdout = code_output.get("stdout", "")
|
||||
result = code_output.get("result", "")
|
||||
output_text = stdout or result
|
||||
else:
|
||||
output_text = str(code_output)
|
||||
if output_text:
|
||||
pending_content.append(f"Output:\n{output_text}")
|
||||
# else: skip extension types
|
||||
if code_output:
|
||||
if isinstance(code_output, dict):
|
||||
stdout = code_output.get("stdout", "")
|
||||
result = code_output.get("result", "")
|
||||
output_text = stdout or result
|
||||
else:
|
||||
output_text = str(code_output)
|
||||
if output_text:
|
||||
pending_content.append(
|
||||
f"<code_interpreter_output>\n{output_text}\n</code_interpreter_output>"
|
||||
)
|
||||
|
||||
elif item_type.startswith("open_webui:"):
|
||||
# Skip other extension types
|
||||
@@ -445,7 +447,7 @@ def openai_chat_completion_message_template(
|
||||
**({"tool_calls": tool_calls} if tool_calls else {}),
|
||||
}
|
||||
|
||||
template["choices"][0]["finish_reason"] = "stop"
|
||||
template["choices"][0]["finish_reason"] = "tool_calls" if tool_calls else "stop"
|
||||
|
||||
if usage:
|
||||
template["usage"] = usage
|
||||
|
||||
@@ -286,9 +286,26 @@ async def get_all_models(request, refresh: bool = False, user: UserModel = None)
|
||||
}
|
||||
]
|
||||
|
||||
def get_function_module_by_id(function_id):
|
||||
function_module, _, _ = get_function_module_from_cache(request, function_id)
|
||||
return function_module
|
||||
# Batch-prefetch all needed function records to avoid N+1 queries
|
||||
all_function_ids = set()
|
||||
for model in models:
|
||||
all_function_ids.update(model.get("action_ids", []))
|
||||
all_function_ids.update(model.get("filter_ids", []))
|
||||
all_function_ids.update(global_action_ids)
|
||||
all_function_ids.update(global_filter_ids)
|
||||
|
||||
functions_by_id = {
|
||||
f.id: f for f in Functions.get_functions_by_ids(list(all_function_ids))
|
||||
}
|
||||
|
||||
# Pre-warm the function module cache once per unique function ID.
|
||||
# This ensures each function's DB freshness check runs exactly once,
|
||||
# not once per (model × function) pair.
|
||||
for function_id in all_function_ids:
|
||||
try:
|
||||
get_function_module_from_cache(request, function_id)
|
||||
except Exception as e:
|
||||
log.info(f"Failed to load function module for {function_id}: {e}")
|
||||
|
||||
for model in models:
|
||||
action_ids = [
|
||||
@@ -304,23 +321,30 @@ async def get_all_models(request, refresh: bool = False, user: UserModel = None)
|
||||
|
||||
model["actions"] = []
|
||||
for action_id in action_ids:
|
||||
action_function = Functions.get_function_by_id(action_id)
|
||||
action_function = functions_by_id.get(action_id)
|
||||
if action_function is None:
|
||||
raise Exception(f"Action not found: {action_id}")
|
||||
log.info(f"Action not found: {action_id}")
|
||||
continue
|
||||
|
||||
function_module = get_function_module_by_id(action_id)
|
||||
function_module = request.app.state.FUNCTIONS.get(action_id)
|
||||
if function_module is None:
|
||||
log.info(f"Failed to load action module: {action_id}")
|
||||
continue
|
||||
model["actions"].extend(
|
||||
get_action_items_from_module(action_function, function_module)
|
||||
)
|
||||
|
||||
model["filters"] = []
|
||||
for filter_id in filter_ids:
|
||||
filter_function = Functions.get_function_by_id(filter_id)
|
||||
filter_function = functions_by_id.get(filter_id)
|
||||
if filter_function is None:
|
||||
raise Exception(f"Filter not found: {filter_id}")
|
||||
|
||||
function_module = get_function_module_by_id(filter_id)
|
||||
log.info(f"Filter not found: {filter_id}")
|
||||
continue
|
||||
|
||||
function_module = request.app.state.FUNCTIONS.get(filter_id)
|
||||
if function_module is None:
|
||||
log.info(f"Failed to load filter module: {filter_id}")
|
||||
continue
|
||||
if getattr(function_module, "toggle", None):
|
||||
model["filters"].extend(
|
||||
get_filter_items_from_module(filter_function, function_module)
|
||||
@@ -371,16 +395,29 @@ def get_filtered_models(models, user, db=None):
|
||||
user.role == "user"
|
||||
or (user.role == "admin" and not BYPASS_ADMIN_ACCESS_CONTROL)
|
||||
) and not BYPASS_MODEL_ACCESS_CONTROL:
|
||||
model_ids = [model["id"] for model in models if not model.get("arena")]
|
||||
model_infos = {
|
||||
model_info.id: model_info
|
||||
for model_info in Models.get_models_by_ids(model_ids)
|
||||
}
|
||||
model_infos = {}
|
||||
for model in models:
|
||||
if model.get("arena"):
|
||||
continue
|
||||
info = model.get("info")
|
||||
if info:
|
||||
model_infos[model["id"]] = info
|
||||
|
||||
filtered_models = []
|
||||
user_group_ids = {
|
||||
group.id for group in Groups.get_groups_by_member_id(user.id, db=db)
|
||||
}
|
||||
|
||||
# Batch-fetch accessible resource IDs in a single query instead of N has_access calls
|
||||
accessible_model_ids = AccessGrants.get_accessible_resource_ids(
|
||||
user_id=user.id,
|
||||
resource_type="model",
|
||||
resource_ids=list(model_infos.keys()),
|
||||
permission="read",
|
||||
user_group_ids=user_group_ids,
|
||||
db=db,
|
||||
)
|
||||
|
||||
filtered_models = []
|
||||
for model in models:
|
||||
if model.get("arena"):
|
||||
meta = model.get("info", {}).get("meta", {})
|
||||
@@ -394,19 +431,12 @@ def get_filtered_models(models, user, db=None):
|
||||
filtered_models.append(model)
|
||||
continue
|
||||
|
||||
model_info = model_infos.get(model["id"], None)
|
||||
model_info = model_infos.get(model["id"])
|
||||
if model_info:
|
||||
if (
|
||||
(user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL)
|
||||
or user.id == model_info.user_id
|
||||
or AccessGrants.has_access(
|
||||
user_id=user.id,
|
||||
resource_type="model",
|
||||
resource_id=model_info.id,
|
||||
permission="read",
|
||||
user_group_ids=user_group_ids,
|
||||
db=db,
|
||||
)
|
||||
or user.id == model_info["user_id"]
|
||||
or model["id"] in accessible_model_ids
|
||||
):
|
||||
filtered_models.append(model)
|
||||
|
||||
|
||||
@@ -166,6 +166,9 @@ async def convert_streaming_response_ollama_to_openai(ollama_streaming_response)
|
||||
model, message_content, reasoning_content, openai_tool_calls, usage
|
||||
)
|
||||
|
||||
if done and openai_tool_calls:
|
||||
data["choices"][0]["finish_reason"] = "tool_calls"
|
||||
|
||||
line = f"data: {json.dumps(data)}\n\n"
|
||||
yield line
|
||||
|
||||
|
||||
@@ -77,6 +77,7 @@ from open_webui.tools.builtin import (
|
||||
query_knowledge_bases,
|
||||
search_knowledge_files,
|
||||
query_knowledge_files,
|
||||
view_file,
|
||||
view_knowledge_file,
|
||||
view_skill,
|
||||
)
|
||||
@@ -445,6 +446,12 @@ def get_builtin_tools(
|
||||
if model_knowledge:
|
||||
# Model has attached knowledge - only allow semantic search within it
|
||||
builtin_functions.append(query_knowledge_files)
|
||||
|
||||
knowledge_types = {item.get("type") for item in model_knowledge}
|
||||
if "file" in knowledge_types or "collection" in knowledge_types:
|
||||
builtin_functions.append(view_file)
|
||||
if "note" in knowledge_types:
|
||||
builtin_functions.append(view_note)
|
||||
else:
|
||||
# No model knowledge - allow full KB browsing
|
||||
builtin_functions.extend(
|
||||
@@ -471,6 +478,7 @@ def get_builtin_tools(
|
||||
is_builtin_tool_enabled("web_search")
|
||||
and getattr(request.app.state.config, "ENABLE_WEB_SEARCH", False)
|
||||
and get_model_capability("web_search")
|
||||
and features.get("web_search")
|
||||
):
|
||||
builtin_functions.extend([search_web, fetch_url])
|
||||
|
||||
@@ -479,12 +487,14 @@ def get_builtin_tools(
|
||||
is_builtin_tool_enabled("image_generation")
|
||||
and getattr(request.app.state.config, "ENABLE_IMAGE_GENERATION", False)
|
||||
and get_model_capability("image_generation")
|
||||
and features.get("image_generation")
|
||||
):
|
||||
builtin_functions.append(generate_image)
|
||||
if (
|
||||
is_builtin_tool_enabled("image_generation")
|
||||
and getattr(request.app.state.config, "ENABLE_IMAGE_EDIT", False)
|
||||
and get_model_capability("image_generation")
|
||||
and features.get("image_generation")
|
||||
):
|
||||
builtin_functions.append(edit_image)
|
||||
|
||||
@@ -493,6 +503,7 @@ def get_builtin_tools(
|
||||
is_builtin_tool_enabled("code_interpreter")
|
||||
and getattr(request.app.state.config, "ENABLE_CODE_INTERPRETER", True)
|
||||
and get_model_capability("code_interpreter")
|
||||
and features.get("code_interpreter")
|
||||
):
|
||||
builtin_functions.append(execute_code)
|
||||
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "open-webui",
|
||||
"version": "0.8.0",
|
||||
"version": "0.8.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "open-webui",
|
||||
"version": "0.8.0",
|
||||
"version": "0.8.3",
|
||||
"dependencies": {
|
||||
"@azure/msal-browser": "^4.5.0",
|
||||
"@codemirror/lang-javascript": "^6.2.2",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "open-webui",
|
||||
"version": "0.8.0",
|
||||
"version": "0.8.3",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "npm run pyodide:fetch && vite dev --host",
|
||||
|
||||
@@ -241,7 +241,8 @@ export const imageEdits = async (
|
||||
prompt: string,
|
||||
model?: string,
|
||||
size?: string,
|
||||
n?: number
|
||||
n?: number,
|
||||
background?: string
|
||||
) => {
|
||||
let error = null;
|
||||
|
||||
@@ -258,7 +259,8 @@ export const imageEdits = async (
|
||||
prompt,
|
||||
...(model && { model }),
|
||||
...(size && { size }),
|
||||
...(n && { n })
|
||||
...(n && { n }),
|
||||
...(background && { background })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -250,8 +250,12 @@
|
||||
const submitHandler = async () => {
|
||||
loading = true;
|
||||
|
||||
// remove trailing slash from url
|
||||
url = url.replace(/\/$/, '');
|
||||
// remove trailing slash from url for non-MCP connections
|
||||
// MCP servers may require a trailing slash; stripping it can cause
|
||||
// 301 redirects that lose auth headers (see #21179)
|
||||
if (type !== 'mcp') {
|
||||
url = url.replace(/\/$/, '');
|
||||
}
|
||||
if (id.includes(':') || id.includes('|')) {
|
||||
toast.error($i18n.t('ID cannot contain ":" or "|" characters'));
|
||||
loading = false;
|
||||
|
||||
@@ -49,188 +49,123 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<form
|
||||
class="flex flex-col h-full justify-between space-y-3 text-sm"
|
||||
on:submit|preventDefault={async () => {
|
||||
saveHandler();
|
||||
}}
|
||||
>
|
||||
<div class=" space-y-3 overflow-y-scroll scrollbar-hidden h-full">
|
||||
<div>
|
||||
<div class=" mb-2 text-sm font-medium">{$i18n.t('Database')}</div>
|
||||
<div class="flex flex-col h-full justify-between text-sm">
|
||||
<div class="space-y-3 overflow-y-scroll scrollbar-hidden h-full">
|
||||
<input
|
||||
id="config-json-input"
|
||||
hidden
|
||||
type="file"
|
||||
accept=".json"
|
||||
on:change={(e) => {
|
||||
const file = e.target.files[0];
|
||||
const reader = new FileReader();
|
||||
|
||||
<input
|
||||
id="config-json-input"
|
||||
hidden
|
||||
type="file"
|
||||
accept=".json"
|
||||
on:change={(e) => {
|
||||
const file = e.target.files[0];
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = async (e) => {
|
||||
const res = await importConfig(localStorage.token, JSON.parse(e.target.result)).catch(
|
||||
(error) => {
|
||||
toast.error(`${error}`);
|
||||
}
|
||||
);
|
||||
|
||||
if (res) {
|
||||
toast.success($i18n.t('Config imported successfully'));
|
||||
reader.onload = async (e) => {
|
||||
const res = await importConfig(localStorage.token, JSON.parse(e.target.result)).catch(
|
||||
(error) => {
|
||||
toast.error(`${error}`);
|
||||
}
|
||||
e.target.value = null;
|
||||
};
|
||||
);
|
||||
|
||||
reader.readAsText(file);
|
||||
}}
|
||||
/>
|
||||
if (res) {
|
||||
toast.success($i18n.t('Config imported successfully'));
|
||||
}
|
||||
e.target.value = null;
|
||||
};
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class=" flex rounded-md py-2 px-3 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
|
||||
on:click={async () => {
|
||||
document.getElementById('config-json-input').click();
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-3">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path d="M2 3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3Z" />
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M13 6H3v6a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V6ZM8.75 7.75a.75.75 0 0 0-1.5 0v2.69L6.03 9.22a.75.75 0 0 0-1.06 1.06l2.5 2.5a.75.75 0 0 0 1.06 0l2.5-2.5a.75.75 0 1 0-1.06-1.06l-1.22 1.22V7.75Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center text-sm font-medium">
|
||||
{$i18n.t('Import Config from JSON File')}
|
||||
</div>
|
||||
</button>
|
||||
reader.readAsText(file);
|
||||
}}
|
||||
/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class=" flex rounded-md py-2 px-3 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
|
||||
on:click={async () => {
|
||||
const config = await exportConfig(localStorage.token);
|
||||
const blob = new Blob([JSON.stringify(config)], {
|
||||
type: 'application/json'
|
||||
});
|
||||
saveAs(blob, `config-${Date.now()}.json`);
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-3">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path d="M2 3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3Z" />
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M13 6H3v6a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V6ZM8.75 7.75a.75.75 0 0 0-1.5 0v2.69L6.03 9.22a.75.75 0 0 0-1.06 1.06l2.5 2.5a.75.75 0 0 0 1.06 0l2.5-2.5a.75.75 0 1 0-1.06-1.06l-1.22 1.22V7.75Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center text-sm font-medium">
|
||||
{$i18n.t('Export Config to JSON File')}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<hr class="border-gray-50 dark:border-gray-850/30 my-1" />
|
||||
|
||||
{#if $config?.features.enable_admin_export ?? true}
|
||||
<div class=" flex w-full justify-between">
|
||||
<!-- <div class=" self-center text-xs font-medium">{$i18n.t('Allow Chat Deletion')}</div> -->
|
||||
<div>
|
||||
<div class="mb-1 text-sm font-medium">{$i18n.t('Config')}</div>
|
||||
|
||||
<div>
|
||||
<div class="py-0.5 flex w-full justify-between">
|
||||
<div class="self-center text-xs">{$i18n.t('Import Config')}</div>
|
||||
<button
|
||||
class=" flex rounded-md py-1.5 px-3 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
|
||||
type="button"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
on:click={() => {
|
||||
// exportAllUserChats();
|
||||
|
||||
downloadDatabase(localStorage.token).catch((error) => {
|
||||
toast.error(`${error}`);
|
||||
});
|
||||
document.getElementById('config-json-input').click();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<div class=" self-center mr-3">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path d="M2 3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3Z" />
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M13 6H3v6a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V6ZM8.75 7.75a.75.75 0 0 0-1.5 0v2.69L6.03 9.22a.75.75 0 0 0-1.06 1.06l2.5 2.5a.75.75 0 0 0 1.06 0l2.5-2.5a.75.75 0 1 0-1.06-1.06l-1.22 1.22V7.75Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center text-sm font-medium">{$i18n.t('Download Database')}</div>
|
||||
<span class="self-center">{$i18n.t('Import')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class=" flex rounded-md py-2 px-3 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
|
||||
on:click={() => {
|
||||
exportAllUserChats();
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-3">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path d="M2 3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3Z" />
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M13 6H3v6a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V6ZM8.75 7.75a.75.75 0 0 0-1.5 0v2.69L6.03 9.22a.75.75 0 0 0-1.06 1.06l2.5 2.5a.75.75 0 0 0 1.06 0l2.5-2.5a.75.75 0 1 0-1.06-1.06l-1.22 1.22V7.75Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center text-sm font-medium">
|
||||
{$i18n.t('Export All Chats (All Users)')}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class=" flex rounded-md py-2 px-3 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
|
||||
on:click={() => {
|
||||
exportUsers();
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-3">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path d="M2 3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3Z" />
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M13 6H3v6a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V6ZM8.75 7.75a.75.75 0 0 0-1.5 0v2.69L6.03 9.22a.75.75 0 0 0-1.06 1.06l2.5 2.5a.75.75 0 0 0 1.06 0l2.5-2.5a.75.75 0 1 0-1.06-1.06l-1.22 1.22V7.75Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center text-sm font-medium">
|
||||
{$i18n.t('Export Users')}
|
||||
</div>
|
||||
</button>
|
||||
{/if}
|
||||
<div>
|
||||
<div class="py-0.5 flex w-full justify-between">
|
||||
<div class="self-center text-xs">{$i18n.t('Export Config')}</div>
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
on:click={async () => {
|
||||
const config = await exportConfig(localStorage.token);
|
||||
const blob = new Blob([JSON.stringify(config)], {
|
||||
type: 'application/json'
|
||||
});
|
||||
saveAs(blob, `config-${Date.now()}.json`);
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<span class="self-center">{$i18n.t('Export')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if $config?.features.enable_admin_export ?? true}
|
||||
<div>
|
||||
<div class="mb-1 text-sm font-medium">{$i18n.t('Database')}</div>
|
||||
|
||||
<div>
|
||||
<div class="py-0.5 flex w-full justify-between">
|
||||
<div class="self-center text-xs">{$i18n.t('Download Database')}</div>
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
on:click={() => {
|
||||
downloadDatabase(localStorage.token).catch((error) => {
|
||||
toast.error(`${error}`);
|
||||
});
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<span class="self-center">{$i18n.t('Download')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="py-0.5 flex w-full justify-between">
|
||||
<div class="self-center text-xs">{$i18n.t('Export All Chats (All Users)')}</div>
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
on:click={() => {
|
||||
exportAllUserChats();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<span class="self-center">{$i18n.t('Export')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="py-0.5 flex w-full justify-between">
|
||||
<div class="self-center text-xs">{$i18n.t('Export Users')}</div>
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
on:click={() => {
|
||||
exportUsers();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<span class="self-center">{$i18n.t('Export')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -152,7 +152,15 @@
|
||||
if (taskConfig.TASK_MODEL) {
|
||||
const model = models.find((m) => m.id === taskConfig.TASK_MODEL);
|
||||
if (model) {
|
||||
if (model?.access_control !== null) {
|
||||
if (
|
||||
model?.access_grants &&
|
||||
!model.access_grants.some(
|
||||
(g) =>
|
||||
g.principal_type === 'user' &&
|
||||
g.principal_id === '*' &&
|
||||
g.permission === 'read'
|
||||
)
|
||||
) {
|
||||
toast.error(
|
||||
$i18n.t(
|
||||
'This model is not publicly available. Please select another model.'
|
||||
@@ -187,7 +195,15 @@
|
||||
if (taskConfig.TASK_MODEL_EXTERNAL) {
|
||||
const model = models.find((m) => m.id === taskConfig.TASK_MODEL_EXTERNAL);
|
||||
if (model) {
|
||||
if (model?.access_control !== null) {
|
||||
if (
|
||||
model?.access_grants &&
|
||||
!model.access_grants.some(
|
||||
(g) =>
|
||||
g.principal_type === 'user' &&
|
||||
g.principal_id === '*' &&
|
||||
g.permission === 'read'
|
||||
)
|
||||
) {
|
||||
toast.error(
|
||||
$i18n.t(
|
||||
'This model is not publicly available. Please select another model.'
|
||||
|
||||
@@ -160,7 +160,7 @@
|
||||
name: model.name,
|
||||
base_model_id: null,
|
||||
params: {},
|
||||
access_control: {},
|
||||
access_grants: [],
|
||||
...model
|
||||
}).catch((error) => {
|
||||
return null;
|
||||
@@ -188,7 +188,7 @@
|
||||
base_model_id: null,
|
||||
meta: {},
|
||||
params: {},
|
||||
access_control: {},
|
||||
access_grants: [],
|
||||
is_active: model.is_active
|
||||
}).catch((error) => {
|
||||
return null;
|
||||
|
||||
@@ -324,6 +324,40 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<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('Skills Sharing')}
|
||||
</div>
|
||||
<Switch bind:state={permissions.sharing.skills} />
|
||||
</div>
|
||||
{#if defaultPermissions?.sharing?.skills && !permissions.sharing.skills}
|
||||
<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 permissions.sharing.skills}
|
||||
<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('Skills Public Sharing')}
|
||||
</div>
|
||||
<Switch bind:state={permissions.sharing.public_skills} />
|
||||
</div>
|
||||
{#if defaultPermissions?.sharing?.public_skills && !permissions.sharing.public_skills}
|
||||
<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 class="flex flex-col w-full">
|
||||
<div class="flex w-full justify-between my-1">
|
||||
<div class=" self-center text-xs font-medium">
|
||||
|
||||
@@ -2085,7 +2085,11 @@
|
||||
),
|
||||
features: getFeatures(),
|
||||
variables: {
|
||||
...getPromptVariables($user?.name, $settings?.userLocation ? userLocation : undefined)
|
||||
...getPromptVariables(
|
||||
$user?.name,
|
||||
$settings?.userLocation ? userLocation : undefined,
|
||||
$user?.email
|
||||
)
|
||||
},
|
||||
model_item: $models.find((m) => m.id === model.id),
|
||||
|
||||
|
||||
@@ -186,12 +186,16 @@
|
||||
}}
|
||||
data-selected={idx === selectedIdx}
|
||||
>
|
||||
<div class=" text-black dark:text-gray-100 flex items-center gap-1 shrink-0">
|
||||
<div class="w-full text-left text-black dark:text-gray-100 flex items-center gap-1">
|
||||
<Tooltip content={$i18n.t('Collection')} placement="top">
|
||||
<Database className="size-4" />
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content={item.description || decodeString(item?.name)} placement="top-start">
|
||||
<Tooltip
|
||||
content={item.description || decodeString(item?.name)}
|
||||
placement="top-start"
|
||||
className="flex flex-1 min-w-0"
|
||||
>
|
||||
<div class="line-clamp-1 flex-1 text-sm">
|
||||
{decodeString(item?.name)}
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { getContext, onMount, tick } from 'svelte';
|
||||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import Markdown from '$lib/components/chat/Messages/Markdown.svelte';
|
||||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||
import { settings } from '$lib/stores';
|
||||
|
||||
@@ -10,6 +11,9 @@
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
const CONTENT_PREVIEW_LIMIT = 10000;
|
||||
let expandedDocs: Set<number> = new Set();
|
||||
|
||||
export let show = false;
|
||||
export let citation;
|
||||
export let showPercentage = false;
|
||||
@@ -35,6 +39,7 @@
|
||||
}
|
||||
|
||||
$: if (citation) {
|
||||
expandedDocs = new Set();
|
||||
mergedDocuments = citation.document?.map((c, i) => {
|
||||
return {
|
||||
source: citation.source,
|
||||
@@ -216,9 +221,36 @@
|
||||
title={$i18n.t('Content')}
|
||||
></iframe>
|
||||
{:else}
|
||||
<pre class="text-sm dark:text-gray-400 whitespace-pre-line">{document.document
|
||||
.trim()
|
||||
.replace(/\n\n+/g, '\n\n')}</pre>
|
||||
{@const rawContent = document.document.trim().replace(/\n\n+/g, '\n\n')}
|
||||
{@const isTruncated =
|
||||
($settings?.renderMarkdownInPreviews ?? true) &&
|
||||
rawContent.length > CONTENT_PREVIEW_LIMIT &&
|
||||
!expandedDocs.has(documentIdx)}
|
||||
{#if $settings?.renderMarkdownInPreviews ?? true}
|
||||
<div class="text-sm prose dark:prose-invert max-w-full">
|
||||
<Markdown
|
||||
content={isTruncated
|
||||
? rawContent.slice(0, CONTENT_PREVIEW_LIMIT)
|
||||
: rawContent}
|
||||
id="citation-{documentIdx}"
|
||||
/>
|
||||
</div>
|
||||
{#if isTruncated}
|
||||
<button
|
||||
class="mt-1 text-xs text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition"
|
||||
on:click={() => {
|
||||
expandedDocs.add(documentIdx);
|
||||
expandedDocs = expandedDocs;
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Show all ({{COUNT}} characters)', {
|
||||
COUNT: rawContent.length.toLocaleString()
|
||||
})}
|
||||
</button>
|
||||
{/if}
|
||||
{:else}
|
||||
<pre class="text-sm dark:text-gray-400 whitespace-pre-line">{rawContent}</pre>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -417,13 +417,13 @@
|
||||
|
||||
<div>
|
||||
<div
|
||||
class="relative {className} flex flex-col rounded-3xl border border-gray-100/30 dark:border-gray-850/30 my-0.5"
|
||||
class="relative {className} flex flex-col rounded-2xl border border-gray-100/30 dark:border-gray-850/30 my-0.5"
|
||||
dir="ltr"
|
||||
>
|
||||
{#if ['mermaid', 'vega', 'vega-lite'].includes(lang)}
|
||||
{#if renderHTML}
|
||||
<SvgPanZoom
|
||||
className=" rounded-3xl max-h-fit overflow-hidden"
|
||||
className=" rounded-2xl max-h-fit overflow-hidden"
|
||||
svg={renderHTML}
|
||||
content={_token.text}
|
||||
/>
|
||||
@@ -441,13 +441,13 @@
|
||||
{/if}
|
||||
{:else}
|
||||
<div
|
||||
class="absolute left-0 right-0 py-2.5 pr-3 text-text-300 pl-4.5 text-xs font-medium dark:text-white"
|
||||
class="absolute left-0 right-0 py-1.5 pr-3 text-text-300 pl-4.5 text-xs font-medium dark:text-white"
|
||||
>
|
||||
{lang}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="sticky {stickyButtonsClassName} left-0 right-0 py-2 pr-3 flex items-center justify-end w-full z-10 text-xs text-black dark:text-white"
|
||||
class="sticky {stickyButtonsClassName} left-0 right-0 py-1.5 pr-3 flex items-center justify-end w-full z-10 text-xs text-black dark:text-white"
|
||||
>
|
||||
<div class="flex items-center gap-0.5">
|
||||
<button
|
||||
@@ -514,13 +514,13 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="language-{lang} rounded-t-3xl -mt-9 {editorClassName
|
||||
class="language-{lang} rounded-t-2xl -mt-8 {editorClassName
|
||||
? editorClassName
|
||||
: executing || stdout || stderr || result
|
||||
? ''
|
||||
: 'rounded-b-3xl'} overflow-hidden"
|
||||
: 'rounded-b-2xl'} overflow-hidden"
|
||||
>
|
||||
<div class=" pt-8 bg-white dark:bg-black"></div>
|
||||
<div class=" pt-6.5 bg-white dark:bg-black"></div>
|
||||
|
||||
{#if !collapsed}
|
||||
{#if edit}
|
||||
@@ -550,7 +550,7 @@
|
||||
{/if}
|
||||
{:else}
|
||||
<div
|
||||
class="bg-white dark:bg-black dark:text-white rounded-b-3xl! pt-0.5 pb-3 px-4 flex flex-col gap-2 text-xs"
|
||||
class="bg-white dark:bg-black dark:text-white rounded-b-2xl! pt-0.5 pb-2 px-4 flex flex-col gap-2 text-xs"
|
||||
>
|
||||
<span class="text-gray-500 italic">
|
||||
{$i18n.t('{{COUNT}} hidden lines', {
|
||||
@@ -569,7 +569,7 @@
|
||||
|
||||
{#if executing || stdout || stderr || result || files}
|
||||
<div
|
||||
class="bg-gray-50 dark:bg-black dark:text-white rounded-b-3xl! py-4 px-4 flex flex-col gap-2"
|
||||
class="bg-gray-50 dark:bg-black dark:text-white rounded-b-2xl! py-4 px-4 flex flex-col gap-2"
|
||||
>
|
||||
{#if executing}
|
||||
<div class=" ">
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
import TextToken from './MarkdownInlineTokens/TextToken.svelte';
|
||||
import CodespanToken from './MarkdownInlineTokens/CodespanToken.svelte';
|
||||
import MentionToken from './MarkdownInlineTokens/MentionToken.svelte';
|
||||
import NoteLinkToken from './MarkdownInlineTokens/NoteLinkToken.svelte';
|
||||
import SourceToken from './SourceToken.svelte';
|
||||
|
||||
export let id: string;
|
||||
@@ -26,6 +27,24 @@
|
||||
export let sourceIds = [];
|
||||
export let onSourceClick: Function = () => {};
|
||||
|
||||
/**
|
||||
* Check if a URL is a same-origin note link and return the note ID if so.
|
||||
*/
|
||||
const getNoteIdFromHref = (href: string): string | null => {
|
||||
try {
|
||||
const url = new URL(href, window.location.origin);
|
||||
if (url.origin === window.location.origin) {
|
||||
const match = url.pathname.match(/^\/notes\/([^/]+)$/);
|
||||
if (match) {
|
||||
return match[1];
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Invalid URL
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle link clicks - intercept same-origin app URLs for in-app navigation
|
||||
*/
|
||||
@@ -54,7 +73,10 @@
|
||||
{:else if token.type === 'html'}
|
||||
<HtmlToken {id} {token} {onSourceClick} />
|
||||
{:else if token.type === 'link'}
|
||||
{#if token.tokens}
|
||||
{@const noteId = getNoteIdFromHref(token.href)}
|
||||
{#if noteId}
|
||||
<NoteLinkToken {noteId} href={token.href} />
|
||||
{:else if token.tokens}
|
||||
<a
|
||||
href={token.href}
|
||||
target="_blank"
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
<script lang="ts">
|
||||
import { onMount, getContext } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { getNoteById } from '$lib/apis/notes';
|
||||
import { getUserInfoById } from '$lib/apis/users';
|
||||
import { capitalizeFirstLetter } from '$lib/utils';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let noteId: string;
|
||||
export let href: string;
|
||||
|
||||
let title = '';
|
||||
let author = '';
|
||||
let loading = true;
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
const note = await getNoteById(localStorage.token, noteId);
|
||||
if (note) {
|
||||
title = note.title || $i18n.t('Untitled');
|
||||
|
||||
if (note.user_id) {
|
||||
try {
|
||||
const userInfo = await getUserInfoById(localStorage.token, note.user_id);
|
||||
if (userInfo) {
|
||||
author = capitalizeFirstLetter(userInfo.name ?? userInfo.email ?? '');
|
||||
}
|
||||
} catch {
|
||||
// user lookup failed, skip author
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
title = $i18n.t('Note');
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<button
|
||||
class="relative group py-2 px-3 w-60 flex flex-col bg-white dark:bg-gray-850 border border-gray-50/30 dark:border-gray-800/30 rounded-xl text-left cursor-pointer"
|
||||
type="button"
|
||||
on:click|preventDefault|stopPropagation={() => {
|
||||
try {
|
||||
const url = new URL(href, window.location.origin);
|
||||
goto(url.pathname);
|
||||
} catch {
|
||||
// fallback
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-col justify-center w-full min-w-0">
|
||||
<div class="dark:text-gray-100 text-sm flex justify-between items-center gap-2">
|
||||
<div class="font-medium line-clamp-1 flex-1 min-w-0">
|
||||
{#if loading}
|
||||
<span class="text-gray-400">...</span>
|
||||
{:else}
|
||||
{title}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="text-gray-500 text-xs shrink-0">{$i18n.t('Note')}</div>
|
||||
</div>
|
||||
{#if author}
|
||||
<div class="text-gray-500 text-xs line-clamp-1 mt-0.5">
|
||||
{$i18n.t('By {{name}}', { name: author })}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
@@ -365,8 +365,13 @@
|
||||
|
||||
await tick();
|
||||
|
||||
const messagesContainer = document.getElementById('messages-container');
|
||||
const savedScrollTop = messagesContainer?.scrollTop;
|
||||
|
||||
editTextAreaElement.style.height = '';
|
||||
editTextAreaElement.style.height = `${editTextAreaElement.scrollHeight}px`;
|
||||
|
||||
if (messagesContainer) messagesContainer.scrollTop = savedScrollTop;
|
||||
};
|
||||
|
||||
const editMessageConfirmHandler = async () => {
|
||||
@@ -697,8 +702,13 @@
|
||||
class=" bg-transparent outline-hidden w-full resize-none"
|
||||
bind:value={editedContent}
|
||||
on:input={(e) => {
|
||||
const messagesContainer = document.getElementById('messages-container');
|
||||
const savedScrollTop = messagesContainer?.scrollTop;
|
||||
|
||||
e.target.style.height = '';
|
||||
e.target.style.height = `${e.target.scrollHeight}px`;
|
||||
|
||||
if (messagesContainer) messagesContainer.scrollTop = savedScrollTop;
|
||||
}}
|
||||
on:keydown={(e) => {
|
||||
if (e.key === 'Escape') {
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
let editedFiles = [];
|
||||
|
||||
let messageEditTextAreaElement: HTMLTextAreaElement;
|
||||
let editScrollContainer: HTMLDivElement;
|
||||
|
||||
let message = JSON.parse(JSON.stringify(history.messages[messageId]));
|
||||
$: if (history.messages) {
|
||||
@@ -73,10 +74,14 @@
|
||||
await tick();
|
||||
|
||||
if (messageEditTextAreaElement) {
|
||||
const messagesContainer = document.getElementById('messages-container');
|
||||
const savedScrollTop = messagesContainer?.scrollTop;
|
||||
|
||||
messageEditTextAreaElement.style.height = '';
|
||||
messageEditTextAreaElement.style.height = `${messageEditTextAreaElement.scrollHeight}px`;
|
||||
|
||||
messageEditTextAreaElement?.focus();
|
||||
if (messagesContainer) messagesContainer.scrollTop = savedScrollTop;
|
||||
messageEditTextAreaElement?.focus({ preventScroll: true });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -283,15 +288,22 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="max-h-96 overflow-auto">
|
||||
<div class="max-h-96 overflow-auto" bind:this={editScrollContainer}>
|
||||
<textarea
|
||||
id="message-edit-{message.id}"
|
||||
bind:this={messageEditTextAreaElement}
|
||||
class=" bg-transparent outline-hidden w-full resize-none"
|
||||
bind:value={editedContent}
|
||||
on:input={(e) => {
|
||||
const messagesContainer = document.getElementById('messages-container');
|
||||
const savedScrollTop = messagesContainer?.scrollTop;
|
||||
const savedInnerScroll = editScrollContainer?.scrollTop;
|
||||
|
||||
e.target.style.height = '';
|
||||
e.target.style.height = `${e.target.scrollHeight}px`;
|
||||
|
||||
if (messagesContainer) messagesContainer.scrollTop = savedScrollTop;
|
||||
if (editScrollContainer) editScrollContainer.scrollTop = savedInnerScroll;
|
||||
}}
|
||||
on:keydown={(e) => {
|
||||
if (e.key === 'Escape') {
|
||||
|
||||
@@ -3,13 +3,15 @@
|
||||
import { flyAndScale } from '$lib/utils/transitions';
|
||||
|
||||
import { getContext } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import Pin from '$lib/components/icons/Pin.svelte';
|
||||
import PinSlash from '$lib/components/icons/PinSlash.svelte';
|
||||
import DocumentDuplicate from '$lib/components/icons/DocumentDuplicate.svelte';
|
||||
import Link from '$lib/components/icons/Link.svelte';
|
||||
import { config, settings } from '$lib/stores';
|
||||
import Pencil from '$lib/components/icons/Pencil.svelte';
|
||||
import { config, settings, user } from '$lib/stores';
|
||||
import GlobeAlt from '$lib/components/icons/GlobeAlt.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
@@ -52,6 +54,30 @@
|
||||
align="end"
|
||||
transition={flyAndScale}
|
||||
>
|
||||
{#if model?.preset || model?.info?.base_model_id ? model?.info?.user_id === $user?.id : $user?.role === 'admin'}
|
||||
<DropdownMenu.Item
|
||||
type="button"
|
||||
class="flex rounded-xl py-1.5 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition items-center gap-2"
|
||||
on:click={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
goto(
|
||||
model?.preset || model?.info?.base_model_id
|
||||
? `/workspace/models/edit?id=${encodeURIComponent(model?.id ?? '')}`
|
||||
: `/admin/settings/models?id=${encodeURIComponent(model?.id ?? '')}`
|
||||
);
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<Pencil className="size-4" />
|
||||
|
||||
<div class="flex items-center">{$i18n.t('Edit')}</div>
|
||||
</DropdownMenu.Item>
|
||||
|
||||
<hr class="border-gray-50 dark:border-gray-800/30 my-1" />
|
||||
{/if}
|
||||
|
||||
<DropdownMenu.Item
|
||||
type="button"
|
||||
aria-pressed={($settings?.pinnedModels ?? []).includes(model?.id)}
|
||||
@@ -96,7 +122,7 @@
|
||||
</DropdownMenu.Item>
|
||||
|
||||
{#if $config?.features.enable_community_sharing}
|
||||
<hr class="border-gray-50 dark:border-gray-850/30 my-1" />
|
||||
<hr class="border-gray-50 dark:border-gray-800/30 my-1" />
|
||||
|
||||
<DropdownMenu.Item
|
||||
type="button"
|
||||
|
||||
@@ -178,6 +178,16 @@
|
||||
selectedModelIdx = 0;
|
||||
}
|
||||
|
||||
// Set the virtual scroll position so the selected item is rendered and centered
|
||||
const targetScrollTop = Math.max(0, selectedModelIdx * ITEM_HEIGHT - 128 + ITEM_HEIGHT / 2);
|
||||
listScrollTop = targetScrollTop;
|
||||
|
||||
await tick();
|
||||
|
||||
if (listContainer) {
|
||||
listContainer.scrollTop = targetScrollTop;
|
||||
}
|
||||
|
||||
await tick();
|
||||
const item = document.querySelector(`[data-arrow-selected="true"]`);
|
||||
item?.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'instant' });
|
||||
@@ -379,6 +389,7 @@
|
||||
bind:open={show}
|
||||
onOpenChange={async () => {
|
||||
searchValue = '';
|
||||
listScrollTop = 0;
|
||||
window.setTimeout(() => document.getElementById('model-search-input')?.focus(), 0);
|
||||
|
||||
resetView();
|
||||
|
||||
@@ -70,6 +70,7 @@
|
||||
let chatFadeStreamingText = true;
|
||||
let collapseCodeBlocks = false;
|
||||
let expandDetails = false;
|
||||
let renderMarkdownInPreviews = true;
|
||||
let showChatTitleInTab = true;
|
||||
|
||||
let showFloatingActionButtons = true;
|
||||
@@ -232,6 +233,7 @@
|
||||
|
||||
collapseCodeBlocks = $settings?.collapseCodeBlocks ?? false;
|
||||
expandDetails = $settings?.expandDetails ?? false;
|
||||
renderMarkdownInPreviews = $settings?.renderMarkdownInPreviews ?? true;
|
||||
|
||||
landingPageMode = $settings?.landingPageMode ?? '';
|
||||
chatBubble = $settings?.chatBubble ?? true;
|
||||
@@ -964,6 +966,25 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div id="render-markdown-in-previews-label" class=" self-center text-xs">
|
||||
{$i18n.t('Render Markdown in Previews')}
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 p-1">
|
||||
<Switch
|
||||
ariaLabelledbyId="render-markdown-in-previews-label"
|
||||
tooltip={true}
|
||||
bind:state={renderMarkdownInPreviews}
|
||||
on:change={() => {
|
||||
saveSettings({ renderMarkdownInPreviews });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div id="keep-followup-prompts-label" class=" self-center text-xs">
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
import { formatFileSize, getLineCount } from '$lib/utils';
|
||||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||
import { settings } from '$lib/stores';
|
||||
import { getKnowledgeById } from '$lib/apis/knowledge';
|
||||
import { getFileById, getFileContentById } from '$lib/apis/files';
|
||||
|
||||
@@ -14,6 +15,9 @@
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
const CONTENT_PREVIEW_LIMIT = 10000;
|
||||
let expandedContent = false;
|
||||
|
||||
import Modal from './Modal.svelte';
|
||||
import XMark from '../icons/XMark.svelte';
|
||||
import Switch from './Switch.svelte';
|
||||
@@ -30,6 +34,7 @@
|
||||
|
||||
let isPDF = false;
|
||||
let isAudio = false;
|
||||
let isImage = false;
|
||||
let isExcel = false;
|
||||
|
||||
let selectedTab = '';
|
||||
@@ -79,6 +84,18 @@
|
||||
(item?.name && item?.name.toLowerCase().endsWith('.m4a')) ||
|
||||
(item?.name && item?.name.toLowerCase().endsWith('.webm'));
|
||||
|
||||
$: isImage =
|
||||
(item?.meta?.content_type ?? '').startsWith('image/') ||
|
||||
(item?.name &&
|
||||
(item.name.toLowerCase().endsWith('.png') ||
|
||||
item.name.toLowerCase().endsWith('.jpg') ||
|
||||
item.name.toLowerCase().endsWith('.jpeg') ||
|
||||
item.name.toLowerCase().endsWith('.gif') ||
|
||||
item.name.toLowerCase().endsWith('.webp') ||
|
||||
item.name.toLowerCase().endsWith('.svg') ||
|
||||
item.name.toLowerCase().endsWith('.bmp') ||
|
||||
item.name.toLowerCase().endsWith('.ico')));
|
||||
|
||||
$: isExcel =
|
||||
item?.meta?.content_type === 'application/vnd.ms-excel' ||
|
||||
item?.meta?.content_type ===
|
||||
@@ -134,6 +151,7 @@
|
||||
|
||||
const loadContent = async () => {
|
||||
selectedTab = '';
|
||||
expandedContent = false;
|
||||
if (item?.type === 'collection') {
|
||||
loading = true;
|
||||
|
||||
@@ -341,15 +359,80 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if selectedTab === ''}
|
||||
{#if isImage}
|
||||
<div class="w-full max-h-[70vh] overflow-auto">
|
||||
<img
|
||||
src={`${WEBUI_API_BASE_URL}/files/${item.id}/content`}
|
||||
alt={item?.name ?? 'Image'}
|
||||
class="w-full object-contain rounded-lg"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
{:else if selectedTab === ''}
|
||||
{#if item?.file?.data}
|
||||
<div class="max-h-96 overflow-scroll scrollbar-hidden text-xs whitespace-pre-wrap">
|
||||
{(item?.file?.data?.content ?? '').trim() || 'No content'}
|
||||
</div>
|
||||
{@const rawContent = (item?.file?.data?.content ?? '').trim() || 'No content'}
|
||||
{@const isTruncated =
|
||||
($settings?.renderMarkdownInPreviews ?? true) &&
|
||||
rawContent.length > CONTENT_PREVIEW_LIMIT &&
|
||||
!expandedContent}
|
||||
{#if $settings?.renderMarkdownInPreviews ?? true}
|
||||
<div
|
||||
class="max-h-96 overflow-scroll scrollbar-hidden text-sm prose dark:prose-invert max-w-full"
|
||||
>
|
||||
<Markdown
|
||||
content={isTruncated ? rawContent.slice(0, CONTENT_PREVIEW_LIMIT) : rawContent}
|
||||
id="file-preview"
|
||||
/>
|
||||
</div>
|
||||
{#if isTruncated}
|
||||
<button
|
||||
class="mt-1 text-xs text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition"
|
||||
on:click={() => {
|
||||
expandedContent = true;
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Show all ({{COUNT}} characters)', {
|
||||
COUNT: rawContent.length.toLocaleString()
|
||||
})}
|
||||
</button>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="max-h-96 overflow-scroll scrollbar-hidden text-xs whitespace-pre-wrap">
|
||||
{rawContent}
|
||||
</div>
|
||||
{/if}
|
||||
{:else if item?.content}
|
||||
<div class="max-h-96 overflow-scroll scrollbar-hidden text-xs whitespace-pre-wrap">
|
||||
{(item?.content ?? '').trim() || 'No content'}
|
||||
</div>
|
||||
{@const rawContent = (item?.content ?? '').trim() || 'No content'}
|
||||
{@const isTruncated =
|
||||
($settings?.renderMarkdownInPreviews ?? true) &&
|
||||
rawContent.length > CONTENT_PREVIEW_LIMIT &&
|
||||
!expandedContent}
|
||||
{#if $settings?.renderMarkdownInPreviews ?? true}
|
||||
<div
|
||||
class="max-h-96 overflow-scroll scrollbar-hidden text-sm prose dark:prose-invert max-w-full"
|
||||
>
|
||||
<Markdown
|
||||
content={isTruncated ? rawContent.slice(0, CONTENT_PREVIEW_LIMIT) : rawContent}
|
||||
id="file-preview-content"
|
||||
/>
|
||||
</div>
|
||||
{#if isTruncated}
|
||||
<button
|
||||
class="mt-1 text-xs text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition"
|
||||
on:click={() => {
|
||||
expandedContent = true;
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Show all ({{COUNT}} characters)', {
|
||||
COUNT: rawContent.length.toLocaleString()
|
||||
})}
|
||||
</button>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="max-h-96 overflow-scroll scrollbar-hidden text-xs whitespace-pre-wrap">
|
||||
{rawContent}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{:else if selectedTab === 'preview'}
|
||||
{#if isAudio}
|
||||
|
||||
@@ -691,7 +691,12 @@
|
||||
element: element,
|
||||
extensions: [
|
||||
StarterKit.configure({
|
||||
link: link
|
||||
link: link,
|
||||
// When rich text is off, disable Strike from StarterKit so we can
|
||||
// re-add it below without its Mod-Shift-s shortcut (which conflicts
|
||||
// with the Toggle Sidebar shortcut). When rich text is on, the user
|
||||
// can undo strikethrough via the toolbar, so the shortcut is fine.
|
||||
...(richText ? {} : { strike: false })
|
||||
}),
|
||||
...(dragHandle ? [ListItemDragHandle] : []),
|
||||
Placeholder.configure({ placeholder: () => _placeholder, showOnlyWhenEditable: false }),
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
import ChevronDown from '../icons/ChevronDown.svelte';
|
||||
import Spinner from './Spinner.svelte';
|
||||
import Markdown from '../chat/Messages/Markdown.svelte';
|
||||
import WrenchSolid from '../icons/WrenchSolid.svelte';
|
||||
import CheckCircle from '../icons/CheckCircle.svelte';
|
||||
import Image from './Image.svelte';
|
||||
import FullHeightIframe from './FullHeightIframe.svelte';
|
||||
|
||||
@@ -45,26 +47,36 @@
|
||||
function formatJSONString(str: string) {
|
||||
try {
|
||||
const parsed = parseJSONString(str);
|
||||
// If parsed is an object/array, then it's valid JSON
|
||||
if (typeof parsed === 'object') {
|
||||
return JSON.stringify(parsed, null, 2);
|
||||
} else {
|
||||
// It's a primitive value like a number, boolean, etc.
|
||||
return `${JSON.stringify(String(parsed))}`;
|
||||
}
|
||||
} catch (e) {
|
||||
// Not valid JSON, return as-is
|
||||
return str;
|
||||
}
|
||||
}
|
||||
|
||||
// Decode and parse attributes
|
||||
function parseArguments(str: string): Record<string, unknown> | null {
|
||||
try {
|
||||
const parsed = parseJSONString(str);
|
||||
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
|
||||
return parsed as Record<string, unknown>;
|
||||
}
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
$: args = decode(attributes?.arguments ?? '');
|
||||
$: result = decode(attributes?.result ?? '');
|
||||
$: files = parseJSONString(decode(attributes?.files ?? ''));
|
||||
$: embeds = parseJSONString(decode(attributes?.embeds ?? ''));
|
||||
$: isDone = attributes?.done === 'true';
|
||||
$: isExecuting = attributes?.done && attributes?.done !== 'true';
|
||||
|
||||
$: parsedArgs = parseArguments(args);
|
||||
</script>
|
||||
|
||||
<div {id} class={className}>
|
||||
@@ -72,11 +84,8 @@
|
||||
<!-- Embed Mode: Show iframes without collapsible behavior -->
|
||||
<div class="py-1 w-full cursor-pointer">
|
||||
<div class="w-full text-xs text-gray-500">
|
||||
<div class="">
|
||||
{attributes.name}
|
||||
</div>
|
||||
{attributes.name}
|
||||
</div>
|
||||
|
||||
{#each embeds as embed, idx}
|
||||
<div class="my-2" id={`${componentId}-tool-call-embed-${idx}`}>
|
||||
<FullHeightIframe
|
||||
@@ -91,7 +100,8 @@
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Standard collapsible tool call display -->
|
||||
<!-- Tool call display -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="{buttonClassName} cursor-pointer"
|
||||
on:pointerup={() => {
|
||||
@@ -99,35 +109,51 @@
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="w-full font-medium flex items-center justify-between gap-2 {isExecuting
|
||||
class="w-full max-w-full font-medium flex items-center gap-1.5 {isExecuting
|
||||
? 'shimmer'
|
||||
: ''}"
|
||||
>
|
||||
<!-- Status icon -->
|
||||
{#if isExecuting}
|
||||
<div>
|
||||
<Spinner className="size-4" />
|
||||
</div>
|
||||
{:else if isDone}
|
||||
<div class="text-emerald-500 dark:text-emerald-400">
|
||||
<CheckCircle className="size-4" strokeWidth="2" />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-gray-400 dark:text-gray-500">
|
||||
<WrenchSolid className="size-3.5" />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="">
|
||||
{#if isDone}
|
||||
<Markdown
|
||||
id={`${componentId}-tool-call-title`}
|
||||
content={$i18n.t('View Result from **{{NAME}}**', {
|
||||
NAME: attributes.name
|
||||
})}
|
||||
/>
|
||||
{:else}
|
||||
<Markdown
|
||||
id={`${componentId}-tool-call-executing`}
|
||||
content={$i18n.t('Executing **{{NAME}}**...', {
|
||||
NAME: attributes.name
|
||||
})}
|
||||
/>
|
||||
{/if}
|
||||
<!-- Label -->
|
||||
<div class="flex-1 line-clamp-1">
|
||||
<!-- Short label (below md) -->
|
||||
<span class="@md:hidden font-semibold text-black dark:text-white">{attributes.name}</span>
|
||||
<!-- Full label (md and above) -->
|
||||
<span class="hidden @md:inline">
|
||||
{#if isDone}
|
||||
<Markdown
|
||||
id={`${componentId}-tool-call-title`}
|
||||
content={$i18n.t('View Result from **{{NAME}}**', {
|
||||
NAME: attributes.name
|
||||
})}
|
||||
/>
|
||||
{:else}
|
||||
<Markdown
|
||||
id={`${componentId}-tool-call-executing`}
|
||||
content={$i18n.t('Executing **{{NAME}}**...', {
|
||||
NAME: attributes.name
|
||||
})}
|
||||
/>
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex self-center translate-y-[1px]">
|
||||
<!-- Chevron -->
|
||||
<div class="flex shrink-0 self-center translate-y-[1px]">
|
||||
{#if open}
|
||||
<ChevronUp strokeWidth="3.5" className="size-3.5" />
|
||||
{:else}
|
||||
@@ -139,22 +165,57 @@
|
||||
|
||||
{#if open}
|
||||
<div transition:slide={{ duration: 300, easing: quintOut, axis: 'y' }}>
|
||||
{#if isDone}
|
||||
<Markdown
|
||||
id={`${componentId}-tool-call-result`}
|
||||
content={`> \`\`\`json
|
||||
> ${formatJSONString(args)}
|
||||
> ${formatJSONString(result)}
|
||||
> \`\`\``}
|
||||
/>
|
||||
{:else}
|
||||
<Markdown
|
||||
id={`${componentId}-tool-call-args`}
|
||||
content={`> \`\`\`json
|
||||
> ${formatJSONString(args)}
|
||||
> \`\`\``}
|
||||
/>
|
||||
{/if}
|
||||
<div class="border border-gray-50 dark:border-gray-850/30 rounded-2xl my-1.5 p-3 space-y-3">
|
||||
<!-- Input -->
|
||||
{#if args}
|
||||
<div>
|
||||
<div
|
||||
class="text-[10px] uppercase tracking-wider font-medium text-gray-400 dark:text-gray-500 mb-1.5 px-1"
|
||||
>
|
||||
{$i18n.t('Input')}
|
||||
</div>
|
||||
|
||||
{#if parsedArgs}
|
||||
<div class="px-1 space-y-0.5">
|
||||
{#each Object.entries(parsedArgs) as [key, value]}
|
||||
<div class="flex gap-2 text-xs py-0.5">
|
||||
<span class="font-medium text-gray-600 dark:text-gray-400 shrink-0"
|
||||
>{key}</span
|
||||
>
|
||||
<span class="text-gray-800 dark:text-gray-200 break-all"
|
||||
>{typeof value === 'object' ? JSON.stringify(value) : value}</span
|
||||
>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="tool-call-body w-full max-w-none!">
|
||||
<Markdown
|
||||
id={`${componentId}-tool-call-args`}
|
||||
content={`\`\`\`json\n${formatJSONString(args)}\n\`\`\``}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Output -->
|
||||
{#if isDone && result}
|
||||
<div>
|
||||
<div
|
||||
class="text-[10px] uppercase tracking-wider font-medium text-gray-400 dark:text-gray-500 mb-1.5 px-1"
|
||||
>
|
||||
{$i18n.t('Output')}
|
||||
</div>
|
||||
<div class="w-full max-w-none!">
|
||||
<Markdown
|
||||
id={`${componentId}-tool-call-result`}
|
||||
content={`\`\`\`json\n${formatJSONString(result)}\n\`\`\``}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import { chatCompletion } from '$lib/apis/openai';
|
||||
|
||||
import { splitStream } from '$lib/utils';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import Selector from '$lib/components/chat/ModelSelector/Selector.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
@@ -160,25 +161,28 @@
|
||||
</div>
|
||||
|
||||
<div class="pb-3 flex justify-end">
|
||||
{#if !loading}
|
||||
<button
|
||||
class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
|
||||
on:click={() => {
|
||||
submitHandler();
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Run')}
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
class="px-3 py-1.5 text-sm font-medium bg-gray-300 text-black transition rounded-full"
|
||||
on:click={() => {
|
||||
stopResponse();
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Cancel')}
|
||||
</button>
|
||||
{/if}
|
||||
<div class="flex gap-2 shrink-0">
|
||||
{#if !loading}
|
||||
<button
|
||||
class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-lg"
|
||||
on:click={() => {
|
||||
submitHandler();
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Run')}
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
class="px-3.5 py-1.5 text-sm font-medium bg-gray-300 text-black transition rounded-lg flex items-center gap-2"
|
||||
on:click={() => {
|
||||
stopResponse();
|
||||
}}
|
||||
>
|
||||
<Spinner className="size-4" />
|
||||
{$i18n.t('Cancel')}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -836,9 +836,7 @@
|
||||
bind:show={showAccessControlModal}
|
||||
bind:accessGrants={knowledge.access_grants}
|
||||
share={$user?.permissions?.sharing?.knowledge || $user?.role === 'admin'}
|
||||
sharePublic={$user?.permissions?.sharing?.public_knowledge ||
|
||||
$user?.role === 'admin' ||
|
||||
knowledge?.write_access}
|
||||
sharePublic={$user?.permissions?.sharing?.public_knowledge || $user?.role === 'admin'}
|
||||
onChange={async () => {
|
||||
try {
|
||||
await updateKnowledgeAccessGrants(localStorage.token, id, knowledge.access_grants ?? []);
|
||||
@@ -856,7 +854,7 @@
|
||||
<div class="w-full flex justify-between items-center">
|
||||
<input
|
||||
type="text"
|
||||
class="text-left w-full font-medium text-lg font-primary bg-transparent outline-hidden flex-1"
|
||||
class="text-left w-full text-lg bg-transparent outline-hidden flex-1"
|
||||
bind:value={knowledge.name}
|
||||
placeholder={$i18n.t('Knowledge Name')}
|
||||
disabled={!knowledge?.write_access}
|
||||
|
||||
@@ -170,15 +170,27 @@
|
||||
>
|
||||
<div class=" text-black dark:text-gray-100 flex items-center gap-1 shrink-0">
|
||||
{#if item.type === 'note'}
|
||||
<Tooltip content={$i18n.t('Note')} placement="top">
|
||||
<Tooltip
|
||||
content={$i18n.t('Note')}
|
||||
placement="top"
|
||||
tippyOptions={{ zIndex: 100000 }}
|
||||
>
|
||||
<PageEdit className="size-4" />
|
||||
</Tooltip>
|
||||
{:else if item.type === 'collection'}
|
||||
<Tooltip content={$i18n.t('Collection')} placement="top">
|
||||
<Tooltip
|
||||
content={$i18n.t('Collection')}
|
||||
placement="top"
|
||||
tippyOptions={{ zIndex: 100000 }}
|
||||
>
|
||||
<Database className="size-4" />
|
||||
</Tooltip>
|
||||
{:else if item.type === 'file'}
|
||||
<Tooltip content={$i18n.t('File')} placement="top">
|
||||
<Tooltip
|
||||
content={$i18n.t('File')}
|
||||
placement="top"
|
||||
tippyOptions={{ zIndex: 100000 }}
|
||||
>
|
||||
<DocumentPage className="size-4" />
|
||||
</Tooltip>
|
||||
{/if}
|
||||
@@ -186,6 +198,7 @@
|
||||
<Tooltip
|
||||
content={item.description || decodeString(item?.name)}
|
||||
placement="top-start"
|
||||
tippyOptions={{ zIndex: 100000 }}
|
||||
>
|
||||
<div class="line-clamp-1 flex-1 text-sm text-left">
|
||||
{decodeString(item?.name)}
|
||||
|
||||
@@ -342,14 +342,14 @@
|
||||
bind:accessGrants
|
||||
accessRoles={preset ? ['read', 'write'] : ['read']}
|
||||
share={$user?.permissions?.sharing?.models || $user?.role === 'admin'}
|
||||
sharePublic={$user?.permissions?.sharing?.public_models || $user?.role === 'admin' || edit}
|
||||
sharePublic={$user?.permissions?.sharing?.public_models || $user?.role === 'admin'}
|
||||
onChange={async () => {
|
||||
if (edit && model?.id) {
|
||||
try {
|
||||
await updateModelAccessGrants(localStorage.token, model.id, accessGrants);
|
||||
toast.success($i18n.t('Saved'));
|
||||
} catch (error) {
|
||||
toast.error(`${error}`);
|
||||
toast.error(error?.detail ?? `${error}`);
|
||||
}
|
||||
}
|
||||
}}
|
||||
@@ -466,11 +466,11 @@
|
||||
}}
|
||||
>
|
||||
<div class="w-full px-1">
|
||||
<div class="flex flex-col md:flex-row gap-4 w-full">
|
||||
<div class="self-center md:self-start flex justify-center my-2 shrink-0">
|
||||
<div class="flex flex-row gap-4 md:gap-6 w-full">
|
||||
<div class="self-start flex justify-center my-2 shrink-0">
|
||||
<div class="self-center">
|
||||
<button
|
||||
class="rounded-xl flex shrink-0 items-center {info.meta.profile_image_url !==
|
||||
class="rounded-2xl flex shrink-0 items-center {info.meta.profile_image_url !==
|
||||
`${WEBUI_BASE_URL}/static/favicon.png`
|
||||
? 'bg-transparent'
|
||||
: 'bg-white'} shadow-xl group relative"
|
||||
@@ -483,13 +483,13 @@
|
||||
<img
|
||||
src={info.meta.profile_image_url}
|
||||
alt="model profile"
|
||||
class="rounded-xl size-60 object-cover shrink-0"
|
||||
class="rounded-xl size-20 md:size-48 object-cover shrink-0"
|
||||
/>
|
||||
{:else}
|
||||
<img
|
||||
src="{WEBUI_BASE_URL}/static/favicon.png"
|
||||
alt="model profile"
|
||||
class=" rounded-xl size-60 object-cover shrink-0"
|
||||
class=" rounded-xl size-20 md:size-48 object-cover shrink-0"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -538,7 +538,7 @@
|
||||
<div class=" flex flex-col w-full">
|
||||
<div class="flex-1 w-full">
|
||||
<input
|
||||
class="text-4xl font-medium w-full bg-transparent outline-hidden"
|
||||
class="text-3xl w-full bg-transparent outline-hidden"
|
||||
placeholder={$i18n.t('Model Name')}
|
||||
bind:value={name}
|
||||
required
|
||||
|
||||
@@ -282,7 +282,7 @@
|
||||
bind:accessGrants
|
||||
accessRoles={['read', 'write']}
|
||||
share={$user?.permissions?.sharing?.prompts || $user?.role === 'admin'}
|
||||
sharePublic={$user?.permissions?.sharing?.public_prompts || $user?.role === 'admin' || edit}
|
||||
sharePublic={$user?.permissions?.sharing?.public_prompts || $user?.role === 'admin'}
|
||||
onChange={async () => {
|
||||
if (edit && prompt?.id) {
|
||||
try {
|
||||
@@ -375,7 +375,7 @@
|
||||
<div class="flex items-start justify-between gap-4 shrink-0">
|
||||
<div class="min-w-0 flex-1">
|
||||
<input
|
||||
class="text-2xl font-medium w-full bg-transparent outline-hidden"
|
||||
class="text-2xl w-full bg-transparent outline-hidden"
|
||||
placeholder={$i18n.t('Prompt Name')}
|
||||
bind:value={name}
|
||||
on:input={debouncedSaveMetadata}
|
||||
@@ -534,7 +534,7 @@
|
||||
<div class="flex flex-col w-full">
|
||||
<div class="flex items-center">
|
||||
<input
|
||||
class="text-2xl font-medium w-full bg-transparent outline-hidden"
|
||||
class="text-2xl w-full bg-transparent outline-hidden"
|
||||
placeholder={$i18n.t('Name')}
|
||||
bind:value={name}
|
||||
required
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
bind:accessGrants
|
||||
accessRoles={['read', 'write']}
|
||||
share={$user?.permissions?.sharing?.skills || $user?.role === 'admin'}
|
||||
sharePublic={$user?.permissions?.sharing?.public_skills || $user?.role === 'admin' || edit}
|
||||
sharePublic={$user?.permissions?.sharing?.public_skills || $user?.role === 'admin'}
|
||||
onChange={async () => {
|
||||
if (edit && skill?.id) {
|
||||
try {
|
||||
@@ -149,7 +149,7 @@
|
||||
<div class="flex-1">
|
||||
<Tooltip content={$i18n.t('e.g. Code Review Guidelines')} placement="top-start">
|
||||
<input
|
||||
class="w-full text-2xl font-medium bg-transparent outline-hidden font-primary"
|
||||
class="w-full text-2xl bg-transparent outline-hidden"
|
||||
type="text"
|
||||
placeholder={$i18n.t('Skill Name')}
|
||||
bind:value={name}
|
||||
|
||||
@@ -192,7 +192,7 @@ class Tools:
|
||||
bind:accessGrants
|
||||
accessRoles={['read', 'write']}
|
||||
share={$user?.permissions?.sharing?.tools || $user?.role === 'admin'}
|
||||
sharePublic={$user?.permissions?.sharing?.public_tools || $user?.role === 'admin' || edit}
|
||||
sharePublic={$user?.permissions?.sharing?.public_tools || $user?.role === 'admin'}
|
||||
onChange={async () => {
|
||||
if (edit && id) {
|
||||
try {
|
||||
@@ -238,7 +238,7 @@ class Tools:
|
||||
<div class="flex-1">
|
||||
<Tooltip content={$i18n.t('e.g. My Tools')} placement="top-start">
|
||||
<input
|
||||
class="w-full text-2xl font-medium bg-transparent outline-hidden font-primary"
|
||||
class="w-full text-2xl bg-transparent outline-hidden"
|
||||
type="text"
|
||||
placeholder={$i18n.t('Tool Name')}
|
||||
bind:value={name}
|
||||
|
||||
@@ -21,6 +21,8 @@ export const DEFAULT_PERMISSIONS = {
|
||||
public_prompts: false,
|
||||
tools: false,
|
||||
public_tools: false,
|
||||
skills: false,
|
||||
public_skills: false,
|
||||
notes: false,
|
||||
public_notes: false
|
||||
},
|
||||
|
||||
@@ -98,7 +98,6 @@
|
||||
"Allow Call": "",
|
||||
"Allow Chat Controls": "",
|
||||
"Allow Chat Delete": "",
|
||||
"Allow Chat Deletion": "يستطيع حذف المحادثات",
|
||||
"Allow Chat Edit": "",
|
||||
"Allow Chat Export": "",
|
||||
"Allow Chat Params": "",
|
||||
@@ -346,6 +345,7 @@
|
||||
"Completions": "",
|
||||
"Compress Images in Channels": "",
|
||||
"Concurrent Requests": "الطلبات المتزامنة",
|
||||
"Config": "",
|
||||
"Config imported successfully": "",
|
||||
"Configure": "",
|
||||
"Confirm": "",
|
||||
@@ -769,7 +769,7 @@
|
||||
"Export All Chats (All Users)": "تصدير جميع الدردشات (جميع المستخدمين)",
|
||||
"Export chat (.json)": "",
|
||||
"Export Chats": "تصدير جميع الدردشات",
|
||||
"Export Config to JSON File": "",
|
||||
"Export Config": "",
|
||||
"Export Models": "",
|
||||
"Export Presets": "",
|
||||
"Export Prompts": "",
|
||||
@@ -976,7 +976,7 @@
|
||||
"Images": "",
|
||||
"Import": "",
|
||||
"Import Chats": "استيراد الدردشات",
|
||||
"Import Config from JSON File": "",
|
||||
"Import Config": "",
|
||||
"Import From Link": "",
|
||||
"Import Models": "",
|
||||
"Import Presets": "",
|
||||
@@ -1342,6 +1342,7 @@
|
||||
"Ordered List": "",
|
||||
"Organize your users": "",
|
||||
"Other": "آخر",
|
||||
"Output": "",
|
||||
"OUTPUT": "",
|
||||
"Output format": "",
|
||||
"Output Format": "",
|
||||
@@ -1478,6 +1479,7 @@
|
||||
"Remove image": "",
|
||||
"Remove Model": "حذف الموديل",
|
||||
"Rename": "إعادة تسمية",
|
||||
"Render Markdown in Previews": "",
|
||||
"Reorder Models": "",
|
||||
"Reply": "",
|
||||
"Reply in Thread": "",
|
||||
@@ -1642,6 +1644,7 @@
|
||||
"Show": "عرض",
|
||||
"Show \"What's New\" modal on login": "",
|
||||
"Show Admin Details in Account Pending Overlay": "",
|
||||
"Show all ({{COUNT}} characters)": "",
|
||||
"Show Files": "",
|
||||
"Show Formatting Toolbar": "",
|
||||
"Show image preview": "",
|
||||
@@ -1669,6 +1672,8 @@
|
||||
"Skill updated successfully": "",
|
||||
"Skills": "",
|
||||
"Skills Access": "",
|
||||
"Skills Public Sharing": "",
|
||||
"Skills Sharing": "",
|
||||
"Skip Cache": "",
|
||||
"Skip the cache and re-run the inference. Defaults to False.": "",
|
||||
"Something went wrong :/": "",
|
||||
|
||||
@@ -98,7 +98,6 @@
|
||||
"Allow Call": "",
|
||||
"Allow Chat Controls": "السماح بوسائل التحكم في المحادثة",
|
||||
"Allow Chat Delete": "السماح بحذف المحادثة",
|
||||
"Allow Chat Deletion": "السماح بحذف المحادثة",
|
||||
"Allow Chat Edit": "السماح بتعديل المحادثة",
|
||||
"Allow Chat Export": "",
|
||||
"Allow Chat Params": "",
|
||||
@@ -346,6 +345,7 @@
|
||||
"Completions": "الإكمالات",
|
||||
"Compress Images in Channels": "",
|
||||
"Concurrent Requests": "الطلبات المتزامنة",
|
||||
"Config": "",
|
||||
"Config imported successfully": "",
|
||||
"Configure": "تكوين",
|
||||
"Confirm": "تأكيد",
|
||||
@@ -769,7 +769,7 @@
|
||||
"Export All Chats (All Users)": "تصدير جميع الدردشات (جميع المستخدمين)",
|
||||
"Export chat (.json)": "تصدير المحادثة (.json)",
|
||||
"Export Chats": "تصدير جميع الدردشات",
|
||||
"Export Config to JSON File": "تصدير الإعدادات إلى ملف JSON",
|
||||
"Export Config": "",
|
||||
"Export Models": "",
|
||||
"Export Presets": "تصدير الإعدادات المسبقة",
|
||||
"Export Prompts": "",
|
||||
@@ -976,7 +976,7 @@
|
||||
"Images": "",
|
||||
"Import": "",
|
||||
"Import Chats": "استيراد الدردشات",
|
||||
"Import Config from JSON File": "استيراد الإعدادات من ملف JSON",
|
||||
"Import Config": "",
|
||||
"Import From Link": "",
|
||||
"Import Models": "",
|
||||
"Import Presets": "استيراد الإعدادات المسبقة",
|
||||
@@ -1342,6 +1342,7 @@
|
||||
"Ordered List": "",
|
||||
"Organize your users": "تنظيم المستخدمين الخاصين بك",
|
||||
"Other": "آخر",
|
||||
"Output": "",
|
||||
"OUTPUT": "الإخراج",
|
||||
"Output format": "تنسيق الإخراج",
|
||||
"Output Format": "",
|
||||
@@ -1478,6 +1479,7 @@
|
||||
"Remove image": "",
|
||||
"Remove Model": "حذف الموديل",
|
||||
"Rename": "إعادة تسمية",
|
||||
"Render Markdown in Previews": "",
|
||||
"Reorder Models": "إعادة ترتيب النماذج",
|
||||
"Reply": "",
|
||||
"Reply in Thread": "الرد داخل سلسلة الرسائل",
|
||||
@@ -1642,6 +1644,7 @@
|
||||
"Show": "عرض",
|
||||
"Show \"What's New\" modal on login": "عرض نافذة \"ما الجديد\" عند تسجيل الدخول",
|
||||
"Show Admin Details in Account Pending Overlay": "عرض تفاصيل المشرف في نافذة \"الحساب قيد الانتظار\"",
|
||||
"Show all ({{COUNT}} characters)": "",
|
||||
"Show Files": "",
|
||||
"Show Formatting Toolbar": "",
|
||||
"Show image preview": "",
|
||||
@@ -1669,6 +1672,8 @@
|
||||
"Skill updated successfully": "",
|
||||
"Skills": "",
|
||||
"Skills Access": "",
|
||||
"Skills Public Sharing": "",
|
||||
"Skills Sharing": "",
|
||||
"Skip Cache": "",
|
||||
"Skip the cache and re-run the inference. Defaults to False.": "",
|
||||
"Something went wrong :/": "",
|
||||
|
||||
@@ -98,7 +98,6 @@
|
||||
"Allow Call": "",
|
||||
"Allow Chat Controls": "Разреши контроли на чата",
|
||||
"Allow Chat Delete": "Разреши изтриване на чат",
|
||||
"Allow Chat Deletion": "Позволи Изтриване на Чат",
|
||||
"Allow Chat Edit": "Разреши редактиране на чат",
|
||||
"Allow Chat Export": "",
|
||||
"Allow Chat Params": "",
|
||||
@@ -346,6 +345,7 @@
|
||||
"Completions": "Довършвания",
|
||||
"Compress Images in Channels": "",
|
||||
"Concurrent Requests": "Едновременни заявки",
|
||||
"Config": "",
|
||||
"Config imported successfully": "",
|
||||
"Configure": "Конфигуриране",
|
||||
"Confirm": "Потвърди",
|
||||
@@ -769,7 +769,7 @@
|
||||
"Export All Chats (All Users)": "Експортване на всички чатове (за всички потребители)",
|
||||
"Export chat (.json)": "Експортиране на чат (.json)",
|
||||
"Export Chats": "Експортване на чатове",
|
||||
"Export Config to JSON File": "Експортиране на конфигурацията в JSON файл",
|
||||
"Export Config": "",
|
||||
"Export Models": "",
|
||||
"Export Presets": "Експортиране на предварителни настройки",
|
||||
"Export Prompts": "",
|
||||
@@ -976,7 +976,7 @@
|
||||
"Images": "",
|
||||
"Import": "",
|
||||
"Import Chats": "Импортване на чатове",
|
||||
"Import Config from JSON File": "Импортиране на конфигурация от JSON файл",
|
||||
"Import Config": "",
|
||||
"Import From Link": "",
|
||||
"Import Models": "",
|
||||
"Import Presets": "Импортиране на предварителни настройки",
|
||||
@@ -1342,6 +1342,7 @@
|
||||
"Ordered List": "",
|
||||
"Organize your users": "Организирайте вашите потребители",
|
||||
"Other": "Друго",
|
||||
"Output": "",
|
||||
"OUTPUT": "ИЗХОД",
|
||||
"Output format": "Изходен формат",
|
||||
"Output Format": "",
|
||||
@@ -1478,6 +1479,7 @@
|
||||
"Remove image": "",
|
||||
"Remove Model": "Изтриване на модела",
|
||||
"Rename": "Преименуване",
|
||||
"Render Markdown in Previews": "",
|
||||
"Reorder Models": "Преорганизиране на моделите",
|
||||
"Reply": "",
|
||||
"Reply in Thread": "Отговори в тред",
|
||||
@@ -1638,6 +1640,7 @@
|
||||
"Show": "Покажи",
|
||||
"Show \"What's New\" modal on login": "Покажи модалния прозорец \"Какво е ново\" при вписване",
|
||||
"Show Admin Details in Account Pending Overlay": "Покажи детайлите на администратора в наслагването на изчакващ акаунт",
|
||||
"Show all ({{COUNT}} characters)": "",
|
||||
"Show Files": "",
|
||||
"Show Formatting Toolbar": "",
|
||||
"Show image preview": "",
|
||||
@@ -1665,6 +1668,8 @@
|
||||
"Skill updated successfully": "",
|
||||
"Skills": "",
|
||||
"Skills Access": "",
|
||||
"Skills Public Sharing": "",
|
||||
"Skills Sharing": "",
|
||||
"Skip Cache": "",
|
||||
"Skip the cache and re-run the inference. Defaults to False.": "",
|
||||
"Something went wrong :/": "",
|
||||
|
||||
@@ -98,7 +98,6 @@
|
||||
"Allow Call": "",
|
||||
"Allow Chat Controls": "",
|
||||
"Allow Chat Delete": "",
|
||||
"Allow Chat Deletion": "চ্যাট ডিলিট করতে দিন",
|
||||
"Allow Chat Edit": "",
|
||||
"Allow Chat Export": "",
|
||||
"Allow Chat Params": "",
|
||||
@@ -346,6 +345,7 @@
|
||||
"Completions": "",
|
||||
"Compress Images in Channels": "",
|
||||
"Concurrent Requests": "সমকালীন অনুরোধ",
|
||||
"Config": "",
|
||||
"Config imported successfully": "",
|
||||
"Configure": "",
|
||||
"Confirm": "",
|
||||
@@ -769,7 +769,7 @@
|
||||
"Export All Chats (All Users)": "সব চ্যাট এক্সপোর্ট করুন (সব ইউজারের)",
|
||||
"Export chat (.json)": "",
|
||||
"Export Chats": "চ্যাটগুলো এক্সপোর্ট করুন",
|
||||
"Export Config to JSON File": "",
|
||||
"Export Config": "",
|
||||
"Export Models": "",
|
||||
"Export Presets": "",
|
||||
"Export Prompts": "",
|
||||
@@ -976,7 +976,7 @@
|
||||
"Images": "",
|
||||
"Import": "",
|
||||
"Import Chats": "চ্যাটগুলি ইমপোর্ট করুন",
|
||||
"Import Config from JSON File": "",
|
||||
"Import Config": "",
|
||||
"Import From Link": "",
|
||||
"Import Models": "",
|
||||
"Import Presets": "",
|
||||
@@ -1342,6 +1342,7 @@
|
||||
"Ordered List": "",
|
||||
"Organize your users": "",
|
||||
"Other": "অন্যান্য",
|
||||
"Output": "",
|
||||
"OUTPUT": "",
|
||||
"Output format": "",
|
||||
"Output Format": "",
|
||||
@@ -1478,6 +1479,7 @@
|
||||
"Remove image": "",
|
||||
"Remove Model": "মডেল রিমুভ করুন",
|
||||
"Rename": "রেনেম",
|
||||
"Render Markdown in Previews": "",
|
||||
"Reorder Models": "",
|
||||
"Reply": "",
|
||||
"Reply in Thread": "",
|
||||
@@ -1638,6 +1640,7 @@
|
||||
"Show": "দেখান",
|
||||
"Show \"What's New\" modal on login": "",
|
||||
"Show Admin Details in Account Pending Overlay": "",
|
||||
"Show all ({{COUNT}} characters)": "",
|
||||
"Show Files": "",
|
||||
"Show Formatting Toolbar": "",
|
||||
"Show image preview": "",
|
||||
@@ -1665,6 +1668,8 @@
|
||||
"Skill updated successfully": "",
|
||||
"Skills": "",
|
||||
"Skills Access": "",
|
||||
"Skills Public Sharing": "",
|
||||
"Skills Sharing": "",
|
||||
"Skip Cache": "",
|
||||
"Skip the cache and re-run the inference. Defaults to False.": "",
|
||||
"Something went wrong :/": "",
|
||||
|
||||
@@ -98,7 +98,6 @@
|
||||
"Allow Call": "",
|
||||
"Allow Chat Controls": "ཁ་བརྡའི་ཚོད་འཛིན་ལ་གནང་བ་སྤྲོད་པ།",
|
||||
"Allow Chat Delete": "ཁ་བརྡ་བསུབ་པར་གནང་བ་སྤྲོད་པ།",
|
||||
"Allow Chat Deletion": "ཁ་བརྡ་བསུབ་པར་གནང་བ་སྤྲོད་པ།",
|
||||
"Allow Chat Edit": "ཁ་བརྡ་ཞུ་དག་ལ་གནང་བ་སྤྲོད་པ།",
|
||||
"Allow Chat Export": "",
|
||||
"Allow Chat Params": "",
|
||||
@@ -346,6 +345,7 @@
|
||||
"Completions": "འགྲུབ་པ།",
|
||||
"Compress Images in Channels": "",
|
||||
"Concurrent Requests": "མཉམ་ལས་རེ་ཞུ།",
|
||||
"Config": "",
|
||||
"Config imported successfully": "",
|
||||
"Configure": "སྒྲིག་འགོད།",
|
||||
"Confirm": "གཏན་འཁེལ།",
|
||||
@@ -769,7 +769,7 @@
|
||||
"Export All Chats (All Users)": "ཁ་བརྡ་ཡོངས་རྫོགས་ཕྱིར་གཏོང་། (བེད་སྤྱོད་མཁན་ཡོངས་)",
|
||||
"Export chat (.json)": "ཁ་བརྡ་ཕྱིར་གཏོང་ (.json)",
|
||||
"Export Chats": "ཁ་བརྡ་ཕྱིར་གཏོང་།",
|
||||
"Export Config to JSON File": "སྒྲིག་འགོད་ JSON ཡིག་ཆར་ཕྱིར་གཏོང་།",
|
||||
"Export Config": "",
|
||||
"Export Models": "",
|
||||
"Export Presets": "སྔོན་སྒྲིག་ཕྱིར་གཏོང་།",
|
||||
"Export Prompts": "",
|
||||
@@ -976,7 +976,7 @@
|
||||
"Images": "",
|
||||
"Import": "",
|
||||
"Import Chats": "ཁ་བརྡ་ནང་འདྲེན།",
|
||||
"Import Config from JSON File": "JSON ཡིག་ཆ་ནས་སྒྲིག་འགོད་ནང་འདྲེན།",
|
||||
"Import Config": "",
|
||||
"Import From Link": "",
|
||||
"Import Models": "",
|
||||
"Import Presets": "སྔོན་སྒྲིག་ནང་འདྲེན།",
|
||||
@@ -1342,6 +1342,7 @@
|
||||
"Ordered List": "",
|
||||
"Organize your users": "ཁྱེད་ཀྱི་བེད་སྤྱོད་མཁན་སྒྲིག་འཛུགས།",
|
||||
"Other": "གཞན།",
|
||||
"Output": "",
|
||||
"OUTPUT": "ཐོན་འབྲས།",
|
||||
"Output format": "ཐོན་འབྲས་ཀྱི་བཀོད་པ།",
|
||||
"Output Format": "",
|
||||
@@ -1478,6 +1479,7 @@
|
||||
"Remove image": "",
|
||||
"Remove Model": "དཔེ་དབྱིབས་འདོར་བ།",
|
||||
"Rename": "མིང་བསྐྱར་འདོགས།",
|
||||
"Render Markdown in Previews": "",
|
||||
"Reorder Models": "དཔེ་དབྱིབས་བསྐྱར་སྒྲིག",
|
||||
"Reply": "",
|
||||
"Reply in Thread": "བརྗོད་གཞིའི་ནང་ལན་འདེབས།",
|
||||
@@ -1637,6 +1639,7 @@
|
||||
"Show": "སྟོན་པ།",
|
||||
"Show \"What's New\" modal on login": "ནང་འཛུལ་སྐབས་ \"གསར་པ་ཅི་ཡོད\" modal སྟོན་པ།",
|
||||
"Show Admin Details in Account Pending Overlay": "རྩིས་ཁྲ་སྒུག་བཞིན་པའི་གཏོགས་ངོས་སུ་དོ་དམ་པའི་ཞིབ་ཕྲ་སྟོན་པ།",
|
||||
"Show all ({{COUNT}} characters)": "",
|
||||
"Show Files": "",
|
||||
"Show Formatting Toolbar": "",
|
||||
"Show image preview": "",
|
||||
@@ -1664,6 +1667,8 @@
|
||||
"Skill updated successfully": "",
|
||||
"Skills": "",
|
||||
"Skills Access": "",
|
||||
"Skills Public Sharing": "",
|
||||
"Skills Sharing": "",
|
||||
"Skip Cache": "",
|
||||
"Skip the cache and re-run the inference. Defaults to False.": "",
|
||||
"Something went wrong :/": "",
|
||||
|
||||
@@ -98,7 +98,6 @@
|
||||
"Allow Call": "Dozvoli poziv",
|
||||
"Allow Chat Controls": "Dozvoli kontrolu razgovora",
|
||||
"Allow Chat Delete": "Dozvoli brisanaje razgovora",
|
||||
"Allow Chat Deletion": "Dopusti brisanje razgovora",
|
||||
"Allow Chat Edit": "Dozvoli uredjivanje razgovora",
|
||||
"Allow Chat Export": "",
|
||||
"Allow Chat Params": "",
|
||||
@@ -346,6 +345,7 @@
|
||||
"Completions": "",
|
||||
"Compress Images in Channels": "",
|
||||
"Concurrent Requests": "Istodobni zahtjevi",
|
||||
"Config": "",
|
||||
"Config imported successfully": "",
|
||||
"Configure": "Promijeni",
|
||||
"Confirm": "Potvrdi",
|
||||
@@ -769,7 +769,7 @@
|
||||
"Export All Chats (All Users)": "Izvoz svih razgovora (svi korisnici)",
|
||||
"Export chat (.json)": "Izvoz četa (.json)",
|
||||
"Export Chats": "Izvoz razgovora",
|
||||
"Export Config to JSON File": "",
|
||||
"Export Config": "",
|
||||
"Export Models": "",
|
||||
"Export Presets": "",
|
||||
"Export Prompts": "",
|
||||
@@ -976,7 +976,7 @@
|
||||
"Images": "",
|
||||
"Import": "",
|
||||
"Import Chats": "Uvoz razgovora",
|
||||
"Import Config from JSON File": "",
|
||||
"Import Config": "",
|
||||
"Import From Link": "",
|
||||
"Import Models": "",
|
||||
"Import Presets": "",
|
||||
@@ -1342,6 +1342,7 @@
|
||||
"Ordered List": "",
|
||||
"Organize your users": "",
|
||||
"Other": "Ostalo",
|
||||
"Output": "",
|
||||
"OUTPUT": "",
|
||||
"Output format": "",
|
||||
"Output Format": "",
|
||||
@@ -1478,6 +1479,7 @@
|
||||
"Remove image": "",
|
||||
"Remove Model": "Ukloni model",
|
||||
"Rename": "Preimenuj",
|
||||
"Render Markdown in Previews": "",
|
||||
"Reorder Models": "",
|
||||
"Reply": "",
|
||||
"Reply in Thread": "",
|
||||
@@ -1639,6 +1641,7 @@
|
||||
"Show": "Pokaži",
|
||||
"Show \"What's New\" modal on login": "",
|
||||
"Show Admin Details in Account Pending Overlay": "",
|
||||
"Show all ({{COUNT}} characters)": "",
|
||||
"Show Files": "",
|
||||
"Show Formatting Toolbar": "",
|
||||
"Show image preview": "",
|
||||
@@ -1666,6 +1669,8 @@
|
||||
"Skill updated successfully": "",
|
||||
"Skills": "",
|
||||
"Skills Access": "",
|
||||
"Skills Public Sharing": "",
|
||||
"Skills Sharing": "",
|
||||
"Skip Cache": "",
|
||||
"Skip the cache and re-run the inference. Defaults to False.": "",
|
||||
"Something went wrong :/": "",
|
||||
|
||||
@@ -98,7 +98,6 @@
|
||||
"Allow Call": "Permetre la trucada",
|
||||
"Allow Chat Controls": "Permetre els controls de xat",
|
||||
"Allow Chat Delete": "Permetre eliminar el xat",
|
||||
"Allow Chat Deletion": "Permetre la supressió del xat",
|
||||
"Allow Chat Edit": "Permetre editar el xat",
|
||||
"Allow Chat Export": "Permetre exportar el xat",
|
||||
"Allow Chat Params": "Permetre els paràmetres de xat",
|
||||
@@ -346,6 +345,7 @@
|
||||
"Completions": "Completaments",
|
||||
"Compress Images in Channels": "Comprimir imatges en els canals",
|
||||
"Concurrent Requests": "Peticions simultànies",
|
||||
"Config": "",
|
||||
"Config imported successfully": "Configuració importada correctament",
|
||||
"Configure": "Configurar",
|
||||
"Confirm": "Confirmar",
|
||||
@@ -769,7 +769,7 @@
|
||||
"Export All Chats (All Users)": "Exportar tots els xats (Tots els usuaris)",
|
||||
"Export chat (.json)": "Exportar el xat (.json)",
|
||||
"Export Chats": "Exportar els xats",
|
||||
"Export Config to JSON File": "Exportar la configuració a un arxiu JSON",
|
||||
"Export Config": "",
|
||||
"Export Models": "Exportar els models",
|
||||
"Export Presets": "Exportar les configuracions",
|
||||
"Export Prompts": "Exportar les indicacions",
|
||||
@@ -976,7 +976,7 @@
|
||||
"Images": "Imatges",
|
||||
"Import": "Importar",
|
||||
"Import Chats": "Importar xats",
|
||||
"Import Config from JSON File": "Importar la configuració des d'un arxiu JSON",
|
||||
"Import Config": "",
|
||||
"Import From Link": "Importar des d'un enllaç",
|
||||
"Import Models": "Importar models",
|
||||
"Import Presets": "Importar configuracions",
|
||||
@@ -1342,6 +1342,7 @@
|
||||
"Ordered List": "Llista ordenada",
|
||||
"Organize your users": "Organitza els teus usuaris",
|
||||
"Other": "Altres",
|
||||
"Output": "",
|
||||
"OUTPUT": "SORTIDA",
|
||||
"Output format": "Format de sortida",
|
||||
"Output Format": "Format de sortida",
|
||||
@@ -1478,6 +1479,7 @@
|
||||
"Remove image": "Eliminar imatge",
|
||||
"Remove Model": "Eliminar el model",
|
||||
"Rename": "Canviar el nom",
|
||||
"Render Markdown in Previews": "",
|
||||
"Reorder Models": "Reordenar els models",
|
||||
"Reply": "Respondre",
|
||||
"Reply in Thread": "Respondre al fil",
|
||||
@@ -1639,6 +1641,7 @@
|
||||
"Show": "Mostrar",
|
||||
"Show \"What's New\" modal on login": "Veure 'Què hi ha de nou' a l'entrada",
|
||||
"Show Admin Details in Account Pending Overlay": "Mostrar els detalls de l'administrador a la superposició del compte pendent",
|
||||
"Show all ({{COUNT}} characters)": "",
|
||||
"Show Files": "Mostra els arxius",
|
||||
"Show Formatting Toolbar": "Mostrar la barra de format",
|
||||
"Show image preview": "Mostrar la previsualització de la imatge",
|
||||
@@ -1666,6 +1669,8 @@
|
||||
"Skill updated successfully": "Habilitat actualitzada correctament",
|
||||
"Skills": "Habilitats",
|
||||
"Skills Access": "",
|
||||
"Skills Public Sharing": "",
|
||||
"Skills Sharing": "",
|
||||
"Skip Cache": "Ometre la memòria cau",
|
||||
"Skip the cache and re-run the inference. Defaults to False.": "Omet la memòria cai i torna a executar la inferència. Per defecte és Fals.",
|
||||
"Something went wrong :/": "Quelcom no ha anat bé",
|
||||
|
||||
@@ -98,7 +98,6 @@
|
||||
"Allow Call": "",
|
||||
"Allow Chat Controls": "",
|
||||
"Allow Chat Delete": "",
|
||||
"Allow Chat Deletion": "Tugoti nga mapapas ang mga chat",
|
||||
"Allow Chat Edit": "",
|
||||
"Allow Chat Export": "",
|
||||
"Allow Chat Params": "",
|
||||
@@ -346,6 +345,7 @@
|
||||
"Completions": "",
|
||||
"Compress Images in Channels": "",
|
||||
"Concurrent Requests": "",
|
||||
"Config": "",
|
||||
"Config imported successfully": "",
|
||||
"Configure": "",
|
||||
"Confirm": "",
|
||||
@@ -769,7 +769,7 @@
|
||||
"Export All Chats (All Users)": "I-export ang tanan nga mga chat (Tanan nga tiggamit)",
|
||||
"Export chat (.json)": "",
|
||||
"Export Chats": "I-export ang mga chat",
|
||||
"Export Config to JSON File": "",
|
||||
"Export Config": "",
|
||||
"Export Models": "",
|
||||
"Export Presets": "",
|
||||
"Export Prompts": "",
|
||||
@@ -976,7 +976,7 @@
|
||||
"Images": "",
|
||||
"Import": "",
|
||||
"Import Chats": "Import nga mga chat",
|
||||
"Import Config from JSON File": "",
|
||||
"Import Config": "",
|
||||
"Import From Link": "",
|
||||
"Import Models": "",
|
||||
"Import Presets": "",
|
||||
@@ -1342,6 +1342,7 @@
|
||||
"Ordered List": "",
|
||||
"Organize your users": "",
|
||||
"Other": "",
|
||||
"Output": "",
|
||||
"OUTPUT": "",
|
||||
"Output format": "",
|
||||
"Output Format": "",
|
||||
@@ -1478,6 +1479,7 @@
|
||||
"Remove image": "",
|
||||
"Remove Model": "",
|
||||
"Rename": "",
|
||||
"Render Markdown in Previews": "",
|
||||
"Reorder Models": "",
|
||||
"Reply": "",
|
||||
"Reply in Thread": "",
|
||||
@@ -1638,6 +1640,7 @@
|
||||
"Show": "Pagpakita",
|
||||
"Show \"What's New\" modal on login": "",
|
||||
"Show Admin Details in Account Pending Overlay": "",
|
||||
"Show all ({{COUNT}} characters)": "",
|
||||
"Show Files": "",
|
||||
"Show Formatting Toolbar": "",
|
||||
"Show image preview": "",
|
||||
@@ -1665,6 +1668,8 @@
|
||||
"Skill updated successfully": "",
|
||||
"Skills": "",
|
||||
"Skills Access": "",
|
||||
"Skills Public Sharing": "",
|
||||
"Skills Sharing": "",
|
||||
"Skip Cache": "",
|
||||
"Skip the cache and re-run the inference. Defaults to False.": "",
|
||||
"Something went wrong :/": "",
|
||||
|
||||
@@ -98,7 +98,6 @@
|
||||
"Allow Call": "Povolit volání",
|
||||
"Allow Chat Controls": "Povolit ovládací prvky chatu",
|
||||
"Allow Chat Delete": "Povolit smazání konverzace",
|
||||
"Allow Chat Deletion": "Povolit smazání konverzace",
|
||||
"Allow Chat Edit": "Povolit úpravu konverzace",
|
||||
"Allow Chat Export": "Povolit export konverzace",
|
||||
"Allow Chat Params": "Povolit parametry chatu",
|
||||
@@ -346,6 +345,7 @@
|
||||
"Completions": "Dokončení",
|
||||
"Compress Images in Channels": "Komprimovat obrázky v kanálech",
|
||||
"Concurrent Requests": "Souběžné požadavky",
|
||||
"Config": "",
|
||||
"Config imported successfully": "Konfigurace byla úspěšně importována",
|
||||
"Configure": "Konfigurovat",
|
||||
"Confirm": "Potvrdit",
|
||||
@@ -769,7 +769,7 @@
|
||||
"Export All Chats (All Users)": "Exportovat všechny konverzace (všichni uživatelé)",
|
||||
"Export chat (.json)": "Exportovat konverzaci (.json)",
|
||||
"Export Chats": "Exportovat konverzace",
|
||||
"Export Config to JSON File": "Exportovat konfiguraci do souboru JSON",
|
||||
"Export Config": "",
|
||||
"Export Models": "",
|
||||
"Export Presets": "Exportovat předvolby",
|
||||
"Export Prompts": "",
|
||||
@@ -976,7 +976,7 @@
|
||||
"Images": "",
|
||||
"Import": "Importovat",
|
||||
"Import Chats": "Importovat konverzace",
|
||||
"Import Config from JSON File": "Importovat konfiguraci ze souboru JSON",
|
||||
"Import Config": "",
|
||||
"Import From Link": "Importovat z odkazu",
|
||||
"Import Models": "",
|
||||
"Import Presets": "Importovat předvolby",
|
||||
@@ -1342,6 +1342,7 @@
|
||||
"Ordered List": "Číslovaný seznam",
|
||||
"Organize your users": "Uspořádejte své uživatele",
|
||||
"Other": "Jiné",
|
||||
"Output": "",
|
||||
"OUTPUT": "VÝSTUP",
|
||||
"Output format": "Formát výstupu",
|
||||
"Output Format": "Formát výstupu",
|
||||
@@ -1478,6 +1479,7 @@
|
||||
"Remove image": "Odebrat obrázek",
|
||||
"Remove Model": "Odebrat model",
|
||||
"Rename": "Přejmenovat",
|
||||
"Render Markdown in Previews": "",
|
||||
"Reorder Models": "Změnit pořadí modelů",
|
||||
"Reply": "",
|
||||
"Reply in Thread": "Odpovědět ve vlákně",
|
||||
@@ -1640,6 +1642,7 @@
|
||||
"Show": "Zobrazit",
|
||||
"Show \"What's New\" modal on login": "Zobrazit okno \"Co je nového\" při přihlášení",
|
||||
"Show Admin Details in Account Pending Overlay": "Zobrazit podrobnosti administrátora v překryvné vrstvě čekajícího účtu",
|
||||
"Show all ({{COUNT}} characters)": "",
|
||||
"Show Files": "",
|
||||
"Show Formatting Toolbar": "Zobrazit panel nástrojů pro formátování",
|
||||
"Show image preview": "Zobrazit náhled obrázku",
|
||||
@@ -1667,6 +1670,8 @@
|
||||
"Skill updated successfully": "",
|
||||
"Skills": "",
|
||||
"Skills Access": "",
|
||||
"Skills Public Sharing": "",
|
||||
"Skills Sharing": "",
|
||||
"Skip Cache": "Přeskočit mezipaměť",
|
||||
"Skip the cache and re-run the inference. Defaults to False.": "Přeskočit mezipaměť a znovu spustit inferenci. Výchozí hodnota je False.",
|
||||
"Something went wrong :/": "Něco se pokazilo :/",
|
||||
|
||||
@@ -98,7 +98,6 @@
|
||||
"Allow Call": "Tillad kald",
|
||||
"Allow Chat Controls": "Tillad kontrol af chats",
|
||||
"Allow Chat Delete": "Tillad sletning af chats",
|
||||
"Allow Chat Deletion": "Tillad sletning af chats",
|
||||
"Allow Chat Edit": "Tillad redigering af chats",
|
||||
"Allow Chat Export": "Tillad eksport af chats",
|
||||
"Allow Chat Params": "Tillad parametre i chats",
|
||||
@@ -346,6 +345,7 @@
|
||||
"Completions": "Completions",
|
||||
"Compress Images in Channels": "Komprimér billeder i kanaler",
|
||||
"Concurrent Requests": "Concurrent requests",
|
||||
"Config": "",
|
||||
"Config imported successfully": "Konfiguration importeret",
|
||||
"Configure": "Konfigurer",
|
||||
"Confirm": "Bekræft",
|
||||
@@ -769,7 +769,7 @@
|
||||
"Export All Chats (All Users)": "Eksportér alle chats (alle brugere)",
|
||||
"Export chat (.json)": "Eksportér chat (.json)",
|
||||
"Export Chats": "Eksportér chats",
|
||||
"Export Config to JSON File": "Eksportér konfiguration til JSON-fil",
|
||||
"Export Config": "",
|
||||
"Export Models": "Eksportér modeller",
|
||||
"Export Presets": "Eksportér indstillinger",
|
||||
"Export Prompts": "Eksportér prompter",
|
||||
@@ -976,7 +976,7 @@
|
||||
"Images": "",
|
||||
"Import": "Importer",
|
||||
"Import Chats": "Importer chats",
|
||||
"Import Config from JSON File": "Importer konfiguration fra JSON-fil",
|
||||
"Import Config": "",
|
||||
"Import From Link": "Importer fra et link",
|
||||
"Import Models": "Importer modeller",
|
||||
"Import Presets": "Importer Presets",
|
||||
@@ -1342,6 +1342,7 @@
|
||||
"Ordered List": "Nummereret liste",
|
||||
"Organize your users": "Organiser dine brugere",
|
||||
"Other": "Andet",
|
||||
"Output": "",
|
||||
"OUTPUT": "OUTPUT",
|
||||
"Output format": "Outputformat",
|
||||
"Output Format": "Output format",
|
||||
@@ -1478,6 +1479,7 @@
|
||||
"Remove image": "Fjern billede",
|
||||
"Remove Model": "Fjern model",
|
||||
"Rename": "Omdøb",
|
||||
"Render Markdown in Previews": "",
|
||||
"Reorder Models": "Omarranger modeller",
|
||||
"Reply": "Svar",
|
||||
"Reply in Thread": "Svar i tråd",
|
||||
@@ -1638,6 +1640,7 @@
|
||||
"Show": "Vis",
|
||||
"Show \"What's New\" modal on login": "Vis \"Hvad er nyt\" modal ved login",
|
||||
"Show Admin Details in Account Pending Overlay": "Vis administratordetaljer i overlay for ventende konto",
|
||||
"Show all ({{COUNT}} characters)": "",
|
||||
"Show Files": "",
|
||||
"Show Formatting Toolbar": "Vis formateringsværktøjslinjen",
|
||||
"Show image preview": "Vis billedforhåndsvisning",
|
||||
@@ -1665,6 +1668,8 @@
|
||||
"Skill updated successfully": "",
|
||||
"Skills": "",
|
||||
"Skills Access": "",
|
||||
"Skills Public Sharing": "",
|
||||
"Skills Sharing": "",
|
||||
"Skip Cache": "Spring cache over",
|
||||
"Skip the cache and re-run the inference. Defaults to False.": "Spring cachen over og kør inferensen igen. Standard er falsk.",
|
||||
"Something went wrong :/": "Noget gik galt :/",
|
||||
|
||||
@@ -98,7 +98,6 @@
|
||||
"Allow Call": "Anruffunktion erlauben",
|
||||
"Allow Chat Controls": "Chat-Steuerung erlauben",
|
||||
"Allow Chat Delete": "Löschen von Chats erlauben",
|
||||
"Allow Chat Deletion": "Löschen von Chats erlauben",
|
||||
"Allow Chat Edit": "Bearbeiten von Chats erlauben",
|
||||
"Allow Chat Export": "Chat-Export erlauben",
|
||||
"Allow Chat Params": "Chat-Parameter erlauben",
|
||||
@@ -273,7 +272,7 @@
|
||||
"Chat Permissions": "Chat-Berechtigungen",
|
||||
"Chat Tags Auto-Generation": "Automatische Chat-Tag-Generierung",
|
||||
"Chat unshared successfully.": "Chat-Freigabe erfolgreich aufgehoben",
|
||||
"chats": "",
|
||||
"chats": "Chats",
|
||||
"Chats": "Chats",
|
||||
"Check Again": "Erneut prüfen",
|
||||
"Check for updates": "Nach Updates suchen",
|
||||
@@ -346,6 +345,7 @@
|
||||
"Completions": "Vervollständigungen",
|
||||
"Compress Images in Channels": "Bilder in Kanälen komprimieren",
|
||||
"Concurrent Requests": "Gleichzeitige Anfragen",
|
||||
"Config": "",
|
||||
"Config imported successfully": "Konfiguration erfolgreich importiert",
|
||||
"Configure": "Konfigurieren",
|
||||
"Confirm": "Bestätigen",
|
||||
@@ -769,7 +769,7 @@
|
||||
"Export All Chats (All Users)": "Alle Chats exportieren (Alle Benutzer)",
|
||||
"Export chat (.json)": "Chat exportieren (.json)",
|
||||
"Export Chats": "Chats exportieren",
|
||||
"Export Config to JSON File": "Konfiguration als JSON exportieren",
|
||||
"Export Config": "",
|
||||
"Export Models": "Modelle exportieren",
|
||||
"Export Presets": "Vorlagen exportieren",
|
||||
"Export Prompts": "Prompts exportieren",
|
||||
@@ -976,7 +976,7 @@
|
||||
"Images": "Bilder",
|
||||
"Import": "Importieren",
|
||||
"Import Chats": "Chats importieren",
|
||||
"Import Config from JSON File": "Konfiguration aus JSON importieren",
|
||||
"Import Config": "",
|
||||
"Import From Link": "Von Link importieren",
|
||||
"Import Models": "Modelle importieren",
|
||||
"Import Presets": "Vorlagen importieren",
|
||||
@@ -1342,6 +1342,7 @@
|
||||
"Ordered List": "Nummerierte Liste",
|
||||
"Organize your users": "Organisieren Sie Ihre Benutzer",
|
||||
"Other": "Andere",
|
||||
"Output": "",
|
||||
"OUTPUT": "AUSGABE",
|
||||
"Output format": "Ausgabeformat",
|
||||
"Output Format": "Ausgabeformat",
|
||||
@@ -1478,6 +1479,7 @@
|
||||
"Remove image": "Bild entfernen",
|
||||
"Remove Model": "Modell entfernen",
|
||||
"Rename": "Umbenennen",
|
||||
"Render Markdown in Previews": "",
|
||||
"Reorder Models": "Modelle neu anordnen",
|
||||
"Reply": "Antworten",
|
||||
"Reply in Thread": "Im Thread antworten",
|
||||
@@ -1638,6 +1640,7 @@
|
||||
"Show": "Anzeigen",
|
||||
"Show \"What's New\" modal on login": "\"Was gibt's Neues\"-Fenster beim Anmelden anzeigen",
|
||||
"Show Admin Details in Account Pending Overlay": "Admin-Details im 'Konto ausstehend'-Overlay anzeigen",
|
||||
"Show all ({{COUNT}} characters)": "",
|
||||
"Show Files": "Dateien anzeigen",
|
||||
"Show Formatting Toolbar": "Formatierungsleiste anzeigen",
|
||||
"Show image preview": "Bildvorschau anzeigen",
|
||||
@@ -1654,7 +1657,7 @@
|
||||
"Sign up to {{WEBUI_NAME}}": "Bei {{WEBUI_NAME}} registrieren",
|
||||
"Significantly improves accuracy by using an LLM to enhance tables, forms, inline math, and layout detection. Will increase latency. Defaults to False.": "Verbessert die Genauigkeit erheblich, indem ein LLM zur Optimierung von Tabellen, Formularen, mathematischen Formeln und Layout-Erkennung genutzt wird. Erhöht die Latenz. Standardmäßig deaktiviert.",
|
||||
"Signing in to {{WEBUI_NAME}}": "Anmeldung bei {{WEBUI_NAME}}...",
|
||||
"Single": "",
|
||||
"Single": "Einzeln",
|
||||
"Sink List": "Sink-Liste",
|
||||
"sk-1234": "sk-1234",
|
||||
"Skill created successfully": "Skill erfolgreich erstellt",
|
||||
@@ -1664,7 +1667,9 @@
|
||||
"Skill Name": "Skill Name",
|
||||
"Skill updated successfully": "Skill erfolgreich aktualisiert",
|
||||
"Skills": "Skills",
|
||||
"Skills Access": "",
|
||||
"Skills Access": "Skills Zugang",
|
||||
"Skills Public Sharing": "",
|
||||
"Skills Sharing": "",
|
||||
"Skip Cache": "Cache überspringen",
|
||||
"Skip the cache and re-run the inference. Defaults to False.": "Cache überspringen und die Inferenz erneut ausführen. Standardmäßig deaktiviert.",
|
||||
"Something went wrong :/": "Etwas ist schiefgelaufen :/",
|
||||
@@ -1818,7 +1823,7 @@
|
||||
"Toggle whether current connection is active.": "Umschalten, ob die aktuelle Verbindung aktiv ist.",
|
||||
"Token": "Token",
|
||||
"Token counts are estimates and may not reflect actual API usage": "Token-Anzahlen sind Schätzwerte und entsprechen möglicherweise nicht der tatsächlichen API-Nutzung.",
|
||||
"tokens": "",
|
||||
"tokens": "Token",
|
||||
"Tokens": "Token",
|
||||
"Too verbose": "Zu ausführlich",
|
||||
"Tool created successfully": "Werkzeug erfolgreich erstellt",
|
||||
@@ -1892,7 +1897,7 @@
|
||||
"URL is required": "URL ist erforderlich",
|
||||
"URL Mode": "URL-Modus",
|
||||
"Usage": "Nutzung",
|
||||
"Use": "",
|
||||
"Use": "Nutze",
|
||||
"Use '#' in the prompt input to load and include your knowledge.": "Verwenden Sie '#' in der Prompt-Eingabe, um Ihr Wissen zu laden und einzubeziehen.",
|
||||
"Use /v1/chat/completions endpoint instead of /v1/audio/transcriptions for potentially better accuracy.": "Verwenden Sie den Endpunkt /v1/chat/completions statt /v1/audio/transcriptions für potenziell bessere Genauigkeit.",
|
||||
"Use Chat Completions API": "Chat Completions API verwenden",
|
||||
|
||||
@@ -98,7 +98,6 @@
|
||||
"Allow Call": "",
|
||||
"Allow Chat Controls": "",
|
||||
"Allow Chat Delete": "",
|
||||
"Allow Chat Deletion": "Allow Delete Chats",
|
||||
"Allow Chat Edit": "",
|
||||
"Allow Chat Export": "",
|
||||
"Allow Chat Params": "",
|
||||
@@ -346,6 +345,7 @@
|
||||
"Completions": "",
|
||||
"Compress Images in Channels": "",
|
||||
"Concurrent Requests": "",
|
||||
"Config": "",
|
||||
"Config imported successfully": "",
|
||||
"Configure": "",
|
||||
"Confirm": "",
|
||||
@@ -769,7 +769,7 @@
|
||||
"Export All Chats (All Users)": "Export All Chats (All Doggos)",
|
||||
"Export chat (.json)": "",
|
||||
"Export Chats": "Export Barks",
|
||||
"Export Config to JSON File": "",
|
||||
"Export Config": "",
|
||||
"Export Models": "",
|
||||
"Export Presets": "",
|
||||
"Export Prompts": "",
|
||||
@@ -976,7 +976,7 @@
|
||||
"Images": "",
|
||||
"Import": "",
|
||||
"Import Chats": "Import Barks",
|
||||
"Import Config from JSON File": "",
|
||||
"Import Config": "",
|
||||
"Import From Link": "",
|
||||
"Import Models": "",
|
||||
"Import Presets": "",
|
||||
@@ -1342,6 +1342,7 @@
|
||||
"Ordered List": "",
|
||||
"Organize your users": "",
|
||||
"Other": "",
|
||||
"Output": "",
|
||||
"OUTPUT": "",
|
||||
"Output format": "",
|
||||
"Output Format": "",
|
||||
@@ -1478,6 +1479,7 @@
|
||||
"Remove image": "",
|
||||
"Remove Model": "",
|
||||
"Rename": "",
|
||||
"Render Markdown in Previews": "",
|
||||
"Reorder Models": "",
|
||||
"Reply": "",
|
||||
"Reply in Thread": "",
|
||||
@@ -1638,6 +1640,7 @@
|
||||
"Show": "Show much show",
|
||||
"Show \"What's New\" modal on login": "",
|
||||
"Show Admin Details in Account Pending Overlay": "",
|
||||
"Show all ({{COUNT}} characters)": "",
|
||||
"Show Files": "",
|
||||
"Show Formatting Toolbar": "",
|
||||
"Show image preview": "",
|
||||
@@ -1665,6 +1668,8 @@
|
||||
"Skill updated successfully": "",
|
||||
"Skills": "",
|
||||
"Skills Access": "",
|
||||
"Skills Public Sharing": "",
|
||||
"Skills Sharing": "",
|
||||
"Skip Cache": "",
|
||||
"Skip the cache and re-run the inference. Defaults to False.": "",
|
||||
"Something went wrong :/": "",
|
||||
|
||||
@@ -98,7 +98,6 @@
|
||||
"Allow Call": "Επιτρέπεται η Κλήση",
|
||||
"Allow Chat Controls": "Επιτρέπεται ο Έλεγχος Συνομιλίας",
|
||||
"Allow Chat Delete": "Επιτρέπεται η διαγραφή συνομιλίας",
|
||||
"Allow Chat Deletion": "Επιτρέπεται η Διαγραφή Συνομιλίας",
|
||||
"Allow Chat Edit": "Επιτρέπεται η Επεξεργασία Συνομιλίας",
|
||||
"Allow Chat Export": "Επιτρέπεται η Εξαγωγή Συνομιλίας",
|
||||
"Allow Chat Params": "Επιτρέπονται οι Παράμετροι Συνομιλίας",
|
||||
@@ -346,6 +345,7 @@
|
||||
"Completions": "Ολοκληρώσεις",
|
||||
"Compress Images in Channels": "Συμπίεση εικόνων σε κανάλια",
|
||||
"Concurrent Requests": "Ταυτόχρονες Αιτήσεις",
|
||||
"Config": "",
|
||||
"Config imported successfully": "",
|
||||
"Configure": "Διαμόρφωση",
|
||||
"Confirm": "Επιβεβαίωση",
|
||||
@@ -769,7 +769,7 @@
|
||||
"Export All Chats (All Users)": "Εξαγωγή Όλων των Συνομιλιών (Όλοι οι Χρήστες)",
|
||||
"Export chat (.json)": "Εξαγωγή συνομιλίας (.json)",
|
||||
"Export Chats": "Εξαγωγή Συνομιλιών",
|
||||
"Export Config to JSON File": "Εξαγωγή Διαμόρφωσης σε Αρχείο JSON",
|
||||
"Export Config": "",
|
||||
"Export Models": "",
|
||||
"Export Presets": "Εξαγωγή Προκαθορισμένων",
|
||||
"Export Prompts": "",
|
||||
@@ -976,7 +976,7 @@
|
||||
"Images": "",
|
||||
"Import": "Εισαγωγή",
|
||||
"Import Chats": "Εισαγωγή Συνομιλιών",
|
||||
"Import Config from JSON File": "Εισαγωγή Διαμόρφωσης από Αρχείο JSON",
|
||||
"Import Config": "",
|
||||
"Import From Link": "",
|
||||
"Import Models": "",
|
||||
"Import Presets": "Εισαγωγή Προκαθορισμένων",
|
||||
@@ -1342,6 +1342,7 @@
|
||||
"Ordered List": "",
|
||||
"Organize your users": "Οργανώστε τους χρήστες σας",
|
||||
"Other": "Άλλο",
|
||||
"Output": "",
|
||||
"OUTPUT": "ΕΞΟΔΟΣ",
|
||||
"Output format": "Μορφή εξόδου",
|
||||
"Output Format": "Μορφή Εξόδου",
|
||||
@@ -1478,6 +1479,7 @@
|
||||
"Remove image": "Αφαίρεση εικόνας",
|
||||
"Remove Model": "Αφαίρεση Μοντέλου",
|
||||
"Rename": "Μετονομασία",
|
||||
"Render Markdown in Previews": "",
|
||||
"Reorder Models": "Επαναταξινόμηση Μοντέλων",
|
||||
"Reply": "Απάντηση",
|
||||
"Reply in Thread": "Απάντηση στο Νήμα Συζήτησης",
|
||||
@@ -1638,6 +1640,7 @@
|
||||
"Show": "Εμφάνιση",
|
||||
"Show \"What's New\" modal on login": "Εμφάνιση του παράθυρου \"Τι νέο υπάρχει\" κατά την είσοδο",
|
||||
"Show Admin Details in Account Pending Overlay": "Εμφάνιση Λεπτομερειών Διαχειριστή στο Υπέρθεση Εκκρεμής Λογαριασμού",
|
||||
"Show all ({{COUNT}} characters)": "",
|
||||
"Show Files": "",
|
||||
"Show Formatting Toolbar": "",
|
||||
"Show image preview": "",
|
||||
@@ -1665,6 +1668,8 @@
|
||||
"Skill updated successfully": "",
|
||||
"Skills": "",
|
||||
"Skills Access": "",
|
||||
"Skills Public Sharing": "",
|
||||
"Skills Sharing": "",
|
||||
"Skip Cache": "",
|
||||
"Skip the cache and re-run the inference. Defaults to False.": "",
|
||||
"Something went wrong :/": "",
|
||||
|
||||
@@ -98,7 +98,6 @@
|
||||
"Allow Call": "",
|
||||
"Allow Chat Controls": "",
|
||||
"Allow Chat Delete": "",
|
||||
"Allow Chat Deletion": "",
|
||||
"Allow Chat Edit": "",
|
||||
"Allow Chat Export": "",
|
||||
"Allow Chat Params": "",
|
||||
@@ -346,6 +345,7 @@
|
||||
"Completions": "",
|
||||
"Compress Images in Channels": "",
|
||||
"Concurrent Requests": "",
|
||||
"Config": "",
|
||||
"Config imported successfully": "",
|
||||
"Configure": "",
|
||||
"Confirm": "",
|
||||
@@ -769,7 +769,7 @@
|
||||
"Export All Chats (All Users)": "",
|
||||
"Export chat (.json)": "",
|
||||
"Export Chats": "",
|
||||
"Export Config to JSON File": "",
|
||||
"Export Config": "",
|
||||
"Export Models": "",
|
||||
"Export Presets": "",
|
||||
"Export Prompts": "",
|
||||
@@ -976,7 +976,7 @@
|
||||
"Images": "",
|
||||
"Import": "",
|
||||
"Import Chats": "",
|
||||
"Import Config from JSON File": "",
|
||||
"Import Config": "",
|
||||
"Import From Link": "",
|
||||
"Import Models": "",
|
||||
"Import Presets": "",
|
||||
@@ -1342,6 +1342,7 @@
|
||||
"Ordered List": "",
|
||||
"Organize your users": "Organise your users",
|
||||
"Other": "",
|
||||
"Output": "",
|
||||
"OUTPUT": "",
|
||||
"Output format": "",
|
||||
"Output Format": "",
|
||||
@@ -1478,6 +1479,7 @@
|
||||
"Remove image": "",
|
||||
"Remove Model": "",
|
||||
"Rename": "",
|
||||
"Render Markdown in Previews": "",
|
||||
"Reorder Models": "",
|
||||
"Reply": "",
|
||||
"Reply in Thread": "",
|
||||
@@ -1638,6 +1640,7 @@
|
||||
"Show": "",
|
||||
"Show \"What's New\" modal on login": "",
|
||||
"Show Admin Details in Account Pending Overlay": "",
|
||||
"Show all ({{COUNT}} characters)": "",
|
||||
"Show Files": "",
|
||||
"Show Formatting Toolbar": "",
|
||||
"Show image preview": "",
|
||||
@@ -1665,6 +1668,8 @@
|
||||
"Skill updated successfully": "",
|
||||
"Skills": "",
|
||||
"Skills Access": "",
|
||||
"Skills Public Sharing": "",
|
||||
"Skills Sharing": "",
|
||||
"Skip Cache": "",
|
||||
"Skip the cache and re-run the inference. Defaults to False.": "",
|
||||
"Something went wrong :/": "",
|
||||
|
||||
@@ -98,7 +98,6 @@
|
||||
"Allow Call": "",
|
||||
"Allow Chat Controls": "",
|
||||
"Allow Chat Delete": "",
|
||||
"Allow Chat Deletion": "",
|
||||
"Allow Chat Edit": "",
|
||||
"Allow Chat Export": "",
|
||||
"Allow Chat Params": "",
|
||||
@@ -346,6 +345,7 @@
|
||||
"Completions": "",
|
||||
"Compress Images in Channels": "",
|
||||
"Concurrent Requests": "",
|
||||
"Config": "",
|
||||
"Config imported successfully": "",
|
||||
"Configure": "",
|
||||
"Confirm": "",
|
||||
@@ -769,7 +769,7 @@
|
||||
"Export All Chats (All Users)": "",
|
||||
"Export chat (.json)": "",
|
||||
"Export Chats": "",
|
||||
"Export Config to JSON File": "",
|
||||
"Export Config": "",
|
||||
"Export Models": "",
|
||||
"Export Presets": "",
|
||||
"Export Prompts": "",
|
||||
@@ -976,7 +976,7 @@
|
||||
"Images": "",
|
||||
"Import": "",
|
||||
"Import Chats": "",
|
||||
"Import Config from JSON File": "",
|
||||
"Import Config": "",
|
||||
"Import From Link": "",
|
||||
"Import Models": "",
|
||||
"Import Presets": "",
|
||||
@@ -1342,6 +1342,7 @@
|
||||
"Ordered List": "",
|
||||
"Organize your users": "",
|
||||
"Other": "",
|
||||
"Output": "",
|
||||
"OUTPUT": "",
|
||||
"Output format": "",
|
||||
"Output Format": "",
|
||||
@@ -1478,6 +1479,7 @@
|
||||
"Remove image": "",
|
||||
"Remove Model": "",
|
||||
"Rename": "",
|
||||
"Render Markdown in Previews": "",
|
||||
"Reorder Models": "",
|
||||
"Reply": "",
|
||||
"Reply in Thread": "",
|
||||
@@ -1638,6 +1640,7 @@
|
||||
"Show": "",
|
||||
"Show \"What's New\" modal on login": "",
|
||||
"Show Admin Details in Account Pending Overlay": "",
|
||||
"Show all ({{COUNT}} characters)": "",
|
||||
"Show Files": "",
|
||||
"Show Formatting Toolbar": "",
|
||||
"Show image preview": "",
|
||||
@@ -1665,6 +1668,8 @@
|
||||
"Skill updated successfully": "",
|
||||
"Skills": "",
|
||||
"Skills Access": "",
|
||||
"Skills Public Sharing": "",
|
||||
"Skills Sharing": "",
|
||||
"Skip Cache": "",
|
||||
"Skip the cache and re-run the inference. Defaults to False.": "",
|
||||
"Something went wrong :/": "",
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
"Accept Autocomplete Generation\nJump to Prompt Variable": "Aceptar Generar Autocompletado\nIr a la Configuración de este Indicador",
|
||||
"Access": "Acceso",
|
||||
"Access Control": "Control de Acceso",
|
||||
"Access List": "",
|
||||
"Access List": "Lista de Acceso",
|
||||
"Accessible to all users": "Accesible para todos los usuarios",
|
||||
"Account": "Cuenta",
|
||||
"Account Activation Pending": "Activación de cuenta Pendiente",
|
||||
@@ -53,8 +53,8 @@
|
||||
"Add a model ID": "Añadir un ID de modelo",
|
||||
"Add a short description about what this model does": "Añadir una breve descripción sobre lo que hace este modelo",
|
||||
"Add a tag": "Añadir una etiqueta",
|
||||
"Add a tag...": "",
|
||||
"Add Access": "",
|
||||
"Add a tag...": "Añadir una etiqueta...",
|
||||
"Add Access": "Añador Acceso",
|
||||
"Add Arena Model": "Añadir modelo a la Arena",
|
||||
"Add Connection": "Añadir Conexión",
|
||||
"Add Content": "Añadir Contenido",
|
||||
@@ -63,13 +63,13 @@
|
||||
"Add Custom Prompt": "Añadir Indicador Personalizado",
|
||||
"Add Details": "Añadir Detalles",
|
||||
"Add Files": "Añadir Archivos",
|
||||
"Add Image": "",
|
||||
"Add Image": "Añadir Imagen",
|
||||
"Add Member": "Añadir Miembro",
|
||||
"Add Members": "Añadir Miembros",
|
||||
"Add Memory": "Añadir Memoria",
|
||||
"Add Model": "Añadir Modelo",
|
||||
"Add Reaction": "Añadir Reacción",
|
||||
"Add tag": "",
|
||||
"Add tag": "Añadir Etiqueta",
|
||||
"Add Tag": "Añadir etiqueta",
|
||||
"Add text content": "Añade contenido de texto",
|
||||
"Add User": "Añadir Usuario",
|
||||
@@ -94,11 +94,10 @@
|
||||
"All": "Todos",
|
||||
"All chats have been unarchived.": "Todos los chats han sido desarchivados",
|
||||
"All models deleted successfully": "Todos los modelos borrados correctamente",
|
||||
"All Users": "",
|
||||
"All Users": "Todos los Usuarios",
|
||||
"Allow Call": "Permitir Llamada",
|
||||
"Allow Chat Controls": "Permitir Controles del Chat",
|
||||
"Allow Chat Delete": "Permitir Borrar Chat",
|
||||
"Allow Chat Deletion": "Permitir Borrado de Chat",
|
||||
"Allow Chat Edit": "Permitir Editar Chat",
|
||||
"Allow Chat Export": "Permitir Exportar Chat",
|
||||
"Allow Chat Params": "Permitir Parametros en Chat",
|
||||
@@ -145,7 +144,7 @@
|
||||
"API Keys": "Claves AI",
|
||||
"API Mode": "Modo de API",
|
||||
"API Timeout": "API Timeout",
|
||||
"API Type": "",
|
||||
"API Type": "Tipo de API",
|
||||
"API Version": "Versión API",
|
||||
"API Version is required": "La Versión API es requerida",
|
||||
"Application DN": "Aplicacion DN",
|
||||
@@ -153,18 +152,18 @@
|
||||
"applies to all users with the \"user\" role": "se aplica a todos los usuarios con el rol \"user\" ",
|
||||
"April": "Abril",
|
||||
"Archive": "Archivar",
|
||||
"Archive All": "",
|
||||
"Archive All": "Arcbhivar Todos",
|
||||
"Archive All Chats": "Archivar Todos los Chats",
|
||||
"Archived Chats": "Chats archivados",
|
||||
"archived-chat-export": "exportar chats archivados",
|
||||
"Are you sure you want to archive all chats? This action cannot be undone.": "",
|
||||
"Are you sure you want to clear all memories? This action cannot be undone.": "¿Segur@ de que quieres borrar todas las memorias? (¡esta acción NO se puede deshacer!)",
|
||||
"Are you sure you want to delete \"{{NAME}}\"?": "¿Segur@ de que quieres eliminar \"{{NAME}}\"?",
|
||||
"Are you sure you want to delete all chats? This action cannot be undone.": "",
|
||||
"Are you sure you want to delete this channel?": "¿Segur@ de que quieres eliminar este canal?",
|
||||
"Are you sure you want to delete this message?": "¿Segur@ de que quieres eliminar este mensaje? ",
|
||||
"Are you sure you want to delete this version? Child versions will be relinked to this version's parent.": "",
|
||||
"Are you sure you want to unarchive all archived chats?": "¿Segur@ de que quieres desarchivar todos los chats archivados?",
|
||||
"Are you sure you want to archive all chats? This action cannot be undone.": "¿Seguro que quieres archivar todos los chats? Esta acción no se puede deshacer.",
|
||||
"Are you sure you want to clear all memories? This action cannot be undone.": "¿Seguro de que quieres borrar todas las memorias? (¡esta acción NO se puede deshacer!)",
|
||||
"Are you sure you want to delete \"{{NAME}}\"?": "¿Seguro de que quieres eliminar \"{{NAME}}\"?",
|
||||
"Are you sure you want to delete all chats? This action cannot be undone.": "¿Seguro que quieres borrar todos los chats? Esta acción no se puede deshacer.",
|
||||
"Are you sure you want to delete this channel?": "¿Seguro de que quieres eliminar este canal?",
|
||||
"Are you sure you want to delete this message?": "¿Seguro de que quieres eliminar este mensaje? ",
|
||||
"Are you sure you want to delete this version? Child versions will be relinked to this version's parent.": "¿Seguro que desea eliminar esta versión? Las versiones secundarias se vincularán a la versión principal de esta.",
|
||||
"Are you sure you want to unarchive all archived chats?": "¿Seguro de que quieres desarchivar todos los chats archivados?",
|
||||
"Arena Models": "Arena de Modelos",
|
||||
"Artifacts": "Artefactos",
|
||||
"Asc": "Asc",
|
||||
@@ -224,7 +223,7 @@
|
||||
"Boosting or penalizing specific tokens for constrained responses. Bias values will be clamped between -100 and 100 (inclusive). (Default: none)": "Impulsando o penalizando tokens específicos para respuestas restringidas. Los valores de sesgo se limitarán entre -100 y 100 (inclusive). (Por defecto: ninguno)",
|
||||
"Brave": "Brave",
|
||||
"Brave Search API Key": "Clave API de Brave Search",
|
||||
"Browse and query knowledge bases": "",
|
||||
"Browse and query knowledge bases": "Explorar y consultar bases de conocimiento",
|
||||
"Builtin Tools": "Herramientas Integradas",
|
||||
"Bullet List": "Lista de Viñetas",
|
||||
"Button ID": "ID del Botón",
|
||||
@@ -241,7 +240,7 @@
|
||||
"Camera": "Cámara",
|
||||
"Cancel": "Cancelar",
|
||||
"Cannot create an empty note.": "No se puede crear una nota vacía.",
|
||||
"Cannot delete the production version": "",
|
||||
"Cannot delete the production version": "No se puede borrar la versión de producción",
|
||||
"Capabilities": "Capacidades",
|
||||
"Capture": "Captura",
|
||||
"Capture Audio": "Capturar Audio",
|
||||
@@ -261,19 +260,19 @@
|
||||
"Chat": "Chat",
|
||||
"Chat Background Image": "Imágen de Fondo del Chat",
|
||||
"Chat Bubble UI": "Interfaz del Chat en Burbuja",
|
||||
"Chat Completions": "",
|
||||
"Chat Completions": "Completación del Chat",
|
||||
"Chat Controls": "Controles del Chat",
|
||||
"Chat Conversation": "Conversación del Chat",
|
||||
"Chat direction": "Dirección del Chat",
|
||||
"Chat exported successfully": "",
|
||||
"Chat History": "",
|
||||
"Chat exported successfully": "Chat exportado correctmente",
|
||||
"Chat History": "Historial del Chat",
|
||||
"Chat ID": "ID del Chat",
|
||||
"Chat moved successfully": "Chat movido correctamente",
|
||||
"Chat Overview": "Vista General del Chat",
|
||||
"Chat Permissions": "Permisos del Chat",
|
||||
"Chat Tags Auto-Generation": "AutoGeneración de Etiquetas de Chat",
|
||||
"Chat unshared successfully.": "",
|
||||
"chats": "",
|
||||
"Chat unshared successfully.": "Chat descompartido correctamente",
|
||||
"chats": "chats",
|
||||
"Chats": "Chats",
|
||||
"Check Again": "Verifica de nuevo",
|
||||
"Check for updates": "Buscar actualizaciones",
|
||||
@@ -302,7 +301,7 @@
|
||||
"Click here to upload a workflow.json file.": "Pulsa aquí para subir un archivo workflow.json",
|
||||
"click here.": "Pulsa aquí.",
|
||||
"Click on the user role button to change a user's role.": "Pulsa en el botón rol de usuario para cambiar su rol.",
|
||||
"Click to copy ID": "",
|
||||
"Click to copy ID": "Pulsa para copiar ID",
|
||||
"Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "Permisos de escritura del portapapeles denegado. Por favor, comprueba la configuración de tu navegador para otorgar el permiso necesario.",
|
||||
"Clone": "Clonar",
|
||||
"Clone Chat": "Clonar Chat",
|
||||
@@ -338,14 +337,15 @@
|
||||
"ComfyUI Workflow": "Flujo de Trabajo de ComfyUI",
|
||||
"ComfyUI Workflow Nodes": "Nodos del Flujo de Trabajo de ComfyUI",
|
||||
"Comma separated Node Ids (e.g. 1 or 1,2)": "IDs de Nodo separados por comas (ej. 1 o 1,2)",
|
||||
"command": "",
|
||||
"command": "comando",
|
||||
"Command": "Comando",
|
||||
"Comment": "Comentario",
|
||||
"Commit Message": "",
|
||||
"Community Reviews": "",
|
||||
"Commit Message": "Corregir Mensaje",
|
||||
"Community Reviews": "Revisiones de la Comunidad",
|
||||
"Completions": "Cumplimientos",
|
||||
"Compress Images in Channels": "Comprimir Imágenes en Canales",
|
||||
"Concurrent Requests": "Número de Solicitudes Concurrentes",
|
||||
"Config": "",
|
||||
"Config imported successfully": "Configuración importada correctamente",
|
||||
"Configure": "Configurar",
|
||||
"Confirm": "Confirmar",
|
||||
@@ -385,8 +385,8 @@
|
||||
"Copy Last Response": "Copiar la última Respuesta",
|
||||
"Copy link": "Copiar Enlace",
|
||||
"Copy Link": "Copiar enlace",
|
||||
"Copy Prompt": "",
|
||||
"Copy Share Link": "",
|
||||
"Copy Prompt": "Copiar Indicador",
|
||||
"Copy Share Link": "Copiar Enlace Compartido",
|
||||
"Copy to clipboard": "Copia a portapapeles",
|
||||
"Copy URL": "Copiar URL",
|
||||
"Copying to clipboard was successful!": "¡La copia al portapapeles se ha realizado correctamente!",
|
||||
@@ -453,13 +453,13 @@
|
||||
"Default User Role": "Rol predeterminado de los nuevos usuarios",
|
||||
"Delete": "Borrar",
|
||||
"Delete a model": "Borrar un modelo",
|
||||
"Delete All": "",
|
||||
"Delete All": "Borrar Todo",
|
||||
"Delete All Chats": "Borrar todos los chats",
|
||||
"Delete all contents inside this folder": "Borrar todo el contenido de esta carpeta",
|
||||
"Delete All Models": "Borrar todos los modelos",
|
||||
"Delete Chat": "Borrar Chat",
|
||||
"Delete chat?": "¿Borrar el chat?",
|
||||
"Delete File": "",
|
||||
"Delete File": "Borrar Fichero",
|
||||
"Delete folder?": "¿Borrar carpeta?",
|
||||
"Delete function?": "Borrar la función?",
|
||||
"Delete Message": "Borrar mensaje",
|
||||
@@ -467,20 +467,20 @@
|
||||
"Delete Model": "Borrar Modelo",
|
||||
"Delete note?": "¿Borrar nota?",
|
||||
"Delete prompt?": "¿Borrar el indicador?",
|
||||
"Delete skill?": "",
|
||||
"Delete skill?": "¿Borrar habilidades?",
|
||||
"delete this link": "Borrar este enlace",
|
||||
"Delete tool?": "¿Borrar la herramienta?",
|
||||
"Delete User": "Borrar Usuario",
|
||||
"Delete Version": "",
|
||||
"Delete Version": "Borrar Versión",
|
||||
"Deleted": "Borrado",
|
||||
"Deleted {{deleteModelTag}}": "{{deleteModelTag}} Borrado",
|
||||
"Deleted {{name}}": "{{nombre}} Borrado",
|
||||
"Deleted User": "Usuario Borrado",
|
||||
"Deployment names are required for Azure OpenAI": "Azure OpenAI requiere nombres de implementación",
|
||||
"Desc": "Desc",
|
||||
"Describe the edit...": "",
|
||||
"Describe the image...": "",
|
||||
"Describe what changed...": "",
|
||||
"Describe the edit...": "Describe la edición...",
|
||||
"Describe the image...": "Describe la Imagen...",
|
||||
"Describe what changed...": "Describe lo cambiado...",
|
||||
"Describe your knowledge base and objectives": "Describe tu Base de Conocimientos y sus objetivos",
|
||||
"Description": "Descripción",
|
||||
"Detect Artifacts Automatically": "Detectar Artefactos Automáticamente",
|
||||
@@ -492,7 +492,7 @@
|
||||
"Direct Message": "Mensaje Directo",
|
||||
"Direct Tool Servers": "Servidores de Herramientas Directos",
|
||||
"Directory selection was cancelled": "La selección de directorio ha sido cancelada",
|
||||
"Disable All": "",
|
||||
"Disable All": "Deshabilitar Todo",
|
||||
"Disable Code Interpreter": "Deshabilitar Interprete de Código",
|
||||
"Disable Image Extraction": "Deshabilitar Extracción de Imágenes",
|
||||
"Disable image extraction from the PDF. If Use LLM is enabled, images will be automatically captioned. Defaults to False.": "Desabilita la extracción de imágenes del pdf. Si está habilitado Usar LLM las imágenes se capturan automáticamente. Por defecto el valor es Falso (las imágenes se extraen).",
|
||||
@@ -552,15 +552,15 @@
|
||||
"e.g. A filter to remove profanity from text": "p.ej. Un filtro para eliminar malas palabras del texto",
|
||||
"e.g. about the Roman Empire": "p.ej. sobre el imperio romano",
|
||||
"e.g. alloy, echo, shimmer": "p.ej. aleación, eco, brillo",
|
||||
"e.g. Code Review Guidelines": "",
|
||||
"e.g. code-review-guidelines": "",
|
||||
"e.g. Code Review Guidelines": "p.ej. Directrices de Revisión de Código",
|
||||
"e.g. code-review-guidelines": "p.ej. directrices-revisión-código",
|
||||
"e.g. en": "p.ej. es",
|
||||
"e.g. My Filter": "p.ej. Mi Filtro",
|
||||
"e.g. My Tools": "p.ej. Mis Herramientas",
|
||||
"e.g. my_filter": "p.ej. mi_filtro",
|
||||
"e.g. my_tools": "p.ej. mis_herramientas",
|
||||
"e.g. pdf, docx, txt": "p.ej. pdf, docx, txt ...",
|
||||
"e.g. Step-by-step instructions for code reviews": "",
|
||||
"e.g. Step-by-step instructions for code reviews": "p.ej. Unstruciones paso a paso para revisión de código",
|
||||
"e.g. Tell me a fun fact": "p.ej. Dime algo divertido",
|
||||
"e.g. Tell me a fun fact about the Roman Empire": "p.ej. Dime algo divertido sobre el imperio romano",
|
||||
"e.g. Tools for performing various operations": "p.ej. Herramientas para realizar diversas operaciones",
|
||||
@@ -577,7 +577,7 @@
|
||||
"Edit Image": "Editar Imagen",
|
||||
"Edit Last Message": "Editar Último Mensaje",
|
||||
"Edit Memory": "Editar Memoria",
|
||||
"Edit Prompt": "",
|
||||
"Edit Prompt": "Editar Indicador",
|
||||
"Edit User": "Editar Usuario",
|
||||
"Edit User Group": "Editar Grupo de Usuarios",
|
||||
"Edit workflow.json content": "Editar el contenido de workflow.json",
|
||||
@@ -592,15 +592,15 @@
|
||||
"Embedding Batch Size": "Tamaño del Lote de Incrustación",
|
||||
"Embedding Model": "Modelo de Incrustación",
|
||||
"Embedding Model Engine": "Motor del Modelo de Incrustación",
|
||||
"Enable All": "",
|
||||
"Enable API Keys": "Activar Claves API",
|
||||
"Enable All": "Habilitar Todo",
|
||||
"Enable API Keys": "Habilitar Claves API",
|
||||
"Enable autocomplete generation for chat messages": "Habilitar generación de autocompletado para mensajes de chat",
|
||||
"Enable Code Execution": "Habilitar Ejecución de Código",
|
||||
"Enable Code Interpreter": "Habilitar Interprete de Código",
|
||||
"Enable Community Sharing": "Habilitar Compartir con la Comunidad",
|
||||
"Enable Memory Locking (mlock) to prevent model data from being swapped out of RAM. This option locks the model's working set of pages into RAM, ensuring that they will not be swapped out to disk. This can help maintain performance by avoiding page faults and ensuring fast data access.": "Habilitar bloqueo de memoria (mlock) para prevenir que los datos del modelo se intercambien fuera de la RAM. Esta opción bloquea el conjunto de páginas de trabajo del modelo en RAM, asegurando que no se intercambiarán fuera a disco. Esto puede ayudar a mantener el rendimiento evitando fallos de página y asegurando un acceso rápido a los datos.",
|
||||
"Enable Memory Mapping (mmap) to load model data. This option allows the system to use disk storage as an extension of RAM by treating disk files as if they were in RAM. This can improve model performance by allowing for faster data access. However, it may not work correctly with all systems and can consume a significant amount of disk space.": "Habilitar Mapeado de Memoria (mmap) para cargar datos del modelo. Esta opción permite al sistema usar el almacenamiento del disco como una extensión de la RAM al tratar los archivos en disco como si estuvieran en la RAM. Esto puede mejorar el rendimiento del modelo al permitir un acceso más rápido a los datos. Sin embargo, puede no funcionar correctamente con todos los sistemas y puede consumir una cantidad significativa de espacio en disco.",
|
||||
"Enable Message Queue": "",
|
||||
"Enable Message Queue": "Habilitar Cola de Mensajes",
|
||||
"Enable Message Rating": "Habilitar Calificación de los Mensajes",
|
||||
"Enable Mirostat sampling for controlling perplexity.": "Algoritmo de decodificación de texto neuronal que controla activamente el proceso generativo para mantener la perplejidad del texto generado en un valor deseado. Previene las trampas de aburrimiento (por excesivas repeticiones) y de incoherencia (por generación de excesivo texto).",
|
||||
"Enable New Sign Ups": "Habilitar Registros de Nuevos Usuarios",
|
||||
@@ -699,7 +699,7 @@
|
||||
"Enter server host": "Ingresar host del servidor",
|
||||
"Enter server label": "Ingresar etiqueta del servidor",
|
||||
"Enter server port": "Ingresar puerto del servidor",
|
||||
"Enter skill instructions in markdown...": "",
|
||||
"Enter skill instructions in markdown...": "Ingresar instrucciones de la habilidad en markdown...",
|
||||
"Enter Sougou Search API sID": "Ingresar Sougou Search API sID",
|
||||
"Enter Sougou Search API SK": "Ingresar Sougou Search API SK",
|
||||
"Enter stop sequence": "Ingresar secuencia de parada",
|
||||
@@ -722,8 +722,8 @@
|
||||
"Enter Yacy Password": "Ingresar Contraseña de Yacy",
|
||||
"Enter Yacy URL (e.g. http://yacy.example.com:8090)": "Ingresar URL de Yacy (p.ej. http://yacy.ejemplo.com:8090)",
|
||||
"Enter Yacy Username": "Ingresar Nombre de Usuario de Yacy",
|
||||
"Enter Yandex Web Search API Key": "",
|
||||
"Enter Yandex Web Search URL": "",
|
||||
"Enter Yandex Web Search API Key": "Ingresar API de la Busqueda Web de Yandex",
|
||||
"Enter Yandex Web Search URL": "Ingresar URL de la Busqueda Wev de Yandex",
|
||||
"Enter your code here...": "Introduce tu código aquí...",
|
||||
"Enter your current password": "Ingresa tu contraseña actual",
|
||||
"Enter Your Email": "Ingresa tu correo electrónico",
|
||||
@@ -757,7 +757,7 @@
|
||||
"Example: sAMAccountName or uid or userPrincipalName": "Ejemplo: sAMNombreCuenta o uid o userNombrePrincipal",
|
||||
"Exceeded the number of seats in your license. Please contact support to increase the number of seats.": "Excedido el número de accesos de usuarios en tu licencia. Por favor, contacta con soporte para aumentar el número de accesos.",
|
||||
"Exclude": "Excluir",
|
||||
"Execute code": "",
|
||||
"Execute code": "Ejecuar código",
|
||||
"Execute code for analysis": "Ejecutar código para análisis",
|
||||
"Executing **{{NAME}}**...": "Ejecutando **{{NAME}}**...",
|
||||
"Expand": "Expandir",
|
||||
@@ -769,7 +769,7 @@
|
||||
"Export All Chats (All Users)": "Exportar Todos los Chats (Todos los Usuarios)",
|
||||
"Export chat (.json)": "Exportar chat (.json)",
|
||||
"Export Chats": "Exportar Chats",
|
||||
"Export Config to JSON File": "Exportar Configuración a archivo JSON",
|
||||
"Export Config": "",
|
||||
"Export Models": "Exportar Modelos",
|
||||
"Export Presets": "Exportar Preajustes",
|
||||
"Export Prompts": "Exportar Indicadores",
|
||||
@@ -792,7 +792,7 @@
|
||||
"Failed to copy link": "Fallo al copiar enlace",
|
||||
"Failed to create API Key.": "Fallo al crear la Clave API.",
|
||||
"Failed to delete note": "Fallo al eliminar nota",
|
||||
"Failed to download image": "",
|
||||
"Failed to download image": "Fallo al descargar imagen",
|
||||
"Failed to extract content from the file: {{error}}": "Fallo al extraer el contenido del archivo: {{error}}",
|
||||
"Failed to extract content from the file.": "Fallo al extraer el contenido del archivo.",
|
||||
"Failed to fetch models": "Fallo al obtener los modelos",
|
||||
@@ -810,7 +810,7 @@
|
||||
"Failed to save connections": "Fallo al guardar las conexiones",
|
||||
"Failed to save conversation": "Fallo al guardar la conversación",
|
||||
"Failed to save models configuration": "Fallo al guardar la configuración de los modelos",
|
||||
"Failed to unshare chat.": "",
|
||||
"Failed to unshare chat.": "Fallo al descompartir chat.",
|
||||
"Failed to update settings": "Fallo al actualizar los ajustes",
|
||||
"Failed to update status": "Fallo al actualizar el estado",
|
||||
"Failed to upload file.": "Fallo al subir el archivo.",
|
||||
@@ -818,7 +818,7 @@
|
||||
"Features Permissions": "Permisos de las Características",
|
||||
"February": "Febrero",
|
||||
"Feedback": "Opinión",
|
||||
"Feedback Activity": "",
|
||||
"Feedback Activity": "Actividad de las Opiniones",
|
||||
"Feedback deleted successfully": "Opinión eliminada correctamente",
|
||||
"Feedback Details": "Detalle de la Opinión",
|
||||
"Feedback History": "Historia de la Opiniones",
|
||||
@@ -828,7 +828,7 @@
|
||||
"File added successfully.": "Archivo añadido correctamente.",
|
||||
"File content updated successfully.": "Contenido del archivo actualizado correctamente.",
|
||||
"File Context": "Contexto del Archivo",
|
||||
"File deleted successfully.": "",
|
||||
"File deleted successfully.": "Archivo borrado correctamente.",
|
||||
"File Mode": "Modo de Archivo",
|
||||
"File not found.": "Archivo no encontrado.",
|
||||
"File removed successfully.": "Archivo eliminado correctamente.",
|
||||
@@ -836,7 +836,7 @@
|
||||
"File Upload": "Subir Archivo",
|
||||
"File uploaded successfully": "Archivo subido correctamente",
|
||||
"File uploaded!": "¡Archivo subido!",
|
||||
"Filename": "",
|
||||
"Filename": "Nombre del Archivo",
|
||||
"Files": "Archivos",
|
||||
"Filter": "Filtro",
|
||||
"Filter is now globally disabled": "El filtro ahora está desactivado globalmente",
|
||||
@@ -862,7 +862,7 @@
|
||||
"Follow Up Generation Prompt": "Seguimiento de la Generación del Indicador",
|
||||
"Follow-Up Auto-Generation": "Seguimiento Auto-Generación",
|
||||
"Followed instructions perfectly": "Siguió las instrucciones perfectamente",
|
||||
"for placeholders": "",
|
||||
"for placeholders": "para marcadores de posición",
|
||||
"Force OCR": "Forzar OCR",
|
||||
"Force OCR on all pages of the PDF. This can lead to worse results if you have good text in your PDFs. Defaults to False.": "Forzar OCR en todas las páginas del PDF. Puede empeorar el resultado en PDFs que ya tengan una buena capa de texto. El valor predeterminado es desactivado (no forzar)",
|
||||
"Forge new paths": "Forjar nuevos caminos",
|
||||
@@ -897,13 +897,13 @@
|
||||
"General": "General",
|
||||
"Generate": "Generar",
|
||||
"Generate an image": "Generar una imagen",
|
||||
"Generate and edit images": "",
|
||||
"Generate and edit images": "Generar y editar imágenes",
|
||||
"Generate Message Pair": "Generar Par de Mensajes",
|
||||
"Generated Image": "Imagen Generada",
|
||||
"Generated images will appear here": "",
|
||||
"Generated images will appear here": "Las imágenes generdas aparecerán aquí",
|
||||
"Generating search query": "Generando consulta de búsqueda",
|
||||
"Generating...": "Generando",
|
||||
"Get current time and perform date/time calculations": "",
|
||||
"Get current time and perform date/time calculations": "Obtener la hora actual y realizar cálculos de fecha y hora",
|
||||
"Get information on {{name}} in the UI": "Obtener información sobre {{name}} en la IU",
|
||||
"Get started": "Empezar",
|
||||
"Get started with {{WEBUI_NAME}}": "Empezar con {{WEBUI_NAME}}",
|
||||
@@ -934,16 +934,16 @@
|
||||
"Height": "Altura",
|
||||
"Hello, {{name}}": "Hola, {{name}}",
|
||||
"Help": "Ayuda",
|
||||
"Help the community discover great models": "",
|
||||
"Help the community discover great models": "Ayuda a la comunidad a descubrir grandes modelos",
|
||||
"Hex Color": "Color Hex",
|
||||
"Hex Color - Leave empty for default color": "Color Hex - Deja vacío para el color predeterminado",
|
||||
"Hidden": "",
|
||||
"Hidden": "Oculto",
|
||||
"Hide": "Esconder",
|
||||
"Hide from Sidebar": "Ocultar Panel Lateral",
|
||||
"Hide Model": "Ocultar Modelo",
|
||||
"High": "Alto",
|
||||
"High Contrast Mode": "Modo Alto Contraste",
|
||||
"History": "",
|
||||
"History": "Historial",
|
||||
"Home": "Inicio",
|
||||
"Host": "Host",
|
||||
"How can I help you today?": "¿Cómo puedo ayudarte hoy?",
|
||||
@@ -955,7 +955,7 @@
|
||||
"I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.": "Aseguro que he leído y entiendo las implicaciones de mi acción. Soy consciente de los riesgos asociados con la ejecución de código arbitrario y he verificado la confiabilidad de la fuente.",
|
||||
"ID": "ID",
|
||||
"ID cannot contain \":\" or \"|\" characters": "ID no puede contener los caracteres \":\" o \"|\"",
|
||||
"ID copied to clipboard": "",
|
||||
"ID copied to clipboard": "ID copiado al portapapeles",
|
||||
"iframe Sandbox Allow Forms": "iframe Sandbox Allow Forms",
|
||||
"iframe Sandbox Allow Same Origin": "iframe Sandbox Allow Same Origin",
|
||||
"Ignite curiosity": "Encender la curiosidad",
|
||||
@@ -976,7 +976,7 @@
|
||||
"Images": "Imágenes",
|
||||
"Import": "Importar",
|
||||
"Import Chats": "Importar Chats",
|
||||
"Import Config from JSON File": "Importar Config desde Archivo JSON",
|
||||
"Import Config": "",
|
||||
"Import From Link": "Importar desde Enlace",
|
||||
"Import Models": "Importar Modelos",
|
||||
"Import Presets": "Importar Preajustes",
|
||||
@@ -984,7 +984,7 @@
|
||||
"Import successful": "Importación realizada correctamente",
|
||||
"Import Tools": "Importar Herramientas",
|
||||
"Important Update": "Actualización importante",
|
||||
"Inactive": "",
|
||||
"Inactive": "Inactivo",
|
||||
"Include": "Incluir",
|
||||
"Include `--api-auth` flag when running stable-diffusion-webui": "Incluir el señalizador `--api-auth` al ejecutar stable-diffusion-webui",
|
||||
"Include `--api` flag when running stable-diffusion-webui": "Incluir el señalizador `--api` al ejecutar stable-diffusion-webui",
|
||||
@@ -1066,15 +1066,15 @@
|
||||
"Learn More": "Saber Más",
|
||||
"Learn more about OpenAPI tool servers.": "Saber más sobre los servidores de herramientas OpenAPI",
|
||||
"Learn more about Voxtral transcription.": "Saber más sobre la transcripción con Voxtral",
|
||||
"Leave a public review for {{modelName}}": "",
|
||||
"Leave empty for no compression": "Lejar vacío para no compresión",
|
||||
"Leave a public review for {{modelName}}": "Dejar revisión pública de {{modelName}}",
|
||||
"Leave empty for no compression": "Dejar vacío para no compresión",
|
||||
"Leave empty for unlimited": "Dejar vacío para ilimitado",
|
||||
"Leave empty to include all models from \"{{url}}\" endpoint": "Dejar vacío para incluir todos los modelos desde el endpoint \"{{url}}\"",
|
||||
"Leave empty to include all models from \"{{url}}/api/tags\" endpoint": "Dejar vacío para incluir todos los modelos desde el endpoint \"{{url}}/api/tags\"",
|
||||
"Leave empty to include all models from \"{{url}}/models\" endpoint": "Dejar vacío para incluir todos los modelos desde el endpoint \"{{url}}/models\"",
|
||||
"Leave empty to include all models or select specific models": "Dejar vacío para incluir todos los modelos o Seleccionar modelos específicos",
|
||||
"Leave empty to use first admin user": "Dejar vacío para utilizar el primer usuario administrador",
|
||||
"Leave empty to use the default config, or enter a valid json (see https://yandex.cloud/en/docs/search-api/api-ref/WebSearch/search#yandex.cloud.searchapi.v2.WebSearchRequest)": "",
|
||||
"Leave empty to use the default config, or enter a valid json (see https://yandex.cloud/en/docs/search-api/api-ref/WebSearch/search#yandex.cloud.searchapi.v2.WebSearchRequest)": "Déjelo vacío para usar la configuración predeterminada o ingresa un json válido (consulta https://yandex.cloud/en/docs/search-api/api-ref/WebSearch/search#yandex.cloud.searchapi.v2.WebSearchRequest)",
|
||||
"Leave empty to use the default model (voxtral-mini-latest).": "Dejar vacío para usar el modo predeterminado (voxtral-mini-latest).",
|
||||
"Leave empty to use the default prompt, or enter a custom prompt": "Dejar vacío para usar el indicador predeterminado, o Ingresar un indicador personalizado",
|
||||
"Leave model field empty to use the default model.": "Dejar vacío el campo modelo para usar el modelo predeterminado.",
|
||||
@@ -1086,7 +1086,7 @@
|
||||
"Limit concurrent search queries. 0 = unlimited (default). Set to 1 for sequential execution (recommended for APIs with strict rate limits like Brave free tier).": "Limitar consultas de búsqueda simultáneas. 0 = ilimitado (predeterminado). Establécerlo en 1 para ejecución secuencial (recomendado para API con límites de velocidad estrictos, como la versión gratuita de Brave).",
|
||||
"List": "Lista",
|
||||
"Listening...": "Escuchando...",
|
||||
"Live": "",
|
||||
"Live": "En Directo",
|
||||
"Llama.cpp": "Llama.cpp",
|
||||
"LLMs can make mistakes. Verify important information.": "Los LLMs pueden cometer errores. Verifica la información importante.",
|
||||
"Loader": "Cargador",
|
||||
@@ -1105,7 +1105,7 @@
|
||||
"Male": "Hombre",
|
||||
"Manage": "Gestionar",
|
||||
"Manage Direct Connections": "Gestionar Conexiones Directas",
|
||||
"Manage Files": "",
|
||||
"Manage Files": "Gestionar Archivos",
|
||||
"Manage Models": "Gestionar Modelos",
|
||||
"Manage Ollama": "Gestionar Ollama",
|
||||
"Manage Ollama API Connections": "Gestionar Conexiones API de Ollama",
|
||||
@@ -1128,7 +1128,7 @@
|
||||
"MCP support is experimental and its specification changes often, which can lead to incompatibilities. OpenAPI specification support is directly maintained by the Open WebUI team, making it the more reliable option for compatibility.": "El soporte de MCP es experimental y su especificación cambia con frecuencia, lo que puede generar incompatibilidades. El equipo de Open WebUI mantiene directamente la compatibilidad con la especificación OpenAPI, lo que la convierte en la opción más fiable para la compatibilidad.",
|
||||
"Medium": "Medio",
|
||||
"Member removed successfully": "Miembro removido correctamente",
|
||||
"members": "",
|
||||
"members": "miembros",
|
||||
"Members": "Miembros",
|
||||
"Members added successfully": "Miembros añadidos correctamente",
|
||||
"Memories": "Memorias",
|
||||
@@ -1142,10 +1142,10 @@
|
||||
"Merged Response": "Respuesta combinada",
|
||||
"Message": "Mensaje",
|
||||
"Message counts and response timestamps": "Recuento de mensajes y marcas de tiempo de la respuesta",
|
||||
"Message counts are based on assistant responses.": "",
|
||||
"Message counts are based on assistant responses.": "El recuento de mensajes está basado en las respuestas del asistente",
|
||||
"Message rating should be enabled to use this feature": "Para usar esta función debe estar habilitada la calificación de mensajes",
|
||||
"messages": "",
|
||||
"Messages": "",
|
||||
"messages": "mensajes",
|
||||
"Messages": "Mensajes",
|
||||
"Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "Los mensajes que envíe después de la creación del enlace no se compartirán. Los usuarios con la URL del enlace podrán ver el chat compartido.",
|
||||
"Microsoft OneDrive": "Microsoft OneDrive",
|
||||
"Microsoft OneDrive (personal)": "Microsoft OneDrive (personal)",
|
||||
@@ -1178,14 +1178,14 @@
|
||||
"Model name already exists, please choose a different one": "Ese nombre de modelo ya existe, por favor elige otro diferente",
|
||||
"Model Name is required.": "El Nombre de Modelo es requerido",
|
||||
"Model names and usage frequency": "Nombres de modelos y frecuencia de uso",
|
||||
"Model not found": "",
|
||||
"Model not found": "Modelo no encontrado",
|
||||
"Model not selected": "Modelo no seleccionado",
|
||||
"Model Params": "Paráms Modelo",
|
||||
"Model Permissions": "Permisos Modelo",
|
||||
"Model responses or outputs": "Respuestas del modelo o salidas",
|
||||
"Model unloaded successfully": "Modelo descargado correctamente",
|
||||
"Model updated successfully": "Modelo actualizado correctamente",
|
||||
"Model Usage": "",
|
||||
"Model Usage": "Uso del Modelo",
|
||||
"Model(s) do not support file upload": "Modelo/s no soportan subida de archivos",
|
||||
"Modelfile Content": "Contenido del Modelfile",
|
||||
"Models": "Modelos",
|
||||
@@ -1198,7 +1198,7 @@
|
||||
"Mojeek Search API Key": "Clave API de Mojeek Search",
|
||||
"More": "Más",
|
||||
"More Concise": "Más Conciso",
|
||||
"More options": "",
|
||||
"More options": "Más Opciones",
|
||||
"More Options": "Más Opciones",
|
||||
"Move": "Mover",
|
||||
"Name": "Nombre",
|
||||
@@ -1215,13 +1215,13 @@
|
||||
"New Note": "Nueva Nota",
|
||||
"New Password": "Nueva Contraseña",
|
||||
"New Prompt": "Nuevo Indicador",
|
||||
"New Skill": "",
|
||||
"New Skill": "Nueva Habilidad",
|
||||
"New Temporary Chat": "Nuevo Chat Temporal",
|
||||
"New Tool": "Nueva Herramienta",
|
||||
"New Webhook": "Nuevo Webhook",
|
||||
"new-channel": "nuevo-canal",
|
||||
"Next message": "Siguiente mensaje",
|
||||
"No access grants. Private to you.": "",
|
||||
"No access grants. Private to you.": "Sin acceso concedido. Privado para tí.",
|
||||
"No activity data": "Sin datos de actividad",
|
||||
"No authentication": "Sin Autentificación",
|
||||
"No chats found": "No se encontró ningún chat",
|
||||
@@ -1231,16 +1231,16 @@
|
||||
"No content found": "No se encontró contenido",
|
||||
"No content to speak": "No hay contenido para hablar",
|
||||
"No conversation to save": "No hay conversación para guardar",
|
||||
"No data": "",
|
||||
"No data found": "",
|
||||
"No data": "Sin datos",
|
||||
"No data found": "No se encontró ningún dato",
|
||||
"No distance available": "No hay distancia disponible",
|
||||
"No expiration can pose security risks.": "No expiración puede poner la seguridad en riesgo.",
|
||||
"No feedback found": "No se encontró ninguna opinión",
|
||||
"No file selected": "No se seleccionó archivo",
|
||||
"No files found": "",
|
||||
"No files found": "No se encontraron archivos",
|
||||
"No files in this knowledge base.": "Ningún archivo en esta base de conocimiento.",
|
||||
"No functions found": "No se encontraron funciones",
|
||||
"No history available": "",
|
||||
"No history available": "No hay historial disponible",
|
||||
"No HTML, CSS, or JavaScript content found.": "No se encontró contenido HTML, CSS, o JavaScript.",
|
||||
"No inference engine with management support found": "No se encontró un motor de inferencia que soporte gestión",
|
||||
"No knowledge bases found.": "No se encontraron bases de conocimiento",
|
||||
@@ -1257,7 +1257,7 @@
|
||||
"No results": "No se encontraron resultados",
|
||||
"No results found": "No se encontraron resultados",
|
||||
"No search query generated": "No se generó ninguna consulta de búsqueda",
|
||||
"No skills found": "",
|
||||
"No skills found": "No se encontraron habilidades",
|
||||
"No source available": "No hay fuente disponible",
|
||||
"No sources found": "No se han encontrado fuentes",
|
||||
"No suggestion prompts": "Sin prompts sugeridos",
|
||||
@@ -1316,7 +1316,7 @@
|
||||
"Open modal to configure connection": "Abrir modal para configurar la conexión",
|
||||
"Open Modal To Manage Floating Quick Actions": "Abrir Modal para Gestionar Acciones Rápidas Flotantes",
|
||||
"Open Modal To Manage Image Compression": "Abrir Modal para Gestionar Compresión de Imagen",
|
||||
"Open Model Selector": "",
|
||||
"Open Model Selector": "Avrir Selector de Modelos",
|
||||
"Open Settings": "Abrir Configuración",
|
||||
"Open Sidebar": "Abrir Barra Lateral",
|
||||
"Open User Profile Menu": "Abrir Menu de Perfiles de Usuario",
|
||||
@@ -1336,19 +1336,20 @@
|
||||
"OpenAPI": "OpenAPI",
|
||||
"OpenAPI Spec": "Especif. OpenAPI",
|
||||
"openapi.json URL or Path": "URL o Ruta a openapi.json",
|
||||
"optional": "",
|
||||
"optional": "opcional",
|
||||
"Optional": "Opcional",
|
||||
"or": "o",
|
||||
"Ordered List": "Lista Ordenada",
|
||||
"Organize your users": "Organiza tus usuarios",
|
||||
"Other": "Otro",
|
||||
"Output": "",
|
||||
"OUTPUT": "SALIDA",
|
||||
"Output format": "Formato de salida",
|
||||
"Output Format": "Formato de Salida",
|
||||
"Overview": "Vista General",
|
||||
"page": "página",
|
||||
"Page": "",
|
||||
"Page mode creates one document per page. Single mode combines all pages into one document for better chunking across page boundaries.": "",
|
||||
"Page": "Página",
|
||||
"Page mode creates one document per page. Single mode combines all pages into one document for better chunking across page boundaries.": "El modo Página crea un documento por página. El modo Individual combina todas las páginas en un solo documento para una mejor segmentación entre páginas.",
|
||||
"Paginate": "Paginar",
|
||||
"Parameters": "Parámetros",
|
||||
"Parent message not found": "Mensaje padre no encontrado",
|
||||
@@ -1358,7 +1359,7 @@
|
||||
"Paste Large Text as File": "Pegar el Texto Largo como Archivo",
|
||||
"PDF document (.pdf)": "Documento PDF (.pdf)",
|
||||
"PDF Extract Images (OCR)": "Extraer imágenes del PDF (OCR)",
|
||||
"PDF Loader Mode": "",
|
||||
"PDF Loader Mode": "Modo de Carga del PDF",
|
||||
"pending": "pendiente",
|
||||
"Pending": "Pendiente",
|
||||
"Pending User Overlay Content": "Contenido de la SobreCapa Usuario Pendiente",
|
||||
@@ -1418,13 +1419,13 @@
|
||||
"Previous message": "Mensaje anterior",
|
||||
"Private": "Privado",
|
||||
"Private conversation between selected users": "Conversación privada entre l@s usuari@s seleccionados",
|
||||
"Production version updated": "",
|
||||
"Production version updated": "Versión de Producción actualizada",
|
||||
"Profile": "Perfil",
|
||||
"Prompt": "Indicador",
|
||||
"Prompt Autocompletion": "Autocompletado del Indicador",
|
||||
"Prompt Content": "Contenido del Indicador",
|
||||
"Prompt created successfully": "Indicador creado exitosamente",
|
||||
"Prompt Name": "",
|
||||
"Prompt Name": "Nombre del Indicador",
|
||||
"Prompt updated successfully": "Indicador actualizado correctamente",
|
||||
"Prompts": "Indicadores",
|
||||
"Prompts Access": "Acceso a Indicadores",
|
||||
@@ -1478,6 +1479,7 @@
|
||||
"Remove image": "Eliminar imagen",
|
||||
"Remove Model": "Eliminar Modelo",
|
||||
"Rename": "Renombrar",
|
||||
"Render Markdown in Previews": "",
|
||||
"Reorder Models": "Reordenar Modelos",
|
||||
"Reply": "Responder",
|
||||
"Reply in Thread": "Responder en Hilo",
|
||||
@@ -1496,7 +1498,7 @@
|
||||
"Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "Las notificaciones de respuesta no pueden activarse ya que los permisos del sitio web han sido denegados. Por favor, comprueba la configuración de tu navegador para otorgar el permiso necesario.",
|
||||
"Response splitting": "Particionado de Respuesta",
|
||||
"Response Watermark": "Marca de Agua en Respuesta",
|
||||
"Responses": "",
|
||||
"Responses": "Respuestas",
|
||||
"Result": "Resultado",
|
||||
"RESULT": "Resultado",
|
||||
"Retrieval": "Recuperación",
|
||||
@@ -1524,13 +1526,13 @@
|
||||
"Search": "Buscar",
|
||||
"Search a model": "Buscar un Modelo",
|
||||
"Search all emojis": "Buscar todos los emojis",
|
||||
"Search and manage user memories": "",
|
||||
"Search and view user chat history": "",
|
||||
"Search and manage user memories": "Buscar y gestionar memorias del usuario",
|
||||
"Search and view user chat history": "Buscr y ver historial de los chat del usuario",
|
||||
"Search Base": "Busqueda Base",
|
||||
"Search channels and channel messages": "",
|
||||
"Search channels and channel messages": "Buscar canales y mensajes del canal",
|
||||
"Search Chats": "Buscar Chats",
|
||||
"Search Collection": "Buscar Colección",
|
||||
"Search Files": "",
|
||||
"Search Files": "Buscar archivos",
|
||||
"Search Filters": "Buscar Filtros",
|
||||
"search for archived chats": "buscar chats archivados",
|
||||
"search for folders": "buscar carpetas",
|
||||
@@ -1545,11 +1547,11 @@
|
||||
"Search options": "Opciones de Búsqueda",
|
||||
"Search Prompts": "Buscar Indicadores",
|
||||
"Search Result Count": "Número de resultados de la búsqueda",
|
||||
"Search Skills": "",
|
||||
"Search Skills": "Buscar Habilidades",
|
||||
"Search the internet": "Buscar en internet",
|
||||
"Search the web and fetch URLs": "",
|
||||
"Search the web and fetch URLs": "Buscar en la web y obtener URLs",
|
||||
"Search Tools": "Buscar Herramientas",
|
||||
"Search, view, and manage user notes": "",
|
||||
"Search, view, and manage user notes": "Buscar, ver y gestionar notas del usuario",
|
||||
"SearchApi API Key": "Clave API de SearchApi",
|
||||
"SearchApi Engine": "Motor SearchApi",
|
||||
"Searched {{count}} sites": "{{count}} sitios buscados",
|
||||
@@ -1584,7 +1586,7 @@
|
||||
"Select an embedding model engine": "Seleccionar un motor de modelos de incrustación",
|
||||
"Select an engine": "Seleccionar un motor",
|
||||
"Select an Ollama instance": "Seleccionar una instancia de Ollama",
|
||||
"Select an option": "",
|
||||
"Select an option": "Seleccionar una opción",
|
||||
"Select an output format": "Seleccionar un formato de salida",
|
||||
"Select dtype": "Seleccionar dtype",
|
||||
"Select Engine": "Seleccionar Motor",
|
||||
@@ -1598,7 +1600,7 @@
|
||||
"Send": "Enviar",
|
||||
"Send a Message": "Enviar un Mensaje",
|
||||
"Send message": "Enviar Mensaje",
|
||||
"Send now": "",
|
||||
"Send now": "Enviar ahora",
|
||||
"Sends `stream_options: { include_usage: true }` in the request.\nSupported providers will return token usage information in the response when set.": "Envia en la solicitud de transmisión la opción: `{ include_usage: true }`.\nSi se activa, los proveedores que soporten esta función devolverán en la respuesta información de uso de los token.",
|
||||
"September": "Septiembre",
|
||||
"SerpApi API Key": "Clave API de SerpApi",
|
||||
@@ -1609,7 +1611,7 @@
|
||||
"Server connection verified": "Conexión al servidor verificada",
|
||||
"Session": "Sesión",
|
||||
"Set as default": "Establecer como Predeterminado",
|
||||
"Set as Production": "",
|
||||
"Set as Production": "Establecer como Producción",
|
||||
"Set embedding model": "Establecer Modelo de Incrustación",
|
||||
"Set embedding model (e.g. {{model}})": "Establecer Modelo para Incrustación (p.ej. {{model}})",
|
||||
"Set reranking model (e.g. {{model}})": "Establecer Modelo para Reclasificación (p.ej. {{model}})",
|
||||
@@ -1630,15 +1632,16 @@
|
||||
"Settings saved successfully!": "¡Ajustes guardados correctamente!",
|
||||
"Share": "Compartir",
|
||||
"Share Chat": "Compartir Chat",
|
||||
"Share link copied to clipboard.": "",
|
||||
"Share link copied to clipboard.": "Enlace Compartido copiado al portapapeles",
|
||||
"Share to Open WebUI Community": "Compartir con la Comunidad Open-WebUI",
|
||||
"Share your background and interests": "Compartir tus antecedentes e intereses",
|
||||
"Shared Chats": "",
|
||||
"Shared Chats": "Chats Compartidos",
|
||||
"Shared with you": "Compartido contigo",
|
||||
"Sharing Permissions": "Permisos al Compartir",
|
||||
"Show": "Mostrar",
|
||||
"Show \"What's New\" modal on login": "Mostrar modal \"Qué hay de Nuevo\" al iniciar sesión",
|
||||
"Show Admin Details in Account Pending Overlay": "Mostrar Detalles Admin en la sobrecapa de 'Cuenta Pendiente'",
|
||||
"Show all ({{COUNT}} characters)": "",
|
||||
"Show Files": "Mostrar Archivos",
|
||||
"Show Formatting Toolbar": "Mostrar barra de herramientas de Formateo",
|
||||
"Show image preview": "Mostrar previsualización de imagen",
|
||||
@@ -1646,7 +1649,7 @@
|
||||
"Show Shortcuts": "Mostrar Atajos de Teclado",
|
||||
"Show your support!": "¡Muestra tu apoyo!",
|
||||
"Showcased creativity": "Creatividad exhibida",
|
||||
"Showing all messages (user + assistant) per user.": "",
|
||||
"Showing all messages (user + assistant) per user.": "Mostrando todos los mensajes (usuario y asistente) del usuario.",
|
||||
"Sign in": "Iniciar Sesión",
|
||||
"Sign in to {{WEBUI_NAME}}": "Iniciar Sesión en {{WEBUI_NAME}}",
|
||||
"Sign in to {{WEBUI_NAME}} with LDAP": "Iniciar Sesión en {{WEBUI_NAME}} con LDAP",
|
||||
@@ -1655,17 +1658,19 @@
|
||||
"Sign up to {{WEBUI_NAME}}": "Crear una Cuenta en {{WEBUI_NAME}}",
|
||||
"Significantly improves accuracy by using an LLM to enhance tables, forms, inline math, and layout detection. Will increase latency. Defaults to False.": "Mejora significativamente la precisión al usar un LLM para optimizar tablas, formularios, cálculos en línea y la detección de diseño. Aumentará la latencia. El valor predeterminado es Falso.",
|
||||
"Signing in to {{WEBUI_NAME}}": "Iniciando Sesión en {{WEBUI_NAME}}",
|
||||
"Single": "",
|
||||
"Single": "Individual",
|
||||
"Sink List": "Plegar Lista",
|
||||
"sk-1234": "sk-1234",
|
||||
"Skill created successfully": "",
|
||||
"Skill deleted successfully": "",
|
||||
"Skill Description": "",
|
||||
"Skill ID": "",
|
||||
"Skill Name": "",
|
||||
"Skill updated successfully": "",
|
||||
"Skills": "",
|
||||
"Skills Access": "",
|
||||
"Skill created successfully": "Habilidad creada correctamente",
|
||||
"Skill deleted successfully": "Habilidad borrada correctamente",
|
||||
"Skill Description": "Descripción de la Habilidad",
|
||||
"Skill ID": "ID de la Habilidad",
|
||||
"Skill Name": "Nombre de la Habilidad",
|
||||
"Skill updated successfully": "Habilidad actualiada correctamente",
|
||||
"Skills": "Habilidades",
|
||||
"Skills Access": "Acceso a Habilidades",
|
||||
"Skills Public Sharing": "",
|
||||
"Skills Sharing": "",
|
||||
"Skip Cache": "Evitar Caché",
|
||||
"Skip the cache and re-run the inference. Defaults to False.": "Evitar caché y reiniciar la interfaz. Valor predeterminado Falso",
|
||||
"Something went wrong :/": "Algo ha ido mal :/",
|
||||
@@ -1772,7 +1777,7 @@
|
||||
"This channel was created on {{createdAt}}. This is the very beginning of the {{channelName}} channel.": "Este canal fue creado el {{createdAt}}. Este es el comienzo del canal {{channelName}}.",
|
||||
"This chat won't appear in history and your messages will not be saved.": "Este chat no aparecerá en el historial y los mensajes no se guardarán.",
|
||||
"This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "Esto garantiza que sus valiosas conversaciones se guardan de forma segura en tu base de datos del servidor trasero (backend). ¡Gracias!",
|
||||
"This feature is currently experimental and may not work as expected.": "",
|
||||
"This feature is currently experimental and may not work as expected.": "Acualmente esta característica es experimental y puede no funcionar como se espera.",
|
||||
"This feature is experimental and may be modified or discontinued without notice.": "Esta característica es experimental y podría ser modificada o discontinuada sin previo aviso.",
|
||||
"This is a default user permission and will remain enabled.": "Este es un permiso predeterminado y se mantentrá activo ",
|
||||
"This is an experimental feature, it may not function as expected and is subject to change at any time.": "Esta es una característica experimental, por lo que puede no funcionar como se esperaba y está sujeta a cambios en cualquier momento.",
|
||||
@@ -1793,11 +1798,11 @@
|
||||
"Thought for {{DURATION}} seconds": "Pensando durante {{DURATION}} segundos",
|
||||
"Thought for less than a second": "Pensando durante menos de un segundo",
|
||||
"Thread": "Hilo",
|
||||
"Thumbs up/down ratings from users on model responses": "",
|
||||
"Thumbs up/down ratings from users on model responses": "Calificación (pulgares arriba/abajo) de los usuarios en las respuestas del modelo",
|
||||
"Tika": "Tika",
|
||||
"Tika Server URL required.": "URL del Servidor Tika necesaria",
|
||||
"Tiktoken": "Tiktoken",
|
||||
"Time & Calculation": "",
|
||||
"Time & Calculation": "Tiempo y Cálculo",
|
||||
"Timeout": "TimeOut",
|
||||
"Title": "Título",
|
||||
"Title Auto-Generation": "AutoGeneración de Títulos",
|
||||
@@ -1810,7 +1815,7 @@
|
||||
"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "Para acceder a WebUI, por favor contacte con Admins. Los administradores pueden gestionar los estados de los usuarios esde el panel de administración.",
|
||||
"To attach knowledge base here, add them to the \"Knowledge\" workspace first.": "Para adjuntar la base de conocimientos aquí, primero añadirla a \"Conocimiento\" en el área de trabajo.",
|
||||
"To learn more about available endpoints, visit our documentation.": "Para aprender más sobre los endpoints disponibles, visite nuestra documentación.",
|
||||
"To select skills here, add them to the \"Skills\" workspace first.": "",
|
||||
"To select skills here, add them to the \"Skills\" workspace first.": "Para seleccionar habilidades aquí, primero añadelas a \"Habilidades\" en el áreas de trabajo.",
|
||||
"To select toolkits here, add them to the \"Tools\" workspace first.": "Para seleccionar herramientas aquí, primero añadelas a \"Herramientas\" en el área de trabajo.",
|
||||
"Toast notifications for new updates": "Notificaciones emergentes para nuevas actualizaciones",
|
||||
"Today": "Hoy",
|
||||
@@ -1818,9 +1823,9 @@
|
||||
"Toggle Sidebar": "Des/Plegar la Barra Lateral",
|
||||
"Toggle whether current connection is active.": "Alternar si la conexión actual está activa",
|
||||
"Token": "Token",
|
||||
"Token counts are estimates and may not reflect actual API usage": "",
|
||||
"tokens": "",
|
||||
"Tokens": "",
|
||||
"Token counts are estimates and may not reflect actual API usage": "El recuento de tokens es estimado y puede diferir del uso real de la API",
|
||||
"tokens": "tokens",
|
||||
"Tokens": "Tokens",
|
||||
"Too verbose": "Demasiado detallado",
|
||||
"Tool created successfully": "Herramienta creada correctamente",
|
||||
"Tool deleted successfully": "Herramienta eliminada correctamente",
|
||||
@@ -1864,7 +1869,7 @@
|
||||
"Unlock mysteries": "Desbloquear misterios",
|
||||
"Unpin": "Desfijar",
|
||||
"Unravel secrets": "Desentrañar secretos",
|
||||
"Unshare Chat": "",
|
||||
"Unshare Chat": "Descompartir Chat",
|
||||
"Unsupported file type.": "Tipo de archivo no soportado",
|
||||
"Untagged": "Sin Etiqueta",
|
||||
"Untitled": "Sin Título",
|
||||
@@ -1893,7 +1898,7 @@
|
||||
"URL is required": "La URL es requerida",
|
||||
"URL Mode": "Modo URL",
|
||||
"Usage": "Uso",
|
||||
"Use": "",
|
||||
"Use": "Usar",
|
||||
"Use '#' in the prompt input to load and include your knowledge.": "Utilizar '#' en el indicador para cargar e incluir tu conocimiento.",
|
||||
"Use /v1/chat/completions endpoint instead of /v1/audio/transcriptions for potentially better accuracy.": "Usar el endpoint /v1/chat/completions en vez de /v1/audio/transcriptions para una (posible) mayor precisión.",
|
||||
"Use Chat Completions API": "Usar Chat Completions",
|
||||
@@ -1903,7 +1908,7 @@
|
||||
"Use proxy designated by http_proxy and https_proxy environment variables to fetch page contents.": "Usar el proxy asignado en las variables del entorno http_proxy y/o https_proxy para extraer contenido",
|
||||
"user": "usuario",
|
||||
"User": "Usuario",
|
||||
"User Activity": "",
|
||||
"User Activity": "Actividad del Usuario",
|
||||
"User Groups": "Grupos de Usuarios",
|
||||
"User location successfully retrieved.": "Ubicación de usuario obtenida correctamente.",
|
||||
"User menu": "Menu de Usuario",
|
||||
@@ -1928,11 +1933,11 @@
|
||||
"Verify SSL Certificate": "Verificar Certificado SSL",
|
||||
"Version": "Versión",
|
||||
"Version {{selectedVersion}} of {{totalVersions}}": "Versión {{selectedVersion}} de {{totalVersions}}",
|
||||
"Version deleted": "",
|
||||
"Version deleted": "Versión eliminada",
|
||||
"View Replies": "Ver Respuestas",
|
||||
"View Result from **{{NAME}}**": "Ver Resultado desde **{{NAME}}**",
|
||||
"Visibility": "Visibilidad",
|
||||
"Visible": "",
|
||||
"Visible": "Visible",
|
||||
"Visible to all users": "Visible para todos los usuarios",
|
||||
"Vision": "Visión",
|
||||
"Voice": "Voz",
|
||||
@@ -1988,9 +1993,9 @@
|
||||
"Yacy Username": "Usuario de Yacy",
|
||||
"Yahoo": "Yahoo",
|
||||
"Yandex": "Yandex",
|
||||
"Yandex Web Search API Key": "",
|
||||
"Yandex Web Search config": "",
|
||||
"Yandex Web Search URL": "",
|
||||
"Yandex Web Search API Key": "Clave API de la Búsqueda Web de Yandex",
|
||||
"Yandex Web Search config": "Confoguración de la Búsqueda Web de Yantex",
|
||||
"Yandex Web Search URL": "URL de la Búsqueda Web de Yandex",
|
||||
"Yesterday": "Ayer",
|
||||
"Yesterday at {{LOCALIZED_TIME}}": "Ayer a las {{LOCALIZED_TIME}}",
|
||||
"You": "Tu",
|
||||
@@ -2000,15 +2005,15 @@
|
||||
"You cannot upload an empty file.": "No puedes subir un archivo vacío.",
|
||||
"You do not have permission to edit this model": "No tienes permiso para editar este modelo",
|
||||
"You do not have permission to edit this prompt.": "No tienes permiso para editar este indicador",
|
||||
"You do not have permission to edit this skill.": "",
|
||||
"You do not have permission to edit this tool": "",
|
||||
"You do not have permission to make this public": "",
|
||||
"You do not have permission to edit this skill.": "No tienes permiso para editar esta habilidad",
|
||||
"You do not have permission to edit this tool": "No tienes permiso para editar esta herramienta",
|
||||
"You do not have permission to make this public": "No tienes permiso para hacer público esto",
|
||||
"You do not have permission to send messages in this channel.": "No tienes permiso para enviar mensajes en este canal",
|
||||
"You do not have permission to send messages in this thread.": "No tienes permiso para enviar mensajes en este hilo",
|
||||
"You do not have permission to upload files to this knowledge base.": "No tienes permiso para subir archivos a esta base de conocimiento.",
|
||||
"You do not have permission to upload files.": "No tienes permiso para subir archivos.",
|
||||
"You have no archived conversations.": "No tienes conversaciones archivadas.",
|
||||
"You have no shared conversations.": "",
|
||||
"You have no shared conversations.": "No tienes conversaciones compartidas",
|
||||
"You have shared this chat": "Has compartido esta conversación",
|
||||
"You're a helpful assistant.": "Eres un asistente atento, amable y servicial.",
|
||||
"You're now logged in.": "Has iniciado sesión.",
|
||||
|
||||
@@ -98,7 +98,6 @@
|
||||
"Allow Call": "Luba kõne",
|
||||
"Allow Chat Controls": "Luba vestluse kontrollnupud",
|
||||
"Allow Chat Delete": "Luba vestluse kustutamine",
|
||||
"Allow Chat Deletion": "Luba vestluse kustutamine",
|
||||
"Allow Chat Edit": "Luba vestluse muutmine",
|
||||
"Allow Chat Export": "Luba vestluse eksport",
|
||||
"Allow Chat Params": "Luba vestluse parameetrid",
|
||||
@@ -346,6 +345,7 @@
|
||||
"Completions": "Lõpetamised",
|
||||
"Compress Images in Channels": "Tihenda pildid kanalites",
|
||||
"Concurrent Requests": "Samaaegsed päringud",
|
||||
"Config": "",
|
||||
"Config imported successfully": "Seadistus edukalt imporditud",
|
||||
"Configure": "Konfigureeri",
|
||||
"Confirm": "Kinnita",
|
||||
@@ -769,7 +769,7 @@
|
||||
"Export All Chats (All Users)": "Ekspordi kõik vestlused (kõik kasutajad)",
|
||||
"Export chat (.json)": "Ekspordi vestlus (.json)",
|
||||
"Export Chats": "Ekspordi vestlused",
|
||||
"Export Config to JSON File": "Ekspordi seadistus JSON-failina",
|
||||
"Export Config": "",
|
||||
"Export Models": "",
|
||||
"Export Presets": "Ekspordi eelseadistused",
|
||||
"Export Prompts": "",
|
||||
@@ -976,7 +976,7 @@
|
||||
"Images": "",
|
||||
"Import": "Impordi",
|
||||
"Import Chats": "Impordi vestlused",
|
||||
"Import Config from JSON File": "Impordi seadistus JSON-failist",
|
||||
"Import Config": "",
|
||||
"Import From Link": "Impordi lingist",
|
||||
"Import Models": "",
|
||||
"Import Presets": "Impordi eelseadistused",
|
||||
@@ -1342,6 +1342,7 @@
|
||||
"Ordered List": "Ordered List",
|
||||
"Organize your users": "Korraldage oma kasutajad",
|
||||
"Other": "Muu",
|
||||
"Output": "",
|
||||
"OUTPUT": "VÄLJUND",
|
||||
"Output format": "Väljundformaat",
|
||||
"Output Format": "Output Format",
|
||||
@@ -1478,6 +1479,7 @@
|
||||
"Remove image": "Eemalda pilt",
|
||||
"Remove Model": "Eemalda mudel",
|
||||
"Rename": "Nimeta ümber",
|
||||
"Render Markdown in Previews": "",
|
||||
"Reorder Models": "Muuda mudelite järjekorda",
|
||||
"Reply": "Reply",
|
||||
"Reply in Thread": "Vasta lõimes",
|
||||
@@ -1638,6 +1640,7 @@
|
||||
"Show": "Näita",
|
||||
"Show \"What's New\" modal on login": "Näita \"Mis on uut\" modaalakent sisselogimisel",
|
||||
"Show Admin Details in Account Pending Overlay": "Näita administraatori üksikasju konto ootel kattekihil",
|
||||
"Show all ({{COUNT}} characters)": "",
|
||||
"Show Files": "",
|
||||
"Show Formatting Toolbar": "Kuva Formatting Toolbar",
|
||||
"Show image preview": "Kuva pilt preview",
|
||||
@@ -1665,6 +1668,8 @@
|
||||
"Skill updated successfully": "",
|
||||
"Skills": "",
|
||||
"Skills Access": "",
|
||||
"Skills Public Sharing": "",
|
||||
"Skills Sharing": "",
|
||||
"Skip Cache": "Skip Vahemälu",
|
||||
"Skip the cache and re-run the inference. Defaults to False.": "Skip the vahemälu ja re-run the inference. Defaults kuni False.",
|
||||
"Something went wrong :/": "Something went wrong :/",
|
||||
|
||||
@@ -98,7 +98,6 @@
|
||||
"Allow Call": "",
|
||||
"Allow Chat Controls": "",
|
||||
"Allow Chat Delete": "Baimendu Txata Ezabatzea",
|
||||
"Allow Chat Deletion": "Baimendu Txata Ezabatzea",
|
||||
"Allow Chat Edit": "Baimendu Txata Editatzea",
|
||||
"Allow Chat Export": "",
|
||||
"Allow Chat Params": "",
|
||||
@@ -346,6 +345,7 @@
|
||||
"Completions": "Osatzeak",
|
||||
"Compress Images in Channels": "",
|
||||
"Concurrent Requests": "Eskari Konkurrenteak",
|
||||
"Config": "",
|
||||
"Config imported successfully": "",
|
||||
"Configure": "Konfiguratu",
|
||||
"Confirm": "Berretsi",
|
||||
@@ -769,7 +769,7 @@
|
||||
"Export All Chats (All Users)": "Esportatu Txat Guztiak (Erabiltzaile Guztiak)",
|
||||
"Export chat (.json)": "Esportatu txata (.json)",
|
||||
"Export Chats": "Esportatu Txatak",
|
||||
"Export Config to JSON File": "Esportatu Konfigurazioa JSON Fitxategira",
|
||||
"Export Config": "",
|
||||
"Export Models": "",
|
||||
"Export Presets": "Esportatu Aurrezarpenak",
|
||||
"Export Prompts": "",
|
||||
@@ -976,7 +976,7 @@
|
||||
"Images": "",
|
||||
"Import": "",
|
||||
"Import Chats": "Inportatu Txatak",
|
||||
"Import Config from JSON File": "Inportatu Konfigurazioa JSON Fitxategitik",
|
||||
"Import Config": "",
|
||||
"Import From Link": "",
|
||||
"Import Models": "",
|
||||
"Import Presets": "Inportatu Aurrezarpenak",
|
||||
@@ -1342,6 +1342,7 @@
|
||||
"Ordered List": "",
|
||||
"Organize your users": "Antolatu zure erabiltzaileak",
|
||||
"Other": "Bestelakoa",
|
||||
"Output": "",
|
||||
"OUTPUT": "IRTEERA",
|
||||
"Output format": "Irteera formatua",
|
||||
"Output Format": "",
|
||||
@@ -1478,6 +1479,7 @@
|
||||
"Remove image": "",
|
||||
"Remove Model": "Kendu modeloa",
|
||||
"Rename": "Berrizendatu",
|
||||
"Render Markdown in Previews": "",
|
||||
"Reorder Models": "Berrantolatu modeloak",
|
||||
"Reply": "",
|
||||
"Reply in Thread": "",
|
||||
@@ -1638,6 +1640,7 @@
|
||||
"Show": "Erakutsi",
|
||||
"Show \"What's New\" modal on login": "Erakutsi \"Berritasunak\" modala saioa hastean",
|
||||
"Show Admin Details in Account Pending Overlay": "Erakutsi administratzaile xehetasunak kontu zain geruzan",
|
||||
"Show all ({{COUNT}} characters)": "",
|
||||
"Show Files": "",
|
||||
"Show Formatting Toolbar": "",
|
||||
"Show image preview": "",
|
||||
@@ -1665,6 +1668,8 @@
|
||||
"Skill updated successfully": "",
|
||||
"Skills": "",
|
||||
"Skills Access": "",
|
||||
"Skills Public Sharing": "",
|
||||
"Skills Sharing": "",
|
||||
"Skip Cache": "",
|
||||
"Skip the cache and re-run the inference. Defaults to False.": "",
|
||||
"Something went wrong :/": "",
|
||||
|
||||
@@ -98,7 +98,6 @@
|
||||
"Allow Call": "اجازه تماس",
|
||||
"Allow Chat Controls": "اجازه کنترل\u200cهای گفتگو",
|
||||
"Allow Chat Delete": "اجازه حذف گفتگو",
|
||||
"Allow Chat Deletion": "اجازهٔ حذف گفتگو",
|
||||
"Allow Chat Edit": "اجازه ویرایش گفتگو",
|
||||
"Allow Chat Export": "مجاز کردن خروجی گرفتن از چت",
|
||||
"Allow Chat Params": "مجاز کردن پارامترهای چت",
|
||||
@@ -346,6 +345,7 @@
|
||||
"Completions": "تکمیل\u200cها",
|
||||
"Compress Images in Channels": "فشرده\u200cسازی تصاویر در کانال\u200cها",
|
||||
"Concurrent Requests": "درخواست های همزمان",
|
||||
"Config": "",
|
||||
"Config imported successfully": "پیکربندی با موفقیت وارد شد",
|
||||
"Configure": "پیکربندی",
|
||||
"Confirm": "تایید",
|
||||
@@ -769,7 +769,7 @@
|
||||
"Export All Chats (All Users)": "خروجی گرفتن همه گفتگو\u200cها (همه کاربران)",
|
||||
"Export chat (.json)": "خروجی گرفتن گفتگو (json)",
|
||||
"Export Chats": "خروجی گرفتن گفتگوها",
|
||||
"Export Config to JSON File": "برون\u200cریزی پیکربندی به پروندهٔ JSON",
|
||||
"Export Config": "",
|
||||
"Export Models": "",
|
||||
"Export Presets": "برون\u200cریزی پیش\u200cتنظیم\u200cها",
|
||||
"Export Prompts": "",
|
||||
@@ -976,7 +976,7 @@
|
||||
"Images": "",
|
||||
"Import": "وارد کردن",
|
||||
"Import Chats": "درون\u200cریزی گفتگوها",
|
||||
"Import Config from JSON File": "درون\u200cریزی از پروندهٔ JSON",
|
||||
"Import Config": "",
|
||||
"Import From Link": "وارد کردن از لینک",
|
||||
"Import Models": "",
|
||||
"Import Presets": "درون\u200cریزی پیش\u200cتنظیم\u200cها",
|
||||
@@ -1342,6 +1342,7 @@
|
||||
"Ordered List": "لیست شماره\u200cگذاری شده",
|
||||
"Organize your users": "کاربران خود را سازماندهی کنید",
|
||||
"Other": "دیگر",
|
||||
"Output": "",
|
||||
"OUTPUT": "خروجی",
|
||||
"Output format": "قالب خروجی",
|
||||
"Output Format": "قالب خروجی",
|
||||
@@ -1478,6 +1479,7 @@
|
||||
"Remove image": "حذف تصویر",
|
||||
"Remove Model": "حذف مدل",
|
||||
"Rename": "تغییر نام",
|
||||
"Render Markdown in Previews": "",
|
||||
"Reorder Models": "ترتیب مجدد مدل\u200cها",
|
||||
"Reply": "پاسخ",
|
||||
"Reply in Thread": "پاسخ در رشته",
|
||||
@@ -1638,6 +1640,7 @@
|
||||
"Show": "نمایش",
|
||||
"Show \"What's New\" modal on login": "نمایش مودال \"موارد جدید\" هنگام ورود",
|
||||
"Show Admin Details in Account Pending Overlay": "نمایش جزئیات مدیر در پوشش حساب در انتظار",
|
||||
"Show all ({{COUNT}} characters)": "",
|
||||
"Show Files": "",
|
||||
"Show Formatting Toolbar": "نمایش نوار ابزار قالب\u200cبندی",
|
||||
"Show image preview": "نمایش پیش\u200cنمایش تصویر",
|
||||
@@ -1665,6 +1668,8 @@
|
||||
"Skill updated successfully": "",
|
||||
"Skills": "",
|
||||
"Skills Access": "",
|
||||
"Skills Public Sharing": "",
|
||||
"Skills Sharing": "",
|
||||
"Skip Cache": "رد کردن کش",
|
||||
"Skip the cache and re-run the inference. Defaults to False.": "کش را رد کرده و استنتاج را مجدداً اجرا کنید. پیش\u200cفرض: False.",
|
||||
"Something went wrong :/": "مشکلی پیش آمد :/",
|
||||
|
||||
@@ -98,7 +98,6 @@
|
||||
"Allow Call": "Salli puhelut",
|
||||
"Allow Chat Controls": "Salli keskustelujen hallinta",
|
||||
"Allow Chat Delete": "Salli keskustelujen poisto",
|
||||
"Allow Chat Deletion": "Salli keskustelujen poisto",
|
||||
"Allow Chat Edit": "Salli keskustelujen muokkaus",
|
||||
"Allow Chat Export": "Salli keskustelujen vienti",
|
||||
"Allow Chat Params": "Salli keskustelujen parametrit",
|
||||
@@ -346,6 +345,7 @@
|
||||
"Completions": "Täydennykset",
|
||||
"Compress Images in Channels": "Pakkaa kuvat kanavissa",
|
||||
"Concurrent Requests": "Samanaikaiset pyynnöt",
|
||||
"Config": "",
|
||||
"Config imported successfully": "Määritysten tuonti onnistui",
|
||||
"Configure": "Määritä",
|
||||
"Confirm": "Vahvista",
|
||||
@@ -769,7 +769,7 @@
|
||||
"Export All Chats (All Users)": "Vie kaikki keskustelut (kaikki käyttäjät)",
|
||||
"Export chat (.json)": "Vie keskustelu (.json)",
|
||||
"Export Chats": "Vie keskustelut",
|
||||
"Export Config to JSON File": "Vie asetukset JSON-tiedostoon",
|
||||
"Export Config": "",
|
||||
"Export Models": "Vie mallit",
|
||||
"Export Presets": "Vie esiasetukset",
|
||||
"Export Prompts": "Vie kehoitteet",
|
||||
@@ -976,7 +976,7 @@
|
||||
"Images": "Kuvat",
|
||||
"Import": "Tuo",
|
||||
"Import Chats": "Tuo keskustelut",
|
||||
"Import Config from JSON File": "Tuo asetukset JSON-tiedostosta",
|
||||
"Import Config": "",
|
||||
"Import From Link": "Tuo verkko-osoitteesta",
|
||||
"Import Models": "Tuo mallit",
|
||||
"Import Presets": "Tuo esiasetuksia",
|
||||
@@ -1342,6 +1342,7 @@
|
||||
"Ordered List": "Järjestetty lista",
|
||||
"Organize your users": "Järjestä käyttäjäsi",
|
||||
"Other": "Muu",
|
||||
"Output": "",
|
||||
"OUTPUT": "TULOSTE",
|
||||
"Output format": "Tulosteen muoto",
|
||||
"Output Format": "Tulosteen muoto",
|
||||
@@ -1478,6 +1479,7 @@
|
||||
"Remove image": "Poista kuva",
|
||||
"Remove Model": "Poista malli",
|
||||
"Rename": "Nimeä uudelleen",
|
||||
"Render Markdown in Previews": "",
|
||||
"Reorder Models": "Uudelleenjärjestä malleja",
|
||||
"Reply": "Vastaa",
|
||||
"Reply in Thread": "Vastaa ketjussa",
|
||||
@@ -1638,6 +1640,7 @@
|
||||
"Show": "Näytä",
|
||||
"Show \"What's New\" modal on login": "Näytä \"Mitä uutta\" -modaali kirjautumisen yhteydessä",
|
||||
"Show Admin Details in Account Pending Overlay": "Näytä ylläpitäjän tiedot odottavan tilin päällä",
|
||||
"Show all ({{COUNT}} characters)": "",
|
||||
"Show Files": "Näytä tiedostot",
|
||||
"Show Formatting Toolbar": "Näytä muotoilupalkki",
|
||||
"Show image preview": "Näytä kuvan esikatselu",
|
||||
@@ -1665,6 +1668,8 @@
|
||||
"Skill updated successfully": "",
|
||||
"Skills": "",
|
||||
"Skills Access": "",
|
||||
"Skills Public Sharing": "",
|
||||
"Skills Sharing": "",
|
||||
"Skip Cache": "Ohita välimuisti",
|
||||
"Skip the cache and re-run the inference. Defaults to False.": "Ohita välimuisti ja suorita päätelmä uudelleen. Oletusarvo ei käytössä.",
|
||||
"Something went wrong :/": "Jokin meni pieleen :/",
|
||||
|
||||
@@ -98,7 +98,6 @@
|
||||
"Allow Call": "Autoriser les appels",
|
||||
"Allow Chat Controls": "Autoriser les contrôles de la conversation",
|
||||
"Allow Chat Delete": "Autoriser la suppression de la conversation",
|
||||
"Allow Chat Deletion": "Autoriser la suppression de l'historique de la conversation",
|
||||
"Allow Chat Edit": "Autoriser la modification de la conversation",
|
||||
"Allow Chat Export": "Autoriser l'exportation de la conversation",
|
||||
"Allow Chat Params": "",
|
||||
@@ -346,6 +345,7 @@
|
||||
"Completions": "Complétions",
|
||||
"Compress Images in Channels": "",
|
||||
"Concurrent Requests": "Demandes concurrentes",
|
||||
"Config": "",
|
||||
"Config imported successfully": "",
|
||||
"Configure": "Configurer",
|
||||
"Confirm": "Confirmer",
|
||||
@@ -769,7 +769,7 @@
|
||||
"Export All Chats (All Users)": "Exporter toutes les conversations (de tous les utilisateurs)",
|
||||
"Export chat (.json)": "Exporter la conversation (.json)",
|
||||
"Export Chats": "Exporter les conversations",
|
||||
"Export Config to JSON File": "Exporter la configuration vers un fichier JSON",
|
||||
"Export Config": "",
|
||||
"Export Models": "",
|
||||
"Export Presets": "Exporter les préréglages",
|
||||
"Export Prompts": "",
|
||||
@@ -976,7 +976,7 @@
|
||||
"Images": "",
|
||||
"Import": "Importer",
|
||||
"Import Chats": "Importer les conversations",
|
||||
"Import Config from JSON File": "Importer la configuration depuis un fichier JSON",
|
||||
"Import Config": "",
|
||||
"Import From Link": "Importer depuis le lien",
|
||||
"Import Models": "",
|
||||
"Import Presets": "Importer les préréglages",
|
||||
@@ -1342,6 +1342,7 @@
|
||||
"Ordered List": "",
|
||||
"Organize your users": "Organisez vos utilisateurs",
|
||||
"Other": "Autre",
|
||||
"Output": "",
|
||||
"OUTPUT": "SORTIE",
|
||||
"Output format": "Format de sortie",
|
||||
"Output Format": "Format de sortie",
|
||||
@@ -1478,6 +1479,7 @@
|
||||
"Remove image": "Retirer l'image",
|
||||
"Remove Model": "Retirer le modèle",
|
||||
"Rename": "Renommer",
|
||||
"Render Markdown in Previews": "",
|
||||
"Reorder Models": "Réorganiser les modèles",
|
||||
"Reply": "",
|
||||
"Reply in Thread": "Répondre dans le fil de discussion",
|
||||
@@ -1639,6 +1641,7 @@
|
||||
"Show": "Afficher",
|
||||
"Show \"What's New\" modal on login": "Afficher la fenêtre modale \"Quoi de neuf\" lors de la connexion",
|
||||
"Show Admin Details in Account Pending Overlay": "Afficher les coordonnées de l'administrateur aux comptes en attente",
|
||||
"Show all ({{COUNT}} characters)": "",
|
||||
"Show Files": "",
|
||||
"Show Formatting Toolbar": "",
|
||||
"Show image preview": "Afficher l'aperçu de l'image",
|
||||
@@ -1666,6 +1669,8 @@
|
||||
"Skill updated successfully": "",
|
||||
"Skills": "",
|
||||
"Skills Access": "",
|
||||
"Skills Public Sharing": "",
|
||||
"Skills Sharing": "",
|
||||
"Skip Cache": "Ne pas utiliser le cache",
|
||||
"Skip the cache and re-run the inference. Defaults to False.": "Ne pas utiliser le cache et re executer l'inférence. Par defaut à False",
|
||||
"Something went wrong :/": "",
|
||||
|
||||
@@ -98,7 +98,6 @@
|
||||
"Allow Call": "Autoriser les appels",
|
||||
"Allow Chat Controls": "Autoriser les contrôles de la conversation",
|
||||
"Allow Chat Delete": "Autoriser la suppression de la conversation",
|
||||
"Allow Chat Deletion": "Autoriser la suppression de l'historique de la conversation",
|
||||
"Allow Chat Edit": "Autoriser la modification de la conversation",
|
||||
"Allow Chat Export": "Autoriser l'exportation de la conversation",
|
||||
"Allow Chat Params": "Autoriser les paramètres de discussion",
|
||||
@@ -346,6 +345,7 @@
|
||||
"Completions": "Complétions",
|
||||
"Compress Images in Channels": "Compresser les images dans les canaux",
|
||||
"Concurrent Requests": "Demandes concurrentes",
|
||||
"Config": "",
|
||||
"Config imported successfully": "Configuration importée avec succès",
|
||||
"Configure": "Configurer",
|
||||
"Confirm": "Confirmer",
|
||||
@@ -769,7 +769,7 @@
|
||||
"Export All Chats (All Users)": "Exporter toutes les conversations (de tous les utilisateurs)",
|
||||
"Export chat (.json)": "Exporter la conversation (.json)",
|
||||
"Export Chats": "Exporter les conversations",
|
||||
"Export Config to JSON File": "Exporter la configuration vers un fichier JSON",
|
||||
"Export Config": "",
|
||||
"Export Models": "Exporter les modèles",
|
||||
"Export Presets": "Exporter les préréglages",
|
||||
"Export Prompts": "Exporter les prompts",
|
||||
@@ -976,7 +976,7 @@
|
||||
"Images": "",
|
||||
"Import": "Importer",
|
||||
"Import Chats": "Importer les conversations",
|
||||
"Import Config from JSON File": "Importer la configuration depuis un fichier JSON",
|
||||
"Import Config": "",
|
||||
"Import From Link": "Importer depuis le lien",
|
||||
"Import Models": "Importer les modèles",
|
||||
"Import Presets": "Importer les préréglages",
|
||||
@@ -1342,6 +1342,7 @@
|
||||
"Ordered List": "Liste ordonnéee",
|
||||
"Organize your users": "Organisez vos utilisateurs",
|
||||
"Other": "Autre",
|
||||
"Output": "",
|
||||
"OUTPUT": "SORTIE",
|
||||
"Output format": "Format de sortie",
|
||||
"Output Format": "Format de sortie",
|
||||
@@ -1478,6 +1479,7 @@
|
||||
"Remove image": "Retirer l'image",
|
||||
"Remove Model": "Retirer le modèle",
|
||||
"Rename": "Renommer",
|
||||
"Render Markdown in Previews": "",
|
||||
"Reorder Models": "Réorganiser les modèles",
|
||||
"Reply": "",
|
||||
"Reply in Thread": "Répondre dans le fil de discussion",
|
||||
@@ -1639,6 +1641,7 @@
|
||||
"Show": "Afficher",
|
||||
"Show \"What's New\" modal on login": "Afficher la fenêtre modale \"Quoi de neuf\" lors de la connexion",
|
||||
"Show Admin Details in Account Pending Overlay": "Afficher les coordonnées de l'administrateur aux comptes en attente",
|
||||
"Show all ({{COUNT}} characters)": "",
|
||||
"Show Files": "",
|
||||
"Show Formatting Toolbar": "Afficher la barre d'outils de formatage",
|
||||
"Show image preview": "Afficher l'aperçu de l'image",
|
||||
@@ -1666,6 +1669,8 @@
|
||||
"Skill updated successfully": "",
|
||||
"Skills": "",
|
||||
"Skills Access": "",
|
||||
"Skills Public Sharing": "",
|
||||
"Skills Sharing": "",
|
||||
"Skip Cache": "Ne pas utiliser le cache",
|
||||
"Skip the cache and re-run the inference. Defaults to False.": "Ne pas utiliser le cache et re executer l'inférence. Par defaut à False",
|
||||
"Something went wrong :/": "Quelque chose s'est mal passé :/",
|
||||
|
||||
@@ -98,7 +98,6 @@
|
||||
"Allow Call": "",
|
||||
"Allow Chat Controls": "Permitir Control dos Chats",
|
||||
"Allow Chat Delete": "Permitir Borrar Chat",
|
||||
"Allow Chat Deletion": "Permitir Borrar Chats",
|
||||
"Allow Chat Edit": "Pemritir Editar Chat",
|
||||
"Allow Chat Export": "",
|
||||
"Allow Chat Params": "",
|
||||
@@ -346,6 +345,7 @@
|
||||
"Completions": "Respostas autoxeradas",
|
||||
"Compress Images in Channels": "",
|
||||
"Concurrent Requests": "Solicitudes simultáneas",
|
||||
"Config": "",
|
||||
"Config imported successfully": "",
|
||||
"Configure": "Configurar",
|
||||
"Confirm": "Confirmar",
|
||||
@@ -769,7 +769,7 @@
|
||||
"Export All Chats (All Users)": "Exportar todos os chats (Todos os usuarios)",
|
||||
"Export chat (.json)": "Exportar chat (.json)",
|
||||
"Export Chats": "Exportar Chats",
|
||||
"Export Config to JSON File": "Exportar configuración a Arquivo JSON",
|
||||
"Export Config": "",
|
||||
"Export Models": "",
|
||||
"Export Presets": "Exportar ajustes preestablecidos",
|
||||
"Export Prompts": "",
|
||||
@@ -976,7 +976,7 @@
|
||||
"Images": "",
|
||||
"Import": "",
|
||||
"Import Chats": "Importar chats",
|
||||
"Import Config from JSON File": "Importar configuración desde Arquivo JSON",
|
||||
"Import Config": "",
|
||||
"Import From Link": "",
|
||||
"Import Models": "",
|
||||
"Import Presets": "Importar ajustes preestablecidos",
|
||||
@@ -1342,6 +1342,7 @@
|
||||
"Ordered List": "",
|
||||
"Organize your users": "Organiza os teus usuarios",
|
||||
"Other": "Outro",
|
||||
"Output": "",
|
||||
"OUTPUT": "SAIDA",
|
||||
"Output format": "Formato de saida",
|
||||
"Output Format": "",
|
||||
@@ -1478,6 +1479,7 @@
|
||||
"Remove image": "",
|
||||
"Remove Model": "Eliminar modelo",
|
||||
"Rename": "Renombrar",
|
||||
"Render Markdown in Previews": "",
|
||||
"Reorder Models": "Reordenar modelos",
|
||||
"Reply": "",
|
||||
"Reply in Thread": "Responder no hilo",
|
||||
@@ -1638,6 +1640,7 @@
|
||||
"Show": "Mostrar",
|
||||
"Show \"What's New\" modal on login": "Mostrar modal \"Qué hay de novo\" al iniciar sesión",
|
||||
"Show Admin Details in Account Pending Overlay": "Mostrar detalles de administración na capa de espera da conta",
|
||||
"Show all ({{COUNT}} characters)": "",
|
||||
"Show Files": "",
|
||||
"Show Formatting Toolbar": "",
|
||||
"Show image preview": "",
|
||||
@@ -1665,6 +1668,8 @@
|
||||
"Skill updated successfully": "",
|
||||
"Skills": "",
|
||||
"Skills Access": "",
|
||||
"Skills Public Sharing": "",
|
||||
"Skills Sharing": "",
|
||||
"Skip Cache": "",
|
||||
"Skip the cache and re-run the inference. Defaults to False.": "",
|
||||
"Something went wrong :/": "",
|
||||
|
||||
@@ -98,7 +98,6 @@
|
||||
"Allow Call": "",
|
||||
"Allow Chat Controls": "",
|
||||
"Allow Chat Delete": "",
|
||||
"Allow Chat Deletion": "אפשר מחיקת צ'אט",
|
||||
"Allow Chat Edit": "אפשר עריכת צ'אט",
|
||||
"Allow Chat Export": "",
|
||||
"Allow Chat Params": "",
|
||||
@@ -346,6 +345,7 @@
|
||||
"Completions": "",
|
||||
"Compress Images in Channels": "",
|
||||
"Concurrent Requests": "בקשות בו-זמניות",
|
||||
"Config": "",
|
||||
"Config imported successfully": "",
|
||||
"Configure": "",
|
||||
"Confirm": "",
|
||||
@@ -769,7 +769,7 @@
|
||||
"Export All Chats (All Users)": "ייצוא כל הצ'אטים (כל המשתמשים)",
|
||||
"Export chat (.json)": "",
|
||||
"Export Chats": "ייצוא צ'אטים",
|
||||
"Export Config to JSON File": "",
|
||||
"Export Config": "",
|
||||
"Export Models": "",
|
||||
"Export Presets": "",
|
||||
"Export Prompts": "",
|
||||
@@ -976,7 +976,7 @@
|
||||
"Images": "",
|
||||
"Import": "",
|
||||
"Import Chats": "יבוא צ'אטים",
|
||||
"Import Config from JSON File": "",
|
||||
"Import Config": "",
|
||||
"Import From Link": "",
|
||||
"Import Models": "",
|
||||
"Import Presets": "",
|
||||
@@ -1342,6 +1342,7 @@
|
||||
"Ordered List": "",
|
||||
"Organize your users": "",
|
||||
"Other": "אחר",
|
||||
"Output": "",
|
||||
"OUTPUT": "",
|
||||
"Output format": "",
|
||||
"Output Format": "",
|
||||
@@ -1478,6 +1479,7 @@
|
||||
"Remove image": "",
|
||||
"Remove Model": "הסר מודל",
|
||||
"Rename": "שנה שם",
|
||||
"Render Markdown in Previews": "",
|
||||
"Reorder Models": "",
|
||||
"Reply": "",
|
||||
"Reply in Thread": "",
|
||||
@@ -1639,6 +1641,7 @@
|
||||
"Show": "הצג",
|
||||
"Show \"What's New\" modal on login": "",
|
||||
"Show Admin Details in Account Pending Overlay": "",
|
||||
"Show all ({{COUNT}} characters)": "",
|
||||
"Show Files": "",
|
||||
"Show Formatting Toolbar": "",
|
||||
"Show image preview": "",
|
||||
@@ -1666,6 +1669,8 @@
|
||||
"Skill updated successfully": "",
|
||||
"Skills": "",
|
||||
"Skills Access": "",
|
||||
"Skills Public Sharing": "",
|
||||
"Skills Sharing": "",
|
||||
"Skip Cache": "",
|
||||
"Skip the cache and re-run the inference. Defaults to False.": "",
|
||||
"Something went wrong :/": "",
|
||||
|
||||
@@ -98,7 +98,6 @@
|
||||
"Allow Call": "",
|
||||
"Allow Chat Controls": "",
|
||||
"Allow Chat Delete": "",
|
||||
"Allow Chat Deletion": "चैट हटाने की अनुमति दें",
|
||||
"Allow Chat Edit": "",
|
||||
"Allow Chat Export": "",
|
||||
"Allow Chat Params": "",
|
||||
@@ -346,6 +345,7 @@
|
||||
"Completions": "",
|
||||
"Compress Images in Channels": "",
|
||||
"Concurrent Requests": "समवर्ती अनुरोध",
|
||||
"Config": "",
|
||||
"Config imported successfully": "",
|
||||
"Configure": "",
|
||||
"Confirm": "",
|
||||
@@ -769,7 +769,7 @@
|
||||
"Export All Chats (All Users)": "सभी चैट निर्यात करें (सभी उपयोगकर्ताओं की)",
|
||||
"Export chat (.json)": "",
|
||||
"Export Chats": "चैट निर्यात करें",
|
||||
"Export Config to JSON File": "",
|
||||
"Export Config": "",
|
||||
"Export Models": "",
|
||||
"Export Presets": "",
|
||||
"Export Prompts": "",
|
||||
@@ -976,7 +976,7 @@
|
||||
"Images": "",
|
||||
"Import": "",
|
||||
"Import Chats": "चैट आयात करें",
|
||||
"Import Config from JSON File": "",
|
||||
"Import Config": "",
|
||||
"Import From Link": "",
|
||||
"Import Models": "",
|
||||
"Import Presets": "",
|
||||
@@ -1342,6 +1342,7 @@
|
||||
"Ordered List": "",
|
||||
"Organize your users": "",
|
||||
"Other": "अन्य",
|
||||
"Output": "",
|
||||
"OUTPUT": "",
|
||||
"Output format": "",
|
||||
"Output Format": "",
|
||||
@@ -1478,6 +1479,7 @@
|
||||
"Remove image": "",
|
||||
"Remove Model": "मोडेल हटाएँ",
|
||||
"Rename": "नाम बदलें",
|
||||
"Render Markdown in Previews": "",
|
||||
"Reorder Models": "",
|
||||
"Reply": "",
|
||||
"Reply in Thread": "",
|
||||
@@ -1638,6 +1640,7 @@
|
||||
"Show": "दिखाओ",
|
||||
"Show \"What's New\" modal on login": "",
|
||||
"Show Admin Details in Account Pending Overlay": "",
|
||||
"Show all ({{COUNT}} characters)": "",
|
||||
"Show Files": "",
|
||||
"Show Formatting Toolbar": "",
|
||||
"Show image preview": "",
|
||||
@@ -1665,6 +1668,8 @@
|
||||
"Skill updated successfully": "",
|
||||
"Skills": "",
|
||||
"Skills Access": "",
|
||||
"Skills Public Sharing": "",
|
||||
"Skills Sharing": "",
|
||||
"Skip Cache": "",
|
||||
"Skip the cache and re-run the inference. Defaults to False.": "",
|
||||
"Something went wrong :/": "",
|
||||
|
||||
@@ -98,7 +98,6 @@
|
||||
"Allow Call": "",
|
||||
"Allow Chat Controls": "",
|
||||
"Allow Chat Delete": "",
|
||||
"Allow Chat Deletion": "Dopusti brisanje razgovora",
|
||||
"Allow Chat Edit": "",
|
||||
"Allow Chat Export": "",
|
||||
"Allow Chat Params": "",
|
||||
@@ -346,6 +345,7 @@
|
||||
"Completions": "",
|
||||
"Compress Images in Channels": "",
|
||||
"Concurrent Requests": "Istodobni zahtjevi",
|
||||
"Config": "",
|
||||
"Config imported successfully": "",
|
||||
"Configure": "",
|
||||
"Confirm": "",
|
||||
@@ -769,7 +769,7 @@
|
||||
"Export All Chats (All Users)": "Izvoz svih razgovora (svi korisnici)",
|
||||
"Export chat (.json)": "Izvoz četa (.json)",
|
||||
"Export Chats": "Izvoz razgovora",
|
||||
"Export Config to JSON File": "",
|
||||
"Export Config": "",
|
||||
"Export Models": "",
|
||||
"Export Presets": "",
|
||||
"Export Prompts": "",
|
||||
@@ -976,7 +976,7 @@
|
||||
"Images": "",
|
||||
"Import": "",
|
||||
"Import Chats": "Uvoz razgovora",
|
||||
"Import Config from JSON File": "",
|
||||
"Import Config": "",
|
||||
"Import From Link": "",
|
||||
"Import Models": "",
|
||||
"Import Presets": "",
|
||||
@@ -1342,6 +1342,7 @@
|
||||
"Ordered List": "",
|
||||
"Organize your users": "",
|
||||
"Other": "Ostalo",
|
||||
"Output": "",
|
||||
"OUTPUT": "",
|
||||
"Output format": "",
|
||||
"Output Format": "",
|
||||
@@ -1478,6 +1479,7 @@
|
||||
"Remove image": "",
|
||||
"Remove Model": "Ukloni model",
|
||||
"Rename": "Preimenuj",
|
||||
"Render Markdown in Previews": "",
|
||||
"Reorder Models": "",
|
||||
"Reply": "",
|
||||
"Reply in Thread": "",
|
||||
@@ -1639,6 +1641,7 @@
|
||||
"Show": "Pokaži",
|
||||
"Show \"What's New\" modal on login": "",
|
||||
"Show Admin Details in Account Pending Overlay": "",
|
||||
"Show all ({{COUNT}} characters)": "",
|
||||
"Show Files": "",
|
||||
"Show Formatting Toolbar": "",
|
||||
"Show image preview": "",
|
||||
@@ -1666,6 +1669,8 @@
|
||||
"Skill updated successfully": "",
|
||||
"Skills": "",
|
||||
"Skills Access": "",
|
||||
"Skills Public Sharing": "",
|
||||
"Skills Sharing": "",
|
||||
"Skip Cache": "",
|
||||
"Skip the cache and re-run the inference. Defaults to False.": "",
|
||||
"Something went wrong :/": "",
|
||||
|
||||
@@ -98,7 +98,6 @@
|
||||
"Allow Call": "",
|
||||
"Allow Chat Controls": "Csevegésvezérlők engedélyezése",
|
||||
"Allow Chat Delete": "Csevegés törlésének engedélyezése",
|
||||
"Allow Chat Deletion": "Beszélgetések törlésének engedélyezése",
|
||||
"Allow Chat Edit": "Csevegés szerkesztésének engedélyezése",
|
||||
"Allow Chat Export": "",
|
||||
"Allow Chat Params": "",
|
||||
@@ -346,6 +345,7 @@
|
||||
"Completions": "Kiegészítések",
|
||||
"Compress Images in Channels": "",
|
||||
"Concurrent Requests": "Párhuzamos kérések",
|
||||
"Config": "",
|
||||
"Config imported successfully": "",
|
||||
"Configure": "Konfigurálás",
|
||||
"Confirm": "Megerősítés",
|
||||
@@ -769,7 +769,7 @@
|
||||
"Export All Chats (All Users)": "Minden beszélgetés exportálása (minden felhasználó)",
|
||||
"Export chat (.json)": "Beszélgetés exportálása (.json)",
|
||||
"Export Chats": "Beszélgetések exportálása",
|
||||
"Export Config to JSON File": "Konfiguráció exportálása JSON fájlba",
|
||||
"Export Config": "",
|
||||
"Export Models": "",
|
||||
"Export Presets": "Előre beállított exportálás",
|
||||
"Export Prompts": "",
|
||||
@@ -976,7 +976,7 @@
|
||||
"Images": "",
|
||||
"Import": "",
|
||||
"Import Chats": "Beszélgetések importálása",
|
||||
"Import Config from JSON File": "Konfiguráció importálása JSON fájlból",
|
||||
"Import Config": "",
|
||||
"Import From Link": "",
|
||||
"Import Models": "",
|
||||
"Import Presets": "Előre beállított importálás",
|
||||
@@ -1342,6 +1342,7 @@
|
||||
"Ordered List": "",
|
||||
"Organize your users": "Szervezd meg a felhasználóidat",
|
||||
"Other": "Egyéb",
|
||||
"Output": "",
|
||||
"OUTPUT": "KIMENET",
|
||||
"Output format": "Kimeneti formátum",
|
||||
"Output Format": "",
|
||||
@@ -1478,6 +1479,7 @@
|
||||
"Remove image": "",
|
||||
"Remove Model": "Modell eltávolítása",
|
||||
"Rename": "Átnevezés",
|
||||
"Render Markdown in Previews": "",
|
||||
"Reorder Models": "Modellek átrendezése",
|
||||
"Reply": "",
|
||||
"Reply in Thread": "Válasz szálban",
|
||||
@@ -1638,6 +1640,7 @@
|
||||
"Show": "Mutat",
|
||||
"Show \"What's New\" modal on login": "\"Mi újság\" modal megjelenítése bejelentkezéskor",
|
||||
"Show Admin Details in Account Pending Overlay": "Admin részletek megjelenítése a függő fiók átfedésben",
|
||||
"Show all ({{COUNT}} characters)": "",
|
||||
"Show Files": "",
|
||||
"Show Formatting Toolbar": "",
|
||||
"Show image preview": "",
|
||||
@@ -1665,6 +1668,8 @@
|
||||
"Skill updated successfully": "",
|
||||
"Skills": "",
|
||||
"Skills Access": "",
|
||||
"Skills Public Sharing": "",
|
||||
"Skills Sharing": "",
|
||||
"Skip Cache": "",
|
||||
"Skip the cache and re-run the inference. Defaults to False.": "",
|
||||
"Something went wrong :/": "",
|
||||
|
||||
@@ -98,7 +98,6 @@
|
||||
"Allow Call": "",
|
||||
"Allow Chat Controls": "",
|
||||
"Allow Chat Delete": "",
|
||||
"Allow Chat Deletion": "Izinkan Penghapusan Obrolan",
|
||||
"Allow Chat Edit": "",
|
||||
"Allow Chat Export": "",
|
||||
"Allow Chat Params": "",
|
||||
@@ -346,6 +345,7 @@
|
||||
"Completions": "",
|
||||
"Compress Images in Channels": "",
|
||||
"Concurrent Requests": "Permintaan Bersamaan",
|
||||
"Config": "",
|
||||
"Config imported successfully": "",
|
||||
"Configure": "",
|
||||
"Confirm": "Konfirmasi",
|
||||
@@ -769,7 +769,7 @@
|
||||
"Export All Chats (All Users)": "Ekspor Semua Obrolan (Semua Pengguna)",
|
||||
"Export chat (.json)": "Ekspor obrolan (.json)",
|
||||
"Export Chats": "Ekspor Obrolan",
|
||||
"Export Config to JSON File": "",
|
||||
"Export Config": "",
|
||||
"Export Models": "",
|
||||
"Export Presets": "",
|
||||
"Export Prompts": "",
|
||||
@@ -976,7 +976,7 @@
|
||||
"Images": "",
|
||||
"Import": "",
|
||||
"Import Chats": "Impor Obrolan",
|
||||
"Import Config from JSON File": "",
|
||||
"Import Config": "",
|
||||
"Import From Link": "",
|
||||
"Import Models": "",
|
||||
"Import Presets": "",
|
||||
@@ -1342,6 +1342,7 @@
|
||||
"Ordered List": "",
|
||||
"Organize your users": "",
|
||||
"Other": "Lainnya",
|
||||
"Output": "",
|
||||
"OUTPUT": "",
|
||||
"Output format": "",
|
||||
"Output Format": "",
|
||||
@@ -1478,6 +1479,7 @@
|
||||
"Remove image": "",
|
||||
"Remove Model": "Hapus Model",
|
||||
"Rename": "Ganti nama",
|
||||
"Render Markdown in Previews": "",
|
||||
"Reorder Models": "",
|
||||
"Reply": "",
|
||||
"Reply in Thread": "",
|
||||
@@ -1637,6 +1639,7 @@
|
||||
"Show": "Tampilkan",
|
||||
"Show \"What's New\" modal on login": "",
|
||||
"Show Admin Details in Account Pending Overlay": "Tampilkan Detail Admin di Hamparan Akun Tertunda",
|
||||
"Show all ({{COUNT}} characters)": "",
|
||||
"Show Files": "",
|
||||
"Show Formatting Toolbar": "",
|
||||
"Show image preview": "",
|
||||
@@ -1664,6 +1667,8 @@
|
||||
"Skill updated successfully": "",
|
||||
"Skills": "",
|
||||
"Skills Access": "",
|
||||
"Skills Public Sharing": "",
|
||||
"Skills Sharing": "",
|
||||
"Skip Cache": "",
|
||||
"Skip the cache and re-run the inference. Defaults to False.": "",
|
||||
"Something went wrong :/": "",
|
||||
|
||||
@@ -98,7 +98,6 @@
|
||||
"Allow Call": "Ceadaigh Glao",
|
||||
"Allow Chat Controls": "Ceadaigh Rialuithe Comhrá",
|
||||
"Allow Chat Delete": "Ceadaigh Comhrá a Scriosadh",
|
||||
"Allow Chat Deletion": "Cead Scriosadh Comhrá",
|
||||
"Allow Chat Edit": "Ceadaigh Eagarthóireacht Comhrá",
|
||||
"Allow Chat Export": "Ceadaigh Easpórtáil Comhrá",
|
||||
"Allow Chat Params": "Ceadaigh Paraiméadair Comhrá",
|
||||
@@ -346,6 +345,7 @@
|
||||
"Completions": "Críochnaithe",
|
||||
"Compress Images in Channels": "Comhbhrúigh Íomhánna i gCainéil",
|
||||
"Concurrent Requests": "Iarrataí Comhthéime",
|
||||
"Config": "",
|
||||
"Config imported successfully": "Cumraíocht allmhairithe go rathúil",
|
||||
"Configure": "Cumraigh",
|
||||
"Confirm": "Deimhnigh",
|
||||
@@ -769,7 +769,7 @@
|
||||
"Export All Chats (All Users)": "Easpórtáil gach comhrá (Gach Úsáideoir)",
|
||||
"Export chat (.json)": "Easpórtáil comhrá (.json)",
|
||||
"Export Chats": "Comhráite Easpórtá",
|
||||
"Export Config to JSON File": "Easpórtáil Cumraíocht chuig Comhad JSON",
|
||||
"Export Config": "",
|
||||
"Export Models": "Samhlacha Easpórtála",
|
||||
"Export Presets": "Easpórtáil Gach Comhrá Cartlainne",
|
||||
"Export Prompts": "Leideanna Easpórtála",
|
||||
@@ -976,7 +976,7 @@
|
||||
"Images": "Íomhánna",
|
||||
"Import": "Iompórtáil",
|
||||
"Import Chats": "Comhráite iompórtá",
|
||||
"Import Config from JSON File": "Cumraíocht Iompórtáil ó Chomhad JSON",
|
||||
"Import Config": "",
|
||||
"Import From Link": "Iompórtáil Ó Nasc",
|
||||
"Import Models": "Iompórtáil Samhlacha",
|
||||
"Import Presets": "Réamhshocruithe Iompórtáil",
|
||||
@@ -1342,6 +1342,7 @@
|
||||
"Ordered List": "Liosta Ordaithe",
|
||||
"Organize your users": "Eagraigh do chuid úsáideoirí",
|
||||
"Other": "Eile",
|
||||
"Output": "",
|
||||
"OUTPUT": "ASCHUR",
|
||||
"Output format": "Formáid aschuir",
|
||||
"Output Format": "Formáid Aschuir",
|
||||
@@ -1478,6 +1479,7 @@
|
||||
"Remove image": "Bain íomhá",
|
||||
"Remove Model": "Bain an tSamhail",
|
||||
"Rename": "Athainmnigh",
|
||||
"Render Markdown in Previews": "",
|
||||
"Reorder Models": "Athordú na Samhlacha",
|
||||
"Reply": "Freagra",
|
||||
"Reply in Thread": "Freagra i Snáithe",
|
||||
@@ -1638,6 +1640,7 @@
|
||||
"Show": "Taispeáin",
|
||||
"Show \"What's New\" modal on login": "Taispeáin módúil \"Cad atá Nua\" ar logáil isteach",
|
||||
"Show Admin Details in Account Pending Overlay": "Taispeáin Sonraí Riaracháin sa Chuntas ar Feitheamh Forleagan",
|
||||
"Show all ({{COUNT}} characters)": "",
|
||||
"Show Files": "Taispeáin Comhaid",
|
||||
"Show Formatting Toolbar": "Taispeáin Barra Uirlisí Formáidithe",
|
||||
"Show image preview": "Taispeáin réamhamharc íomhá",
|
||||
@@ -1665,6 +1668,8 @@
|
||||
"Skill updated successfully": "",
|
||||
"Skills": "",
|
||||
"Skills Access": "",
|
||||
"Skills Public Sharing": "",
|
||||
"Skills Sharing": "",
|
||||
"Skip Cache": "Seachain an Taisce",
|
||||
"Skip the cache and re-run the inference. Defaults to False.": "Seachain an taisce agus athrith an tátal. Réamhshocrú Bréagach.",
|
||||
"Something went wrong :/": "Chuaigh rud éigin mícheart :/",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user