Browse Source

Merge pull request #15738 from open-webui/dev

0.6.17
Tim Jaeryang Baek 2 months ago
parent
commit
b249809d2d
100 changed files with 3960 additions and 858 deletions
  1. 2 2
      .github/workflows/format-build-frontend.yaml
  2. 47 0
      CHANGELOG.md
  3. 1 1
      Dockerfile
  4. 53 0
      LICENSE_HISTORY
  5. 24 0
      backend/open_webui/config.py
  6. 16 4
      backend/open_webui/env.py
  7. 2 2
      backend/open_webui/models/folders.py
  8. 53 1
      backend/open_webui/models/groups.py
  9. 12 0
      backend/open_webui/models/users.py
  10. 3 0
      backend/open_webui/retrieval/utils.py
  11. 7 3
      backend/open_webui/routers/audio.py
  12. 14 6
      backend/open_webui/routers/chats.py
  13. 3 1
      backend/open_webui/routers/files.py
  14. 2 2
      backend/open_webui/routers/folders.py
  15. 51 0
      backend/open_webui/routers/groups.py
  16. 9 0
      backend/open_webui/routers/notes.py
  17. 14 1
      backend/open_webui/routers/ollama.py
  18. 11 12
      backend/open_webui/routers/openai.py
  19. 19 7
      backend/open_webui/routers/retrieval.py
  20. 1 1
      backend/open_webui/routers/tasks.py
  21. 2 1
      backend/open_webui/routers/users.py
  22. 34 2
      backend/open_webui/socket/main.py
  23. 793 0
      backend/open_webui/test/util/test_redis.py
  24. 16 3
      backend/open_webui/utils/logger.py
  25. 44 21
      backend/open_webui/utils/middleware.py
  26. 100 4
      backend/open_webui/utils/redis.py
  27. 5 6
      backend/open_webui/utils/response.py
  28. 40 0
      backend/open_webui/utils/telemetry/metrics.py
  29. 1 0
      backend/requirements.txt
  30. 307 262
      package-lock.json
  31. 19 20
      package.json
  32. 7 0
      pyproject.toml
  33. 5 0
      src/app.css
  34. 7 9
      src/lib/apis/folders/index.ts
  35. 1 1
      src/lib/components/admin/Settings/Audio.svelte
  36. 24 8
      src/lib/components/chat/Chat.svelte
  37. 20 0
      src/lib/components/chat/MessageInput.svelte
  38. 3 0
      src/lib/components/chat/Messages.svelte
  39. 2 0
      src/lib/components/chat/Messages/ContentRenderer.svelte
  40. 2 0
      src/lib/components/chat/Messages/Markdown.svelte
  41. 6 11
      src/lib/components/chat/Messages/Markdown/MarkdownInlineTokens.svelte
  42. 33 0
      src/lib/components/chat/Messages/Markdown/MarkdownInlineTokens/CodespanToken.svelte
  43. 19 0
      src/lib/components/chat/Messages/Markdown/MarkdownInlineTokens/TextToken.svelte
  44. 29 3
      src/lib/components/chat/Messages/Markdown/MarkdownTokens.svelte
  45. 3 0
      src/lib/components/chat/Messages/Message.svelte
  46. 2 0
      src/lib/components/chat/Messages/MultiResponseMessages.svelte
  47. 13 3
      src/lib/components/chat/Messages/ResponseMessage.svelte
  48. 40 36
      src/lib/components/chat/Placeholder.svelte
  49. 103 0
      src/lib/components/chat/Placeholder/ChatList.svelte
  50. 0 0
      src/lib/components/chat/Placeholder/FolderKnowledge.svelte
  51. 51 0
      src/lib/components/chat/Placeholder/FolderPlaceholder.svelte
  52. 147 0
      src/lib/components/chat/Placeholder/FolderTitle.svelte
  53. 93 0
      src/lib/components/chat/Settings/Interface.svelte
  54. 1 1
      src/lib/components/chat/Settings/Personalization/ManageModal.svelte
  55. 1 1
      src/lib/components/chat/SettingsModal.svelte
  56. 2 2
      src/lib/components/common/FileItemModal.svelte
  57. 8 0
      src/lib/components/common/Modal.svelte
  58. 165 37
      src/lib/components/common/RichTextInput.svelte
  59. 27 0
      src/lib/components/common/RichTextInput/FormattingButtons.svelte
  60. 197 0
      src/lib/components/common/RichTextInput/Image/image.ts
  61. 5 0
      src/lib/components/common/RichTextInput/Image/index.ts
  62. 19 0
      src/lib/components/icons/AdjustmentsHorizontalOutline.svelte
  63. 20 0
      src/lib/components/icons/ArrowLeftTag.svelte
  64. 20 0
      src/lib/components/icons/ArrowRightTag.svelte
  65. 212 64
      src/lib/components/layout/SearchModal.svelte
  66. 20 8
      src/lib/components/layout/Sidebar.svelte
  67. 2 1
      src/lib/components/layout/Sidebar/Folders/FolderMenu.svelte
  68. 28 11
      src/lib/components/layout/Sidebar/Folders/FolderModal.svelte
  69. 8 8
      src/lib/components/layout/Sidebar/RecursiveFolder.svelte
  70. 4 0
      src/lib/components/layout/Sidebar/SearchInput.svelte
  71. 3 5
      src/lib/components/layout/Sidebar/UserMenu.svelte
  72. 259 117
      src/lib/components/notes/NoteEditor.svelte
  73. 49 21
      src/lib/components/notes/NoteEditor/Chat.svelte
  74. 103 0
      src/lib/components/notes/NoteEditor/Controls.svelte
  75. 0 46
      src/lib/components/notes/NoteEditor/Settings.svelte
  76. 3 1
      src/lib/components/notes/Notes/NoteMenu.svelte
  77. 161 4
      src/lib/components/workspace/Models/Knowledge.svelte
  78. 1 1
      src/lib/components/workspace/Models/ModelEditor.svelte
  79. 4 5
      src/lib/components/workspace/Prompts/PromptEditor.svelte
  80. 14 3
      src/lib/i18n/locales/ar-BH/translation.json
  81. 14 3
      src/lib/i18n/locales/ar/translation.json
  82. 14 3
      src/lib/i18n/locales/bg-BG/translation.json
  83. 14 3
      src/lib/i18n/locales/bn-BD/translation.json
  84. 14 3
      src/lib/i18n/locales/bo-TB/translation.json
  85. 14 3
      src/lib/i18n/locales/ca-ES/translation.json
  86. 14 3
      src/lib/i18n/locales/ceb-PH/translation.json
  87. 14 3
      src/lib/i18n/locales/cs-CZ/translation.json
  88. 14 3
      src/lib/i18n/locales/da-DK/translation.json
  89. 23 12
      src/lib/i18n/locales/de-DE/translation.json
  90. 14 3
      src/lib/i18n/locales/dg-DG/translation.json
  91. 14 3
      src/lib/i18n/locales/el-GR/translation.json
  92. 14 3
      src/lib/i18n/locales/en-GB/translation.json
  93. 14 3
      src/lib/i18n/locales/en-US/translation.json
  94. 14 3
      src/lib/i18n/locales/es-ES/translation.json
  95. 14 3
      src/lib/i18n/locales/et-EE/translation.json
  96. 14 3
      src/lib/i18n/locales/eu-ES/translation.json
  97. 14 3
      src/lib/i18n/locales/fa-IR/translation.json
  98. 14 3
      src/lib/i18n/locales/fi-FI/translation.json
  99. 14 3
      src/lib/i18n/locales/fr-CA/translation.json
  100. 34 23
      src/lib/i18n/locales/fr-FR/translation.json

+ 2 - 2
.github/workflows/format-build-frontend.yaml

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

+ 47 - 0
CHANGELOG.md

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

+ 1 - 1
Dockerfile

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

+ 53 - 0
LICENSE_HISTORY

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

+ 24 - 0
backend/open_webui/config.py

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

+ 16 - 4
backend/open_webui/env.py

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

+ 2 - 2
backend/open_webui/models/folders.py

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

+ 53 - 1
backend/open_webui/models/groups.py

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

+ 12 - 0
backend/open_webui/models/users.py

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

+ 3 - 0
backend/open_webui/retrieval/utils.py

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

+ 7 - 3
backend/open_webui/routers/audio.py

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

+ 14 - 6
backend/open_webui/routers/chats.py

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

+ 3 - 1
backend/open_webui/routers/files.py

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

+ 2 - 2
backend/open_webui/routers/folders.py

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

+ 51 - 0
backend/open_webui/routers/groups.py

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

+ 9 - 0
backend/open_webui/routers/notes.py

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

+ 14 - 1
backend/open_webui/routers/ollama.py

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

+ 11 - 12
backend/open_webui/routers/openai.py

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

+ 19 - 7
backend/open_webui/routers/retrieval.py

@@ -815,7 +815,11 @@ async def update_rag_config(
         f"Updating reranking model: {request.app.state.config.RAG_RERANKING_MODEL} to {form_data.RAG_RERANKING_MODEL}"
     )
     try:
-        request.app.state.config.RAG_RERANKING_MODEL = form_data.RAG_RERANKING_MODEL
+        request.app.state.config.RAG_RERANKING_MODEL = (
+            form_data.RAG_RERANKING_MODEL
+            if form_data.RAG_RERANKING_MODEL is not None
+            else request.app.state.config.RAG_RERANKING_MODEL
+        )
 
         try:
             request.app.state.rf = get_rf(
@@ -2050,11 +2054,13 @@ def query_doc_handler(
                 ),
                 k=form_data.k if form_data.k else request.app.state.config.TOP_K,
                 reranking_function=(
-                    lambda sentences: (
-                        request.app.state.RERANKING_FUNCTION(sentences, user=user)
-                        if request.app.state.RERANKING_FUNCTION
-                        else None
+                    (
+                        lambda sentences: request.app.state.RERANKING_FUNCTION(
+                            sentences, user=user
+                        )
                     )
+                    if request.app.state.RERANKING_FUNCTION
+                    else None
                 ),
                 k_reranker=form_data.k_reranker
                 or request.app.state.config.TOP_K_RERANKER,
@@ -2112,8 +2118,14 @@ def query_collection_handler(
                     query, prefix=prefix, user=user
                 ),
                 k=form_data.k if form_data.k else request.app.state.config.TOP_K,
-                reranking_function=lambda sentences: request.app.state.RERANKING_FUNCTION(
-                    sentences, user=user
+                reranking_function=(
+                    (
+                        lambda sentences: request.app.state.RERANKING_FUNCTION(
+                            sentences, user=user
+                        )
+                    )
+                    if request.app.state.RERANKING_FUNCTION
+                    else None
                 ),
                 k_reranker=form_data.k_reranker
                 or request.app.state.config.TOP_K_RERANKER,

+ 1 - 1
backend/open_webui/routers/tasks.py

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

+ 2 - 1
backend/open_webui/routers/users.py

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

+ 34 - 2
backend/open_webui/socket/main.py

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

+ 793 - 0
backend/open_webui/test/util/test_redis.py

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

+ 16 - 3
backend/open_webui/utils/logger.py

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

+ 44 - 21
backend/open_webui/utils/middleware.py

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

+ 100 - 4
backend/open_webui/utils/redis.py

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

+ 5 - 6
backend/open_webui/utils/response.py

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

+ 40 - 0
backend/open_webui/utils/telemetry/metrics.py

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

+ 1 - 0
backend/requirements.txt

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

+ 307 - 262
package-lock.json

@@ -1,42 +1,40 @@
 {
 	"name": "open-webui",
-	"version": "0.6.16",
+	"version": "0.6.17",
 	"lockfileVersion": 3,
 	"requires": true,
 	"packages": {
 		"": {
 			"name": "open-webui",
-			"version": "0.6.16",
+			"version": "0.6.17",
 			"dependencies": {
 				"@azure/msal-browser": "^4.5.0",
 				"@codemirror/lang-javascript": "^6.2.2",
 				"@codemirror/lang-python": "^6.1.6",
 				"@codemirror/language-data": "^6.5.1",
 				"@codemirror/theme-one-dark": "^6.1.2",
+				"@floating-ui/dom": "^1.7.2",
 				"@huggingface/transformers": "^3.0.0",
 				"@mediapipe/tasks-vision": "^0.10.17",
 				"@pyscript/core": "^0.4.32",
 				"@sveltejs/adapter-node": "^2.0.0",
 				"@sveltejs/svelte-virtual-list": "^3.0.1",
-				"@tiptap/core": "^2.11.9",
-				"@tiptap/extension-bubble-menu": "^2.25.0",
-				"@tiptap/extension-character-count": "^2.25.0",
-				"@tiptap/extension-code-block-lowlight": "^2.11.9",
-				"@tiptap/extension-floating-menu": "^2.25.0",
-				"@tiptap/extension-highlight": "^2.10.0",
-				"@tiptap/extension-history": "^2.25.1",
-				"@tiptap/extension-link": "^2.25.0",
-				"@tiptap/extension-placeholder": "^2.10.0",
-				"@tiptap/extension-table": "^2.12.0",
-				"@tiptap/extension-table-cell": "^2.12.0",
-				"@tiptap/extension-table-header": "^2.12.0",
-				"@tiptap/extension-table-row": "^2.12.0",
-				"@tiptap/extension-task-item": "^2.25.0",
-				"@tiptap/extension-task-list": "^2.25.0",
-				"@tiptap/extension-typography": "^2.10.0",
-				"@tiptap/extension-underline": "^2.25.0",
-				"@tiptap/pm": "^2.11.7",
-				"@tiptap/starter-kit": "^2.10.0",
+				"@tiptap/core": "^3.0.7",
+				"@tiptap/extension-bubble-menu": "^2.26.1",
+				"@tiptap/extension-code-block-lowlight": "^3.0.7",
+				"@tiptap/extension-drag-handle": "^3.0.7",
+				"@tiptap/extension-file-handler": "^3.0.7",
+				"@tiptap/extension-floating-menu": "^2.26.1",
+				"@tiptap/extension-highlight": "^3.0.7",
+				"@tiptap/extension-image": "^3.0.7",
+				"@tiptap/extension-link": "^3.0.7",
+				"@tiptap/extension-list": "^3.0.7",
+				"@tiptap/extension-table": "^3.0.7",
+				"@tiptap/extension-typography": "^3.0.7",
+				"@tiptap/extension-youtube": "^3.0.7",
+				"@tiptap/extensions": "^3.0.7",
+				"@tiptap/pm": "^3.0.7",
+				"@tiptap/starter-kit": "^3.0.7",
 				"@xyflow/svelte": "^0.1.19",
 				"async": "^3.2.5",
 				"bits-ui": "^0.21.15",
@@ -64,6 +62,7 @@
 				"katex": "^0.16.22",
 				"kokoro-js": "^1.1.1",
 				"leaflet": "^1.9.4",
+				"lowlight": "^3.3.0",
 				"marked": "^9.1.0",
 				"mermaid": "^11.6.0",
 				"paneforge": "^0.0.6",
@@ -1216,28 +1215,28 @@
 			}
 		},
 		"node_modules/@floating-ui/core": {
-			"version": "1.7.1",
-			"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.1.tgz",
-			"integrity": "sha512-azI0DrjMMfIug/ExbBaeDVJXcY0a7EPvPjb2xAJPa4HeimBX+Z18HK8QQR3jb6356SnDDdxx+hinMLcJEDdOjw==",
+			"version": "1.7.2",
+			"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.2.tgz",
+			"integrity": "sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==",
 			"license": "MIT",
 			"dependencies": {
-				"@floating-ui/utils": "^0.2.9"
+				"@floating-ui/utils": "^0.2.10"
 			}
 		},
 		"node_modules/@floating-ui/dom": {
-			"version": "1.7.1",
-			"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.1.tgz",
-			"integrity": "sha512-cwsmW/zyw5ltYTUeeYJ60CnQuPqmGwuGVhG9w0PRaRKkAyi38BT5CKrpIbb+jtahSwUl04cWzSx9ZOIxeS6RsQ==",
+			"version": "1.7.2",
+			"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.2.tgz",
+			"integrity": "sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA==",
 			"license": "MIT",
 			"dependencies": {
-				"@floating-ui/core": "^1.7.1",
-				"@floating-ui/utils": "^0.2.9"
+				"@floating-ui/core": "^1.7.2",
+				"@floating-ui/utils": "^0.2.10"
 			}
 		},
 		"node_modules/@floating-ui/utils": {
-			"version": "0.2.9",
-			"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz",
-			"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
+			"version": "0.2.10",
+			"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
+			"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
 			"license": "MIT"
 		},
 		"node_modules/@gulpjs/to-absolute-glob": {
@@ -3109,48 +3108,48 @@
 			}
 		},
 		"node_modules/@tiptap/core": {
-			"version": "2.11.9",
-			"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.11.9.tgz",
-			"integrity": "sha512-UZSxQLLyJst47xep3jlyKM6y1ebZnmvbGsB7njBVjfxf5H+4yFpRJwwNqrBHM/vyU55LCtPChojqaYC1wXLf6g==",
+			"version": "3.0.7",
+			"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.0.7.tgz",
+			"integrity": "sha512-/NC0BbekWzi5sC+s7gRrGIv33cUfuiZUG5DWx8TNedA6b6aTFPHUe+2wKRPaPQ0pfGdOWU0nsOkboUJ9dAjl4g==",
 			"license": "MIT",
 			"funding": {
 				"type": "github",
 				"url": "https://github.com/sponsors/ueberdosis"
 			},
 			"peerDependencies": {
-				"@tiptap/pm": "^2.7.0"
+				"@tiptap/pm": "^3.0.7"
 			}
 		},
 		"node_modules/@tiptap/extension-blockquote": {
-			"version": "2.10.0",
-			"resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-2.10.0.tgz",
-			"integrity": "sha512-6Xmfo2lpfIRcbfkLD/NGX4YgQqfgAbu6XaZQZf5oGtHLPTrz4D7Mw20GgNBHzae2XwUCwLMt6zXOkBgU/LnlZg==",
+			"version": "3.0.7",
+			"resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.0.7.tgz",
+			"integrity": "sha512-bYJ7r4hYcBZ7GI0LSV0Oxb9rmy/qb0idAf/osvflG2r1tf5CsiW5NYAqlOYAsIVA2OCwXELDlRGCgeKBQ26Kyw==",
 			"license": "MIT",
 			"funding": {
 				"type": "github",
 				"url": "https://github.com/sponsors/ueberdosis"
 			},
 			"peerDependencies": {
-				"@tiptap/core": "^2.7.0"
+				"@tiptap/core": "^3.0.7"
 			}
 		},
 		"node_modules/@tiptap/extension-bold": {
-			"version": "2.10.0",
-			"resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-2.10.0.tgz",
-			"integrity": "sha512-1wL8UI1Aii0u2cbDEvwyqsZb2pgBt8HLJdsIax/ELoF2tKCD5821nElqTGLBBg4pUGPa0ru9ZemuL8GdXZp3Qg==",
+			"version": "3.0.7",
+			"resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.0.7.tgz",
+			"integrity": "sha512-CQG07yvrIsScLe5NplAuCkVh0sd97Udv1clAGbqfzeV8YfzpV3M7J/Vb09pWyovx3SjDqfsZpkr3RemeKEPY9Q==",
 			"license": "MIT",
 			"funding": {
 				"type": "github",
 				"url": "https://github.com/sponsors/ueberdosis"
 			},
 			"peerDependencies": {
-				"@tiptap/core": "^2.7.0"
+				"@tiptap/core": "^3.0.7"
 			}
 		},
 		"node_modules/@tiptap/extension-bubble-menu": {
-			"version": "2.25.0",
-			"resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.25.0.tgz",
-			"integrity": "sha512-BnbfQWRXJDDy9/x/0Atu2Nka5ZAMyXLDFqzSLMAXqXSQcG6CZRTSNRgOCnjpda6Hq2yCtq7l/YEoXkbHT1ZZdQ==",
+			"version": "2.26.1",
+			"resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.26.1.tgz",
+			"integrity": "sha512-oHevUcZbTMFOTpdCEo4YEDe044MB4P1ZrWyML8CGe5tnnKdlI9BN03AXpI1mEEa5CA3H1/eEckXx8EiCgYwQ3Q==",
 			"license": "MIT",
 			"dependencies": {
 				"tippy.js": "^6.3.7"
@@ -3165,107 +3164,144 @@
 			}
 		},
 		"node_modules/@tiptap/extension-bullet-list": {
-			"version": "2.10.0",
-			"resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-2.10.0.tgz",
-			"integrity": "sha512-Cl+DGu6D3SgF/hlKUDNet3gaZFy6cPEonOOkHwzXoybDXXdddFbaTvt9MLkBRUR3ldksXuVRP2/LwZsK5WyxJQ==",
+			"version": "3.0.7",
+			"resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.0.7.tgz",
+			"integrity": "sha512-9gPc3Tw2Bw7qKLbyW0s05YntE77127pOXQXcclB4I3MXAuz/K03f+DGuSRhOq9K2Oo86BPHdL5I9Ap9cmuS0Tg==",
 			"license": "MIT",
 			"funding": {
 				"type": "github",
 				"url": "https://github.com/sponsors/ueberdosis"
 			},
 			"peerDependencies": {
-				"@tiptap/core": "^2.7.0"
+				"@tiptap/extension-list": "^3.0.7"
 			}
 		},
-		"node_modules/@tiptap/extension-character-count": {
-			"version": "2.25.0",
-			"resolved": "https://registry.npmjs.org/@tiptap/extension-character-count/-/extension-character-count-2.25.0.tgz",
-			"integrity": "sha512-F+4DxJFptbX3oioqNwS38zOTi6gH9CumV/ISeOIvr4ao7Iija3tNonGDsHhxD05njjbYNIp1OKsxtnzbWukgMA==",
+		"node_modules/@tiptap/extension-code": {
+			"version": "3.0.7",
+			"resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.0.7.tgz",
+			"integrity": "sha512-6wdUqtXbnIuyKR7xteF2UCnsW2dLNtBKxWvAiOweA7L41HYvburh/tjbkffkNc5KP2XsKzdGbygpunwJMPj6+A==",
 			"license": "MIT",
 			"funding": {
 				"type": "github",
 				"url": "https://github.com/sponsors/ueberdosis"
 			},
 			"peerDependencies": {
-				"@tiptap/core": "^2.7.0",
-				"@tiptap/pm": "^2.7.0"
+				"@tiptap/core": "^3.0.7"
 			}
 		},
-		"node_modules/@tiptap/extension-code": {
-			"version": "2.10.0",
-			"resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-2.10.0.tgz",
-			"integrity": "sha512-8JznKG1Jmv8gJezZGPoka8oRmfrcAAnMEOeMpKXjwMrIbQ6QynTZpqMGGVL1kfkZlLV84PYm+CGjGgjSsT4iZw==",
+		"node_modules/@tiptap/extension-code-block": {
+			"version": "3.0.7",
+			"resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.0.7.tgz",
+			"integrity": "sha512-WifMv7N1G1Fnd2oZ+g80FjBpV/eI/fxHKCK3hw03l8LoWgeFaU/6LC93qTV6idkfia3YwiA6WnuyOqlI0FSZ9A==",
 			"license": "MIT",
 			"funding": {
 				"type": "github",
 				"url": "https://github.com/sponsors/ueberdosis"
 			},
 			"peerDependencies": {
-				"@tiptap/core": "^2.7.0"
+				"@tiptap/core": "^3.0.7",
+				"@tiptap/pm": "^3.0.7"
 			}
 		},
-		"node_modules/@tiptap/extension-code-block": {
-			"version": "2.10.0",
-			"resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-2.10.0.tgz",
-			"integrity": "sha512-QH+LP7L1s1EJlrDFnfgOP0q+Siqt0Zbkx4ICMcUGvEsycl53Ti8P0DRW7fAjRISdTCItuWJYvtmiYY7O3rYb+Q==",
+		"node_modules/@tiptap/extension-code-block-lowlight": {
+			"version": "3.0.7",
+			"resolved": "https://registry.npmjs.org/@tiptap/extension-code-block-lowlight/-/extension-code-block-lowlight-3.0.7.tgz",
+			"integrity": "sha512-y1sHjzxpYqIKikdT5y5ajCOw4hDIPGjPpIBP7x7iw7jyt8a/w/bI8ozUk4epLBpgOvvAwmdIqi7eV7ORMvQaGQ==",
 			"license": "MIT",
 			"funding": {
 				"type": "github",
 				"url": "https://github.com/sponsors/ueberdosis"
 			},
 			"peerDependencies": {
-				"@tiptap/core": "^2.7.0",
-				"@tiptap/pm": "^2.7.0"
+				"@tiptap/core": "^3.0.7",
+				"@tiptap/extension-code-block": "^3.0.7",
+				"@tiptap/pm": "^3.0.7",
+				"highlight.js": "^11",
+				"lowlight": "^2 || ^3"
 			}
 		},
-		"node_modules/@tiptap/extension-code-block-lowlight": {
-			"version": "2.11.9",
-			"resolved": "https://registry.npmjs.org/@tiptap/extension-code-block-lowlight/-/extension-code-block-lowlight-2.11.9.tgz",
-			"integrity": "sha512-bB8N59A2aU18/ieyKRZAI0J0xyimmUckYePqBkUX8HFnq8yf9HsM0NPFpqZdK0eqjnZYCXcNwAI3YluLsHuutw==",
+		"node_modules/@tiptap/extension-collaboration": {
+			"version": "3.0.7",
+			"resolved": "https://registry.npmjs.org/@tiptap/extension-collaboration/-/extension-collaboration-3.0.7.tgz",
+			"integrity": "sha512-so59vQCAS1vy6k86byk96fYvAPM5w8u8/Yp3jKF1LPi9LH4wzS4hGnOP/dEbedxPU48an9WB1lSOczSKPECJaQ==",
 			"license": "MIT",
+			"peer": true,
 			"funding": {
 				"type": "github",
 				"url": "https://github.com/sponsors/ueberdosis"
 			},
 			"peerDependencies": {
-				"@tiptap/core": "^2.7.0",
-				"@tiptap/extension-code-block": "^2.7.0",
-				"@tiptap/pm": "^2.7.0",
-				"highlight.js": "^11",
-				"lowlight": "^2 || ^3"
+				"@tiptap/core": "^3.0.7",
+				"@tiptap/pm": "^3.0.7",
+				"@tiptap/y-tiptap": "^3.0.0-beta.3",
+				"yjs": "^13"
 			}
 		},
 		"node_modules/@tiptap/extension-document": {
-			"version": "2.10.0",
-			"resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-2.10.0.tgz",
-			"integrity": "sha512-vseMW3EKiQAPgdbN48Y8F0nRqWhhrAo9DLacAfP7tu0x3uv44uotNjDBtAgp5QmJmqQVyrEdkLSZaU5vFzduhQ==",
+			"version": "3.0.7",
+			"resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.0.7.tgz",
+			"integrity": "sha512-HJg1nPPZ9fv5oEMwpONeIfT0FjTrgNGuGAat/hgcBi/R2GUNir2/PM/3d6y8QtkR/EgkgcFakCc9azySXLmyUQ==",
+			"license": "MIT",
+			"funding": {
+				"type": "github",
+				"url": "https://github.com/sponsors/ueberdosis"
+			},
+			"peerDependencies": {
+				"@tiptap/core": "^3.0.7"
+			}
+		},
+		"node_modules/@tiptap/extension-drag-handle": {
+			"version": "3.0.7",
+			"resolved": "https://registry.npmjs.org/@tiptap/extension-drag-handle/-/extension-drag-handle-3.0.7.tgz",
+			"integrity": "sha512-rm8+0kPz5C5JTp4f1QY61Qd5d7zlJAxLeJtOvgC9RCnrNG1F7LCsmOkvy5fsU6Qk2YCCYOiSSMC4S4HKPrUJhw==",
 			"license": "MIT",
+			"dependencies": {
+				"@floating-ui/dom": "^1.6.13"
+			},
 			"funding": {
 				"type": "github",
 				"url": "https://github.com/sponsors/ueberdosis"
 			},
 			"peerDependencies": {
-				"@tiptap/core": "^2.7.0"
+				"@tiptap/core": "^3.0.7",
+				"@tiptap/extension-collaboration": "^3.0.7",
+				"@tiptap/extension-node-range": "^3.0.7",
+				"@tiptap/pm": "^3.0.7",
+				"@tiptap/y-tiptap": "^3.0.0-beta.3"
 			}
 		},
 		"node_modules/@tiptap/extension-dropcursor": {
-			"version": "2.10.0",
-			"resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-2.10.0.tgz",
-			"integrity": "sha512-tifxp/a3NxTjLAuYBx9XAwVo4MSDoY/mQ8E18QtuXj0vuieCFxd8Bkyre0otubIAAQePXLTVGQoxPrKmMAa+Jg==",
+			"version": "3.0.7",
+			"resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.0.7.tgz",
+			"integrity": "sha512-0i2XWdRgYbj6PEPC+pMcGiF/hwg0jl+MavPt1733qWzoDqMEls9cEBTQ9S4HS0TI/jbN/kNavTQ5LlI33kWrww==",
 			"license": "MIT",
 			"funding": {
 				"type": "github",
 				"url": "https://github.com/sponsors/ueberdosis"
 			},
 			"peerDependencies": {
-				"@tiptap/core": "^2.7.0",
-				"@tiptap/pm": "^2.7.0"
+				"@tiptap/extensions": "^3.0.7"
+			}
+		},
+		"node_modules/@tiptap/extension-file-handler": {
+			"version": "3.0.7",
+			"resolved": "https://registry.npmjs.org/@tiptap/extension-file-handler/-/extension-file-handler-3.0.7.tgz",
+			"integrity": "sha512-eNJOqLaM91erqm6W7k+ocG09fuiVI4B+adWhv97sFim9TboF0sEIWEYdl68z06N1/+tXv6w8S4zUYQCOzxlVtw==",
+			"license": "MIT",
+			"funding": {
+				"type": "github",
+				"url": "https://github.com/sponsors/ueberdosis"
+			},
+			"peerDependencies": {
+				"@tiptap/core": "^3.0.7",
+				"@tiptap/extension-text-style": "^3.0.7",
+				"@tiptap/pm": "^3.0.7"
 			}
 		},
 		"node_modules/@tiptap/extension-floating-menu": {
-			"version": "2.25.0",
-			"resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-2.25.0.tgz",
-			"integrity": "sha512-hPZ5SNpI14smTz4GpWQXTnxmeICINYiABSgXcsU5V66tik9OtxKwoCSR/gpU35esaAFUVRdjW7+sGkACLZD5AQ==",
+			"version": "2.26.1",
+			"resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-2.26.1.tgz",
+			"integrity": "sha512-OJF+H6qhQogVTMedAGSWuoL1RPe3LZYXONuFCVyzHnvvMpK+BP1vm180E2zDNFnn/DVA+FOrzNGpZW7YjoFH1w==",
 			"license": "MIT",
 			"dependencies": {
 				"tippy.js": "^6.3.7"
@@ -3280,103 +3316,101 @@
 			}
 		},
 		"node_modules/@tiptap/extension-gapcursor": {
-			"version": "2.10.0",
-			"resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-2.10.0.tgz",
-			"integrity": "sha512-GViEnSnEBE74k7SYdXrQ4aXlKmWkrd9awdj/TgDSORgpZ4Dfyqtn+ENIWWby4NhL+BPM9P5hGCjkQXZsi6JKOw==",
+			"version": "3.0.7",
+			"resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.0.7.tgz",
+			"integrity": "sha512-F4ERd5r59WHbY0ALBbrJ/2z9dl+7VSmsMV/ZkzTgq0TZV9KKz3SsCFcCdIZEYzRCEp69/yYtkTofN10xIa+J6A==",
 			"license": "MIT",
 			"funding": {
 				"type": "github",
 				"url": "https://github.com/sponsors/ueberdosis"
 			},
 			"peerDependencies": {
-				"@tiptap/core": "^2.7.0",
-				"@tiptap/pm": "^2.7.0"
+				"@tiptap/extensions": "^3.0.7"
 			}
 		},
 		"node_modules/@tiptap/extension-hard-break": {
-			"version": "2.10.0",
-			"resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-2.10.0.tgz",
-			"integrity": "sha512-NL/xPYUhhvQyCnOO5Yn+BlBOMLC1ru32nw7ox12TShGmaeKBrnV0DhzBRkyJU0MqCS26oWjieNPxfu0lR3oMSA==",
+			"version": "3.0.7",
+			"resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.0.7.tgz",
+			"integrity": "sha512-OWrFrKp9PDs9nKJRmyPX22YoscqmoW25VZYeUfvNcAYtI84xYz871s1JmLZkpxqOyI9TafUADFiaRISDnX5EcA==",
 			"license": "MIT",
 			"funding": {
 				"type": "github",
 				"url": "https://github.com/sponsors/ueberdosis"
 			},
 			"peerDependencies": {
-				"@tiptap/core": "^2.7.0"
+				"@tiptap/core": "^3.0.7"
 			}
 		},
 		"node_modules/@tiptap/extension-heading": {
-			"version": "2.10.0",
-			"resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-2.10.0.tgz",
-			"integrity": "sha512-x2Uj5wrAHFaUdlChwLoQVmWtzZCuNyJpBRA19kA4idWL5z+6cIrUWepvwVBxA8ou6ictbzWW15o+blKtW7DlqA==",
+			"version": "3.0.7",
+			"resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.0.7.tgz",
+			"integrity": "sha512-uS7fFcilFuzKEvhUgndELqlGweD+nZeLOb6oqUE5hM49vECjM7qVjVQnlhV+MH2W1w8eD08cn1lu6lDxaMOe5w==",
 			"license": "MIT",
 			"funding": {
 				"type": "github",
 				"url": "https://github.com/sponsors/ueberdosis"
 			},
 			"peerDependencies": {
-				"@tiptap/core": "^2.7.0"
+				"@tiptap/core": "^3.0.7"
 			}
 		},
 		"node_modules/@tiptap/extension-highlight": {
-			"version": "2.10.0",
-			"resolved": "https://registry.npmjs.org/@tiptap/extension-highlight/-/extension-highlight-2.10.0.tgz",
-			"integrity": "sha512-HU8UuKU7ljlzNn7jg29pM8QtIX7QvePcBjcWAt6K3qVwF1cbBNguIjKRY2rmoonU2nu8I6GknQNgV847kZifCQ==",
+			"version": "3.0.7",
+			"resolved": "https://registry.npmjs.org/@tiptap/extension-highlight/-/extension-highlight-3.0.7.tgz",
+			"integrity": "sha512-3oIRuXAg7l9+VPIMwHycXcqtZ7XJcC5vnLhPAQXIesYun6L9EoXmQox0225z8jpPG70N8zfl+YSd4qjsTMPaAg==",
 			"license": "MIT",
 			"funding": {
 				"type": "github",
 				"url": "https://github.com/sponsors/ueberdosis"
 			},
 			"peerDependencies": {
-				"@tiptap/core": "^2.7.0"
+				"@tiptap/core": "^3.0.7"
 			}
 		},
-		"node_modules/@tiptap/extension-history": {
-			"version": "2.25.1",
-			"resolved": "https://registry.npmjs.org/@tiptap/extension-history/-/extension-history-2.25.1.tgz",
-			"integrity": "sha512-ZoxxOAObk1U8H3d+XEG0MjccJN0ViGIKEZqnLUSswmVweYPdkJG2WF2pEif9hpwJONslvLTKa+f8jwK5LEnJLQ==",
+		"node_modules/@tiptap/extension-horizontal-rule": {
+			"version": "3.0.7",
+			"resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.0.7.tgz",
+			"integrity": "sha512-m0r4tzfVX3r0ZD7uvDf/GAiVr7lJjYwhZHC+M+JMhYXVI6eB9OXXzhdOIsw9W5QcmhCBaqU+VuPKUusTn4TKLg==",
 			"license": "MIT",
 			"funding": {
 				"type": "github",
 				"url": "https://github.com/sponsors/ueberdosis"
 			},
 			"peerDependencies": {
-				"@tiptap/core": "^2.7.0",
-				"@tiptap/pm": "^2.7.0"
+				"@tiptap/core": "^3.0.7",
+				"@tiptap/pm": "^3.0.7"
 			}
 		},
-		"node_modules/@tiptap/extension-horizontal-rule": {
-			"version": "2.10.0",
-			"resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-2.10.0.tgz",
-			"integrity": "sha512-el1SzI/x/h4HW8UltxJlyMSrRsO55ypKPLQHJC9h7F6kTTR31fJUzQa3AeTFrZvXS0kNHIFRpAMstw+N0L5TYg==",
+		"node_modules/@tiptap/extension-image": {
+			"version": "3.0.7",
+			"resolved": "https://registry.npmjs.org/@tiptap/extension-image/-/extension-image-3.0.7.tgz",
+			"integrity": "sha512-hs6TiSmefwvAqxwhy4+ZFCbmAXiAeWq4v5Zd65kQ7dvN7epeV0NM7ME5su/oscQgoKvNAy1r/4sJVaTnHomYMQ==",
 			"license": "MIT",
 			"funding": {
 				"type": "github",
 				"url": "https://github.com/sponsors/ueberdosis"
 			},
 			"peerDependencies": {
-				"@tiptap/core": "^2.7.0",
-				"@tiptap/pm": "^2.7.0"
+				"@tiptap/core": "^3.0.7"
 			}
 		},
 		"node_modules/@tiptap/extension-italic": {
-			"version": "2.10.0",
-			"resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-2.10.0.tgz",
-			"integrity": "sha512-MqPYbHAEeO8QBvZRIkF4J2OTf/uiUPzUiXGLJ50w1ozfMBIw1txMvfR3g2cpwfvZlcOgYTgy7M0Oq00nQz5eXg==",
+			"version": "3.0.7",
+			"resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.0.7.tgz",
+			"integrity": "sha512-L05cehSOd7iZWI/igPb90TgQ6RKk2UuuYdatmXff3QUJpYPYct6abcrMb+CeFKJqE9vaXy46dCQkOuPW+bFwkA==",
 			"license": "MIT",
 			"funding": {
 				"type": "github",
 				"url": "https://github.com/sponsors/ueberdosis"
 			},
 			"peerDependencies": {
-				"@tiptap/core": "^2.7.0"
+				"@tiptap/core": "^3.0.7"
 			}
 		},
 		"node_modules/@tiptap/extension-link": {
-			"version": "2.25.0",
-			"resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-2.25.0.tgz",
-			"integrity": "sha512-jNd+1Fd7wiIbxlS51weBzyDtBEBSVzW0cgzdwOzBYQtPJueRyXNNVERksyinDuVgcfvEWgmNZUylgzu7mehnEg==",
+			"version": "3.0.7",
+			"resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.0.7.tgz",
+			"integrity": "sha512-e53MddBSVKpxxQ2JmHfyZQ2VBLwqlZxqwn0DQHFMXyCKTzpdUC0DOtkvrY7OVz6HA3yz29qR+qquQxIxcDPrfg==",
 			"license": "MIT",
 			"dependencies": {
 				"linkifyjs": "^4.2.0"
@@ -3386,215 +3420,205 @@
 				"url": "https://github.com/sponsors/ueberdosis"
 			},
 			"peerDependencies": {
-				"@tiptap/core": "^2.7.0",
-				"@tiptap/pm": "^2.7.0"
-			}
-		},
-		"node_modules/@tiptap/extension-list-item": {
-			"version": "2.10.0",
-			"resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-2.10.0.tgz",
-			"integrity": "sha512-BxC6NNHd2xcC+mk5hpYWURUdj/mRz6TGFwH5CsyrUXPxApx0+V+EPHaAgdpu8dr+jtTEzjXF62V6e2JmOAPimg==",
-			"license": "MIT",
-			"funding": {
-				"type": "github",
-				"url": "https://github.com/sponsors/ueberdosis"
-			},
-			"peerDependencies": {
-				"@tiptap/core": "^2.7.0"
+				"@tiptap/core": "^3.0.7",
+				"@tiptap/pm": "^3.0.7"
 			}
 		},
-		"node_modules/@tiptap/extension-ordered-list": {
-			"version": "2.10.0",
-			"resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-2.10.0.tgz",
-			"integrity": "sha512-jsK+mvzs7HmxQuQOU3HgIga+v7zUbQlmSP4/danusqUihJ+lc1n0frDCIkVvJrnSB3FChvNgT6ZEA14HOhdJzg==",
+		"node_modules/@tiptap/extension-list": {
+			"version": "3.0.7",
+			"resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.0.7.tgz",
+			"integrity": "sha512-rwu5dXRO0YLyxndMHI17PoxK0x0ZaMZKRZflqOy8fSnXNwd3Tdy8/6a9tsmpgO38kOZEYuvMVaeB7J/+UeBVLg==",
 			"license": "MIT",
 			"funding": {
 				"type": "github",
 				"url": "https://github.com/sponsors/ueberdosis"
 			},
 			"peerDependencies": {
-				"@tiptap/core": "^2.7.0"
+				"@tiptap/core": "^3.0.7",
+				"@tiptap/pm": "^3.0.7"
 			}
 		},
-		"node_modules/@tiptap/extension-paragraph": {
-			"version": "2.10.0",
-			"resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-2.10.0.tgz",
-			"integrity": "sha512-4LUkVaJYjNdNZ7QOX6TRcA+m7oCtyrLGk49G22wl7XcPBkQPILP1mCUCU4f41bhjfhCgK5PPWP63kMtD+cEACg==",
+		"node_modules/@tiptap/extension-list-item": {
+			"version": "3.0.7",
+			"resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.0.7.tgz",
+			"integrity": "sha512-QfW+dtukl5v6oOA1n4wtAYev5yY78nqc2O8jHGZD18xhqNVerh2xBVIH9wOGHPz4q5Em2Ju7xbqXYl0vg2De+w==",
 			"license": "MIT",
 			"funding": {
 				"type": "github",
 				"url": "https://github.com/sponsors/ueberdosis"
 			},
 			"peerDependencies": {
-				"@tiptap/core": "^2.7.0"
+				"@tiptap/extension-list": "^3.0.7"
 			}
 		},
-		"node_modules/@tiptap/extension-placeholder": {
-			"version": "2.10.0",
-			"resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-2.10.0.tgz",
-			"integrity": "sha512-1o6azk2plgYAFgMrV3prnBb1NZjl2V1T3wwnH4n3/h9z9lJ0v5BBAk9r+TRYSrcdXknwwHAWFYnQe6dc9buG2g==",
+		"node_modules/@tiptap/extension-list-keymap": {
+			"version": "3.0.7",
+			"resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.0.7.tgz",
+			"integrity": "sha512-KJWXsyHU8E6SGmlZMHNjSg+XrkmCncJT2l5QGEjTUjlhqwulu+4psTDRio9tCdtepiasTL7qEekGWAhz9wEgzQ==",
 			"license": "MIT",
 			"funding": {
 				"type": "github",
 				"url": "https://github.com/sponsors/ueberdosis"
 			},
 			"peerDependencies": {
-				"@tiptap/core": "^2.7.0",
-				"@tiptap/pm": "^2.7.0"
+				"@tiptap/extension-list": "^3.0.7"
 			}
 		},
-		"node_modules/@tiptap/extension-strike": {
-			"version": "2.10.0",
-			"resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-2.10.0.tgz",
-			"integrity": "sha512-SxApLJMQkxnmPGR3lwaskvLK61yI+Bu9hGZGdwMZqNh6o3LoDOxDaXjHD5joeMYQiqQrBE9zg46506MsXtrU7Q==",
+		"node_modules/@tiptap/extension-node-range": {
+			"version": "3.0.7",
+			"resolved": "https://registry.npmjs.org/@tiptap/extension-node-range/-/extension-node-range-3.0.7.tgz",
+			"integrity": "sha512-cHViNqtOUD9CLJxEj28rcj8tb8RYQZ7kwmtSvIye84Y3MJIzigRm4IUBNNOYnZfq5YAZIR97WKcJeFz3EU1VPg==",
 			"license": "MIT",
+			"peer": true,
 			"funding": {
 				"type": "github",
 				"url": "https://github.com/sponsors/ueberdosis"
 			},
 			"peerDependencies": {
-				"@tiptap/core": "^2.7.0"
+				"@tiptap/core": "^3.0.7",
+				"@tiptap/pm": "^3.0.7"
 			}
 		},
-		"node_modules/@tiptap/extension-table": {
-			"version": "2.12.0",
-			"resolved": "https://registry.npmjs.org/@tiptap/extension-table/-/extension-table-2.12.0.tgz",
-			"integrity": "sha512-tT3IbbBal0vPQ1Bc/3Xl+tmqqZQCYWxnycBPl/WZBqhd57DWzfJqRPESwCGUIJgjOtTnipy/ulvj0FxHi1j9JA==",
+		"node_modules/@tiptap/extension-ordered-list": {
+			"version": "3.0.7",
+			"resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.0.7.tgz",
+			"integrity": "sha512-F/cbG0vt1cjkoJ4A65E6vpZQizZwnE4gJHKAw3ymDdCoZKYaO4OV1UTo98W/jgryORy/HLO12+hogsRvgRvK9Q==",
 			"license": "MIT",
 			"funding": {
 				"type": "github",
 				"url": "https://github.com/sponsors/ueberdosis"
 			},
 			"peerDependencies": {
-				"@tiptap/core": "^2.7.0",
-				"@tiptap/pm": "^2.7.0"
+				"@tiptap/extension-list": "^3.0.7"
 			}
 		},
-		"node_modules/@tiptap/extension-table-cell": {
-			"version": "2.12.0",
-			"resolved": "https://registry.npmjs.org/@tiptap/extension-table-cell/-/extension-table-cell-2.12.0.tgz",
-			"integrity": "sha512-8i35uCkmkSiQxMiZ+DLgT/wj24P5U/Zo3jr1e0tMAAMG7sRO1MljjLmkpV8WCdBo0xoRqzkz4J7Nkq+DtzZv9Q==",
+		"node_modules/@tiptap/extension-paragraph": {
+			"version": "3.0.7",
+			"resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.0.7.tgz",
+			"integrity": "sha512-1lp+/CbYmm1ZnR6CNlreUIWCNQk0cBzLVgS5R8SKfVyYaXo11qQq6Yq8URLhpuge4yXkPGMhClwCLzJ9D9R+eg==",
 			"license": "MIT",
 			"funding": {
 				"type": "github",
 				"url": "https://github.com/sponsors/ueberdosis"
 			},
 			"peerDependencies": {
-				"@tiptap/core": "^2.7.0"
+				"@tiptap/core": "^3.0.7"
 			}
 		},
-		"node_modules/@tiptap/extension-table-header": {
-			"version": "2.12.0",
-			"resolved": "https://registry.npmjs.org/@tiptap/extension-table-header/-/extension-table-header-2.12.0.tgz",
-			"integrity": "sha512-gRKEsy13KKLpg9RxyPeUGqh4BRFSJ2Bc2KQP1ldhef6CPRYHCbGycxXCVQ5aAb7Mhpo54L+AAkmAv1iMHUTflw==",
+		"node_modules/@tiptap/extension-strike": {
+			"version": "3.0.7",
+			"resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.0.7.tgz",
+			"integrity": "sha512-WUCd5CMgS6pg0ZGKXsaxVrnEvO/h6XUehebL0yggAsRKSoGERInR2iLfhU4p1f4zk0cD3ydNLJdqZu0H/MIABw==",
 			"license": "MIT",
 			"funding": {
 				"type": "github",
 				"url": "https://github.com/sponsors/ueberdosis"
 			},
 			"peerDependencies": {
-				"@tiptap/core": "^2.7.0"
+				"@tiptap/core": "^3.0.7"
 			}
 		},
-		"node_modules/@tiptap/extension-table-row": {
-			"version": "2.12.0",
-			"resolved": "https://registry.npmjs.org/@tiptap/extension-table-row/-/extension-table-row-2.12.0.tgz",
-			"integrity": "sha512-AEW/Zl9V0IoaYDBLMhF5lVl0xgoIJs3IuKCsIYxGDlxBfTVFC6PfQzvuy296CMjO5ZcZ0xalVipPV9ggsMRD+w==",
+		"node_modules/@tiptap/extension-table": {
+			"version": "3.0.7",
+			"resolved": "https://registry.npmjs.org/@tiptap/extension-table/-/extension-table-3.0.7.tgz",
+			"integrity": "sha512-S4tvIgagzWnvXLHfltXucgS9TlBwPcQTjQR4llbxmKHAQM4+e77+NGcXXDcQ7E1TdAp3Tk8xRGerGIP7kjCFRA==",
 			"license": "MIT",
 			"funding": {
 				"type": "github",
 				"url": "https://github.com/sponsors/ueberdosis"
 			},
 			"peerDependencies": {
-				"@tiptap/core": "^2.7.0"
+				"@tiptap/core": "^3.0.7",
+				"@tiptap/pm": "^3.0.7"
 			}
 		},
-		"node_modules/@tiptap/extension-task-item": {
-			"version": "2.25.0",
-			"resolved": "https://registry.npmjs.org/@tiptap/extension-task-item/-/extension-task-item-2.25.0.tgz",
-			"integrity": "sha512-8F7Z7jbsyGrPLHQCn+n39zdqIgxwR1kJ1nL5ZwhEW3ZhJgkFF0WMJSv36mwIJwL08p8um/c6g72AYB/e8CD7eA==",
+		"node_modules/@tiptap/extension-text": {
+			"version": "3.0.7",
+			"resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.0.7.tgz",
+			"integrity": "sha512-yf5dNcPLB5SbQ0cQq8qyjiMj9khx4Y4EJoyrDSAok/9zYM3ULqwTPkTSZ2eW6VX/grJeyBVleeBHk1PjJ7NiVw==",
 			"license": "MIT",
 			"funding": {
 				"type": "github",
 				"url": "https://github.com/sponsors/ueberdosis"
 			},
 			"peerDependencies": {
-				"@tiptap/core": "^2.7.0",
-				"@tiptap/pm": "^2.7.0"
+				"@tiptap/core": "^3.0.7"
 			}
 		},
-		"node_modules/@tiptap/extension-task-list": {
-			"version": "2.25.0",
-			"resolved": "https://registry.npmjs.org/@tiptap/extension-task-list/-/extension-task-list-2.25.0.tgz",
-			"integrity": "sha512-2mASqp8MJ0dyc1OK6c8P7m/zwoVDv8PV+XsRR9O3tpIz/zjUVrOl0W4IndjUPBMa7cpJX8fGj8iC3DaRNpSMcg==",
+		"node_modules/@tiptap/extension-text-style": {
+			"version": "3.0.7",
+			"resolved": "https://registry.npmjs.org/@tiptap/extension-text-style/-/extension-text-style-3.0.7.tgz",
+			"integrity": "sha512-naJ1XxlbFJ1qlpA+i54lQYKuhWP1dnkUslM86OT0TZt0zJBeu7LIrqSOVGmMB++lF/btnQLMnYkYSSnkLgIw3A==",
 			"license": "MIT",
+			"peer": true,
 			"funding": {
 				"type": "github",
 				"url": "https://github.com/sponsors/ueberdosis"
 			},
 			"peerDependencies": {
-				"@tiptap/core": "^2.7.0"
+				"@tiptap/core": "^3.0.7"
 			}
 		},
-		"node_modules/@tiptap/extension-text": {
-			"version": "2.10.0",
-			"resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-2.10.0.tgz",
-			"integrity": "sha512-SSnNncADS1KucdEcJlF6WGCs5+1pAhPrD68vlw34oj3NDT3Zh05KiyXsCV3Nw4wpHOnbWahV+z3uT2SnR+xgoQ==",
+		"node_modules/@tiptap/extension-typography": {
+			"version": "3.0.7",
+			"resolved": "https://registry.npmjs.org/@tiptap/extension-typography/-/extension-typography-3.0.7.tgz",
+			"integrity": "sha512-Oz0EIkq8TDd15aupMYcH2L6izdI/LEO0e7+K+OhljTK5g/sGApLxCDdTlmX2szB9EXbTbOpwLKIEz2bPc3HvBA==",
 			"license": "MIT",
 			"funding": {
 				"type": "github",
 				"url": "https://github.com/sponsors/ueberdosis"
 			},
 			"peerDependencies": {
-				"@tiptap/core": "^2.7.0"
+				"@tiptap/core": "^3.0.7"
 			}
 		},
-		"node_modules/@tiptap/extension-text-style": {
-			"version": "2.10.0",
-			"resolved": "https://registry.npmjs.org/@tiptap/extension-text-style/-/extension-text-style-2.10.0.tgz",
-			"integrity": "sha512-VZtH1dp64wg1UcFtUPpRQK+kOm4JHBIv+WXuKX7EnpIEKjHKnyfV94BBVmaqY5UE4n3kbkkmIRB2Cmix/10AMg==",
+		"node_modules/@tiptap/extension-underline": {
+			"version": "3.0.7",
+			"resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.0.7.tgz",
+			"integrity": "sha512-pw2v5kbkovaWaC1G2IxP7g94vmUMlRBzZlCnLEyfFxtGa9LVAsUFlFFWaYJEmq7ZPG/tblWCnFfEZuQqFVd8Sg==",
 			"license": "MIT",
 			"funding": {
 				"type": "github",
 				"url": "https://github.com/sponsors/ueberdosis"
 			},
 			"peerDependencies": {
-				"@tiptap/core": "^2.7.0"
+				"@tiptap/core": "^3.0.7"
 			}
 		},
-		"node_modules/@tiptap/extension-typography": {
-			"version": "2.10.0",
-			"resolved": "https://registry.npmjs.org/@tiptap/extension-typography/-/extension-typography-2.10.0.tgz",
-			"integrity": "sha512-03IOfJm4bk2hZ4SsSfxgBOVzcDxMRBlFD7ZY12H2EGNf1TKxj/0ANWhAH54FtquuOMoY5aWg5LZf0lk++8UDAw==",
+		"node_modules/@tiptap/extension-youtube": {
+			"version": "3.0.7",
+			"resolved": "https://registry.npmjs.org/@tiptap/extension-youtube/-/extension-youtube-3.0.7.tgz",
+			"integrity": "sha512-BD4rc7Xoi3O+puXSEArHAbBVu4dhj+9TuuVYzEFgNHI+FN/py9J5AiNf4TXGKBSlMUOYPpODaEROwyGmqAmpuA==",
 			"license": "MIT",
 			"funding": {
 				"type": "github",
 				"url": "https://github.com/sponsors/ueberdosis"
 			},
 			"peerDependencies": {
-				"@tiptap/core": "^2.7.0"
+				"@tiptap/core": "^3.0.7"
 			}
 		},
-		"node_modules/@tiptap/extension-underline": {
-			"version": "2.25.0",
-			"resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-2.25.0.tgz",
-			"integrity": "sha512-RqXkWSMJyllfsDukugDzWEZfWRUOgcqzuMWC40BnuDUs4KgdRA0nhVUWJbLfUEmXI0UVqN5OwYTTAdhaiF7kjQ==",
+		"node_modules/@tiptap/extensions": {
+			"version": "3.0.7",
+			"resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.0.7.tgz",
+			"integrity": "sha512-GkXX5l7Q/543BKsC14j8M3qT+75ILb7138zy7cZoHm/s1ztV1XTknpEswBZIRZA9n6qq+Wd9g5qkbR879s6xhA==",
 			"license": "MIT",
 			"funding": {
 				"type": "github",
 				"url": "https://github.com/sponsors/ueberdosis"
 			},
 			"peerDependencies": {
-				"@tiptap/core": "^2.7.0"
+				"@tiptap/core": "^3.0.7",
+				"@tiptap/pm": "^3.0.7"
 			}
 		},
 		"node_modules/@tiptap/pm": {
-			"version": "2.11.7",
-			"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.11.7.tgz",
-			"integrity": "sha512-7gEEfz2Q6bYKXM07vzLUD0vqXFhC5geWRA6LCozTiLdVFDdHWiBrvb2rtkL5T7mfLq03zc1QhH7rI3F6VntOEA==",
+			"version": "3.0.7",
+			"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.0.7.tgz",
+			"integrity": "sha512-f8PnWjYqbMCxny8cyjbFNeIyeOYLECTa/7gj8DJr53Ns+P94b4kYIt/GkveR5KoOxsbmXi8Uc4mjcR1giQPaIQ==",
 			"license": "MIT",
 			"dependencies": {
-				"prosemirror-changeset": "^2.2.1",
+				"prosemirror-changeset": "^2.3.0",
 				"prosemirror-collab": "^1.3.1",
 				"prosemirror-commands": "^1.6.2",
 				"prosemirror-dropcursor": "^1.8.1",
@@ -3604,14 +3628,14 @@
 				"prosemirror-keymap": "^1.2.2",
 				"prosemirror-markdown": "^1.13.1",
 				"prosemirror-menu": "^1.2.4",
-				"prosemirror-model": "^1.23.0",
+				"prosemirror-model": "^1.24.1",
 				"prosemirror-schema-basic": "^1.2.3",
-				"prosemirror-schema-list": "^1.4.1",
+				"prosemirror-schema-list": "^1.5.0",
 				"prosemirror-state": "^1.4.3",
 				"prosemirror-tables": "^1.6.4",
 				"prosemirror-trailing-node": "^3.0.0",
 				"prosemirror-transform": "^1.10.2",
-				"prosemirror-view": "^1.37.0"
+				"prosemirror-view": "^1.38.1"
 			},
 			"funding": {
 				"type": "github",
@@ -3619,38 +3643,62 @@
 			}
 		},
 		"node_modules/@tiptap/starter-kit": {
-			"version": "2.10.0",
-			"resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-2.10.0.tgz",
-			"integrity": "sha512-hMIM9a6HjYZo25EzhZHlKEIR7CFi0grRSOltEyggiyBuQqKFkI7iwCpZVVtviDV1FwV0EPANpIAxPS7aBRgFdg==",
-			"license": "MIT",
-			"dependencies": {
-				"@tiptap/core": "^2.10.0",
-				"@tiptap/extension-blockquote": "^2.10.0",
-				"@tiptap/extension-bold": "^2.10.0",
-				"@tiptap/extension-bullet-list": "^2.10.0",
-				"@tiptap/extension-code": "^2.10.0",
-				"@tiptap/extension-code-block": "^2.10.0",
-				"@tiptap/extension-document": "^2.10.0",
-				"@tiptap/extension-dropcursor": "^2.10.0",
-				"@tiptap/extension-gapcursor": "^2.10.0",
-				"@tiptap/extension-hard-break": "^2.10.0",
-				"@tiptap/extension-heading": "^2.10.0",
-				"@tiptap/extension-history": "^2.10.0",
-				"@tiptap/extension-horizontal-rule": "^2.10.0",
-				"@tiptap/extension-italic": "^2.10.0",
-				"@tiptap/extension-list-item": "^2.10.0",
-				"@tiptap/extension-ordered-list": "^2.10.0",
-				"@tiptap/extension-paragraph": "^2.10.0",
-				"@tiptap/extension-strike": "^2.10.0",
-				"@tiptap/extension-text": "^2.10.0",
-				"@tiptap/extension-text-style": "^2.10.0",
-				"@tiptap/pm": "^2.10.0"
+			"version": "3.0.7",
+			"resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.0.7.tgz",
+			"integrity": "sha512-oTHZp6GXQQaZfZi8Fh7klH2YUeGq73XPF35CFw41mwdWdUUUms3ipaCKFqUyEYO21JMf3pZylJLxUucx5U7isg==",
+			"license": "MIT",
+			"dependencies": {
+				"@tiptap/core": "^3.0.7",
+				"@tiptap/extension-blockquote": "^3.0.7",
+				"@tiptap/extension-bold": "^3.0.7",
+				"@tiptap/extension-bullet-list": "^3.0.7",
+				"@tiptap/extension-code": "^3.0.7",
+				"@tiptap/extension-code-block": "^3.0.7",
+				"@tiptap/extension-document": "^3.0.7",
+				"@tiptap/extension-dropcursor": "^3.0.7",
+				"@tiptap/extension-gapcursor": "^3.0.7",
+				"@tiptap/extension-hard-break": "^3.0.7",
+				"@tiptap/extension-heading": "^3.0.7",
+				"@tiptap/extension-horizontal-rule": "^3.0.7",
+				"@tiptap/extension-italic": "^3.0.7",
+				"@tiptap/extension-link": "^3.0.7",
+				"@tiptap/extension-list": "^3.0.7",
+				"@tiptap/extension-list-item": "^3.0.7",
+				"@tiptap/extension-list-keymap": "^3.0.7",
+				"@tiptap/extension-ordered-list": "^3.0.7",
+				"@tiptap/extension-paragraph": "^3.0.7",
+				"@tiptap/extension-strike": "^3.0.7",
+				"@tiptap/extension-text": "^3.0.7",
+				"@tiptap/extension-underline": "^3.0.7",
+				"@tiptap/extensions": "^3.0.7",
+				"@tiptap/pm": "^3.0.7"
 			},
 			"funding": {
 				"type": "github",
 				"url": "https://github.com/sponsors/ueberdosis"
 			}
 		},
+		"node_modules/@tiptap/y-tiptap": {
+			"version": "3.0.0",
+			"resolved": "https://registry.npmjs.org/@tiptap/y-tiptap/-/y-tiptap-3.0.0.tgz",
+			"integrity": "sha512-HIeJZCj+KYJde2x6fONzo4o6kd7gW7eonwhQsv2p2VQnUgwNXMVhN+D6Z3AH/2i541Sq33y1PO4U/1ThCPjqbA==",
+			"license": "MIT",
+			"peer": true,
+			"dependencies": {
+				"lib0": "^0.2.100"
+			},
+			"engines": {
+				"node": ">=16.0.0",
+				"npm": ">=8.0.0"
+			},
+			"peerDependencies": {
+				"prosemirror-model": "^1.7.1",
+				"prosemirror-state": "^1.2.3",
+				"prosemirror-view": "^1.9.10",
+				"y-protocols": "^1.0.1",
+				"yjs": "^13.5.38"
+			}
+		},
 		"node_modules/@types/cookie": {
 			"version": "0.6.0",
 			"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
@@ -3919,7 +3967,6 @@
 			"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
 			"integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
 			"license": "MIT",
-			"peer": true,
 			"dependencies": {
 				"@types/unist": "*"
 			}
@@ -4003,8 +4050,7 @@
 		"node_modules/@types/unist": {
 			"version": "2.0.10",
 			"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz",
-			"integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==",
-			"peer": true
+			"integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA=="
 		},
 		"node_modules/@types/yauzl": {
 			"version": "2.10.3",
@@ -6378,7 +6424,6 @@
 			"resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
 			"integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
 			"license": "MIT",
-			"peer": true,
 			"dependencies": {
 				"dequal": "^2.0.0"
 			},
@@ -7665,9 +7710,10 @@
 			"dev": true
 		},
 		"node_modules/highlight.js": {
-			"version": "11.9.0",
-			"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.9.0.tgz",
-			"integrity": "sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==",
+			"version": "11.11.1",
+			"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
+			"integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
+			"license": "BSD-3-Clause",
 			"engines": {
 				"node": ">=12.0.0"
 			}
@@ -8941,15 +8987,14 @@
 			}
 		},
 		"node_modules/lowlight": {
-			"version": "3.1.0",
-			"resolved": "https://registry.npmjs.org/lowlight/-/lowlight-3.1.0.tgz",
-			"integrity": "sha512-CEbNVoSikAxwDMDPjXlqlFYiZLkDJHwyGu/MfOsJnF3d7f3tds5J3z8s/l9TMXhzfsJCCJEAsD78842mwmg0PQ==",
+			"version": "3.3.0",
+			"resolved": "https://registry.npmjs.org/lowlight/-/lowlight-3.3.0.tgz",
+			"integrity": "sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==",
 			"license": "MIT",
-			"peer": true,
 			"dependencies": {
 				"@types/hast": "^3.0.0",
 				"devlop": "^1.0.0",
-				"highlight.js": "~11.9.0"
+				"highlight.js": "~11.11.0"
 			},
 			"funding": {
 				"type": "github",
@@ -10108,9 +10153,9 @@
 			}
 		},
 		"node_modules/prosemirror-changeset": {
-			"version": "2.2.1",
-			"resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.2.1.tgz",
-			"integrity": "sha512-J7msc6wbxB4ekDFj+n9gTW/jav/p53kdlivvuppHsrZXCaQdVgRghoZbSS3kwrRyAstRVQ4/+u5k7YfLgkkQvQ==",
+			"version": "2.3.1",
+			"resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.3.1.tgz",
+			"integrity": "sha512-j0kORIBm8ayJNl3zQvD1TTPHJX3g042et6y/KQhZhnPrruO8exkTgG8X+NRpj7kIyMMEx74Xb3DyMIBtO0IKkQ==",
 			"license": "MIT",
 			"dependencies": {
 				"prosemirror-transform": "^1.0.0"

+ 19 - 20
package.json

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

+ 7 - 0
pyproject.toml

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

+ 5 - 0
src/app.css

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

+ 7 - 9
src/lib/apis/folders/index.ts

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

+ 1 - 1
src/lib/components/admin/Settings/Audio.svelte

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

+ 24 - 8
src/lib/components/chat/Chat.svelte

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

+ 20 - 0
src/lib/components/chat/MessageInput.svelte

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

+ 3 - 0
src/lib/components/chat/Messages.svelte

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

+ 2 - 0
src/lib/components/chat/Messages/ContentRenderer.svelte

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

+ 2 - 0
src/lib/components/chat/Messages/Markdown.svelte

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

+ 6 - 11
src/lib/components/chat/Messages/Markdown/MarkdownInlineTokens.svelte

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

+ 33 - 0
src/lib/components/chat/Messages/Markdown/MarkdownInlineTokens/CodespanToken.svelte

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

+ 19 - 0
src/lib/components/chat/Messages/Markdown/MarkdownInlineTokens/TextToken.svelte

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

+ 29 - 3
src/lib/components/chat/Messages/Markdown/MarkdownTokens.svelte

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

+ 3 - 0
src/lib/components/chat/Messages/Message.svelte

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

+ 2 - 0
src/lib/components/chat/Messages/MultiResponseMessages.svelte

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

+ 13 - 3
src/lib/components/chat/Messages/ResponseMessage.svelte

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

+ 40 - 36
src/lib/components/chat/Placeholder.svelte

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

+ 103 - 0
src/lib/components/chat/Placeholder/ChatList.svelte

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

+ 0 - 0
src/lib/components/chat/Placeholder/FolderKnowledge.svelte


+ 51 - 0
src/lib/components/chat/Placeholder/FolderPlaceholder.svelte

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

+ 147 - 0
src/lib/components/chat/Placeholder/FolderTitle.svelte

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

+ 93 - 0
src/lib/components/chat/Settings/Interface.svelte

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

+ 1 - 1
src/lib/components/chat/Settings/Personalization/ManageModal.svelte

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

+ 1 - 1
src/lib/components/chat/SettingsModal.svelte

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

+ 2 - 2
src/lib/components/common/FileItemModal.svelte

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

+ 8 - 0
src/lib/components/common/Modal.svelte

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

+ 165 - 37
src/lib/components/common/RichTextInput.svelte

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

+ 27 - 0
src/lib/components/common/RichTextInput/FormattingButtons.svelte

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

+ 197 - 0
src/lib/components/common/RichTextInput/Image/image.ts

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

+ 5 - 0
src/lib/components/common/RichTextInput/Image/index.ts

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

+ 19 - 0
src/lib/components/icons/AdjustmentsHorizontalOutline.svelte

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

+ 20 - 0
src/lib/components/icons/ArrowLeftTag.svelte

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

+ 20 - 0
src/lib/components/icons/ArrowRightTag.svelte

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

+ 212 - 64
src/lib/components/layout/SearchModal.svelte

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

+ 20 - 8
src/lib/components/layout/Sidebar.svelte

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

+ 2 - 1
src/lib/components/layout/Sidebar/Folders/FolderMenu.svelte

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

+ 28 - 11
src/lib/components/layout/Sidebar/Folders/EditFolderModal.svelte → src/lib/components/layout/Sidebar/Folders/FolderModal.svelte

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

+ 8 - 8
src/lib/components/layout/Sidebar/RecursiveFolder.svelte

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

+ 4 - 0
src/lib/components/layout/Sidebar/SearchInput.svelte

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

+ 3 - 5
src/lib/components/layout/Sidebar/UserMenu.svelte

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

+ 259 - 117
src/lib/components/notes/NoteEditor.svelte

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

+ 49 - 21
src/lib/components/notes/NoteEditor/Chat.svelte

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

+ 103 - 0
src/lib/components/notes/NoteEditor/Controls.svelte

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

+ 0 - 46
src/lib/components/notes/NoteEditor/Settings.svelte

@@ -1,46 +0,0 @@
-<script lang="ts">
-	import { getContext } from 'svelte';
-	const i18n = getContext('i18n');
-
-	import XMark from '$lib/components/icons/XMark.svelte';
-	import { models } from '$lib/stores';
-
-	export let show = false;
-	export let selectedModelId = '';
-</script>
-
-<div class="flex items-center mb-1.5 pt-1.5">
-	<div class=" -translate-x-1.5 flex items-center">
-		<button
-			class="p-0.5 bg-transparent transition rounded-lg"
-			on:click={() => {
-				show = !show;
-			}}
-		>
-			<XMark className="size-5" strokeWidth="2.5" />
-		</button>
-	</div>
-
-	<div class=" font-medium text-base flex items-center gap-1">
-		<div>
-			{$i18n.t('Settings')}
-		</div>
-	</div>
-</div>
-
-<div class="mt-1">
-	<div>
-		<div class=" text-xs font-medium mb-1">Model</div>
-
-		<div class="w-full">
-			<select class="w-full bg-transparent text-sm outline-hidden" bind:value={selectedModelId}>
-				<option value="" class="bg-gray-50 dark:bg-gray-700" disabled>
-					{$i18n.t('Select a model')}
-				</option>
-				{#each $models.filter((model) => !(model?.info?.meta?.hidden ?? false)) as model}
-					<option value={model.id} class="bg-gray-50 dark:bg-gray-700">{model.name}</option>
-				{/each}
-			</select>
-		</div>
-	</div>
-</div>

+ 3 - 1
src/lib/components/notes/Notes/NoteMenu.svelte

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

+ 161 - 4
src/lib/components/workspace/Models/Knowledge.svelte

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

+ 1 - 1
src/lib/components/workspace/Models/ModelEditor.svelte

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

+ 4 - 5
src/lib/components/workspace/Prompts/PromptEditor.svelte

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

+ 14 - 3
src/lib/i18n/locales/ar-BH/translation.json

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

+ 14 - 3
src/lib/i18n/locales/ar/translation.json

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

+ 14 - 3
src/lib/i18n/locales/bg-BG/translation.json

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

+ 14 - 3
src/lib/i18n/locales/bn-BD/translation.json

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

+ 14 - 3
src/lib/i18n/locales/bo-TB/translation.json

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

+ 14 - 3
src/lib/i18n/locales/ca-ES/translation.json

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

+ 14 - 3
src/lib/i18n/locales/ceb-PH/translation.json

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

+ 14 - 3
src/lib/i18n/locales/cs-CZ/translation.json

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

+ 14 - 3
src/lib/i18n/locales/da-DK/translation.json

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

+ 23 - 12
src/lib/i18n/locales/de-DE/translation.json

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

+ 14 - 3
src/lib/i18n/locales/dg-DG/translation.json

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

+ 14 - 3
src/lib/i18n/locales/el-GR/translation.json

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

+ 14 - 3
src/lib/i18n/locales/en-GB/translation.json

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

+ 14 - 3
src/lib/i18n/locales/en-US/translation.json

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

+ 14 - 3
src/lib/i18n/locales/es-ES/translation.json

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

+ 14 - 3
src/lib/i18n/locales/et-EE/translation.json

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

+ 14 - 3
src/lib/i18n/locales/eu-ES/translation.json

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

+ 14 - 3
src/lib/i18n/locales/fa-IR/translation.json

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

+ 14 - 3
src/lib/i18n/locales/fi-FI/translation.json

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

+ 14 - 3
src/lib/i18n/locales/fr-CA/translation.json

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

+ 34 - 23
src/lib/i18n/locales/fr-FR/translation.json

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

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