Browse Source

Merge branch 'open-webui:main' into main

df-cgdm 5 months ago
parent
commit
126045318f
100 changed files with 3962 additions and 1670 deletions
  1. 1 1
      .github/workflows/release-pypi.yml
  2. 65 0
      CHANGELOG.md
  3. 7 2
      README.md
  4. 163 10
      backend/open_webui/config.py
  5. 1 0
      backend/open_webui/env.py
  6. 74 18
      backend/open_webui/main.py
  7. 95 28
      backend/open_webui/retrieval/utils.py
  8. 10 14
      backend/open_webui/retrieval/web/duckduckgo.py
  9. 48 0
      backend/open_webui/retrieval/web/serpapi.py
  10. 8 2
      backend/open_webui/retrieval/web/tavily.py
  11. 455 25
      backend/open_webui/retrieval/web/utils.py
  12. 13 3
      backend/open_webui/routers/audio.py
  13. 23 6
      backend/open_webui/routers/auths.py
  14. 3 1
      backend/open_webui/routers/channels.py
  15. 46 4
      backend/open_webui/routers/configs.py
  16. 16 9
      backend/open_webui/routers/files.py
  17. 64 0
      backend/open_webui/routers/images.py
  18. 16 3
      backend/open_webui/routers/ollama.py
  19. 59 51
      backend/open_webui/routers/pipelines.py
  20. 87 15
      backend/open_webui/routers/retrieval.py
  21. 13 2
      backend/open_webui/routers/tasks.py
  22. 42 11
      backend/open_webui/routers/utils.py
  23. 0 0
      backend/open_webui/static/loader.js
  24. 0 2
      backend/open_webui/static/swagger-ui/swagger-ui.css
  25. 76 0
      backend/open_webui/storage/provider.py
  26. 150 0
      backend/open_webui/test/apps/webui/storage/test_provider.py
  27. 67 1
      backend/open_webui/utils/auth.py
  28. 18 18
      backend/open_webui/utils/chat.py
  29. 153 88
      backend/open_webui/utils/middleware.py
  30. 5 4
      backend/open_webui/utils/misc.py
  31. 1 1
      backend/open_webui/utils/models.py
  32. 68 14
      backend/open_webui/utils/oauth.py
  33. 77 49
      backend/open_webui/utils/payload.py
  34. 31 50
      backend/open_webui/utils/response.py
  35. 1 1
      backend/open_webui/utils/task.py
  36. 3 3
      backend/open_webui/utils/webhook.py
  37. 11 7
      backend/requirements.txt
  38. 11 0
      backend/start.sh
  39. 11 0
      backend/start_windows.bat
  40. 10 0
      docker-compose.playwright.yaml
  41. 525 309
      package-lock.json
  42. 4 3
      package.json
  43. 1 2
      postcss.config.js
  44. 10 6
      pyproject.toml
  45. 9 0
      run-compose.sh
  46. 32 0
      scripts/prepare-pyodide.js
  47. 17 5
      src/app.css
  48. 1 0
      src/app.html
  49. 1 1
      src/lib/apis/chats/index.ts
  50. 4 4
      src/lib/apis/configs/index.ts
  51. 4 2
      src/lib/apis/users/index.ts
  52. 46 8
      src/lib/apis/utils/index.ts
  53. 6 6
      src/lib/components/AddConnectionModal.svelte
  54. 1 1
      src/lib/components/ChangelogModal.svelte
  55. 2 2
      src/lib/components/NotificationToast.svelte
  56. 2 2
      src/lib/components/OnBoarding.svelte
  57. 5 3
      src/lib/components/admin/Evaluations/Feedbacks.svelte
  58. 5 3
      src/lib/components/admin/Evaluations/Leaderboard.svelte
  59. 6 6
      src/lib/components/admin/Functions.svelte
  60. 5 5
      src/lib/components/admin/Functions/FunctionEditor.svelte
  61. 3 3
      src/lib/components/admin/Functions/FunctionMenu.svelte
  62. 6 6
      src/lib/components/admin/Settings.svelte
  63. 24 24
      src/lib/components/admin/Settings/Audio.svelte
  64. 317 0
      src/lib/components/admin/Settings/CodeExecution.svelte
  65. 0 166
      src/lib/components/admin/Settings/CodeInterpreter.svelte
  66. 4 4
      src/lib/components/admin/Settings/Connections.svelte
  67. 1 1
      src/lib/components/admin/Settings/Connections/ManageOllamaModal.svelte
  68. 1 1
      src/lib/components/admin/Settings/Connections/OllamaConnection.svelte
  69. 2 2
      src/lib/components/admin/Settings/Connections/OpenAIConnection.svelte
  70. 1 1
      src/lib/components/admin/Settings/Database.svelte
  71. 41 24
      src/lib/components/admin/Settings/Documents.svelte
  72. 5 5
      src/lib/components/admin/Settings/Evaluations/ArenaModelModal.svelte
  73. 1 1
      src/lib/components/admin/Settings/Evaluations/Model.svelte
  74. 471 316
      src/lib/components/admin/Settings/General.svelte
  75. 39 17
      src/lib/components/admin/Settings/Images.svelte
  76. 228 207
      src/lib/components/admin/Settings/Interface.svelte
  77. 1 1
      src/lib/components/admin/Settings/Models.svelte
  78. 2 2
      src/lib/components/admin/Settings/Models/ConfigureModelsModal.svelte
  79. 1 1
      src/lib/components/admin/Settings/Models/Manage/ManageMultipleOllama.svelte
  80. 7 7
      src/lib/components/admin/Settings/Models/Manage/ManageOllama.svelte
  81. 8 8
      src/lib/components/admin/Settings/Pipelines.svelte
  82. 58 14
      src/lib/components/admin/Settings/WebSearch.svelte
  83. 3 3
      src/lib/components/admin/Users/Groups.svelte
  84. 2 2
      src/lib/components/admin/Users/Groups/AddGroupModal.svelte
  85. 3 3
      src/lib/components/admin/Users/Groups/Display.svelte
  86. 6 6
      src/lib/components/admin/Users/Groups/Permissions.svelte
  87. 1 1
      src/lib/components/admin/Users/Groups/Users.svelte
  88. 7 4
      src/lib/components/admin/Users/UserList.svelte
  89. 6 6
      src/lib/components/admin/Users/UserList/AddUserModal.svelte
  90. 5 5
      src/lib/components/admin/Users/UserList/EditUserModal.svelte
  91. 1 1
      src/lib/components/admin/Users/UserList/UserChatsModal.svelte
  92. 1 1
      src/lib/components/channel/Channel.svelte
  93. 5 3
      src/lib/components/channel/MessageInput.svelte
  94. 1 1
      src/lib/components/channel/MessageInput/InputMenu.svelte
  95. 4 4
      src/lib/components/channel/Messages/Message.svelte
  96. 1 1
      src/lib/components/channel/Messages/Message/ProfilePreview.svelte
  97. 2 2
      src/lib/components/channel/Messages/Message/ReactionPicker.svelte
  98. 1 1
      src/lib/components/channel/Navbar.svelte
  99. 2 2
      src/lib/components/chat/Chat.svelte
  100. 2 2
      src/lib/components/chat/ChatControls.svelte

+ 1 - 1
.github/workflows/release-pypi.yml

@@ -19,7 +19,7 @@ jobs:
         uses: actions/checkout@v4
         uses: actions/checkout@v4
       - uses: actions/setup-node@v4
       - uses: actions/setup-node@v4
         with:
         with:
-          node-version: 18
+          node-version: 22
       - uses: actions/setup-python@v5
       - uses: actions/setup-python@v5
         with:
         with:
           python-version: 3.11
           python-version: 3.11

+ 65 - 0
CHANGELOG.md

@@ -5,6 +5,71 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 
+## [0.5.16] - 2025-02-20
+
+### Fixed
+
+- **🔍 Web Search Retrieval Restored**: Resolved a critical issue that broke web search retrieval by reverting deduplication changes, ensuring complete and accurate search results once again.
+
+## [0.5.15] - 2025-02-20
+
+### Added
+
+- **📄 Full Context Mode for Local Document Search (RAG)**: Toggle full context mode from Admin Settings > Documents to inject entire document content into context, improving accuracy for models with large context windows—ideal for deep context understanding.
+- **🌍 Smarter Web Search with Agentic Workflows**: Web searches now intelligently gather and refine multiple relevant terms, similar to RAG handling, delivering significantly better search results for more accurate information retrieval.
+- **🔎 Experimental Playwright Support for Web Loader**: Web content retrieval is taken to the next level with Playwright-powered scraping for enhanced accuracy in extracted web data.
+- **☁️ Experimental Azure Storage Provider**: Early-stage support for Azure Storage allows more cloud storage flexibility directly within Open WebUI.
+- **📊 Improved Jupyter Code Execution with Plots**: Interactive coding now properly displays inline plots, making data visualization more seamless inside chat interactions.
+- **⏳ Adjustable Execution Timeout for Jupyter Interpreter**: Customize execution timeout (default: 60s) for Jupyter-based code execution, allowing longer or more constrained execution based on your needs.
+- **▶️ "Running..." Indicator for Jupyter Code Execution**: A visual indicator now appears while code execution is in progress, providing real-time status updates on ongoing computations.
+- **⚙️ General Backend & Frontend Stability Enhancements**: Extensive refactoring improves reliability, performance, and overall user experience for a more seamless Open WebUI.
+- **🌍 Translation Updates**: Various international translation refinements ensure better localization and a more natural user interface experience.
+
+### Fixed
+
+- **📱 Mobile Hover Issue Resolved**: Users can now edit responses smoothly on mobile without interference, fixing a longstanding hover issue.
+- **🔄 Temporary Chat Message Duplication Fixed**: Eliminated buggy behavior where messages were being unnecessarily repeated in temporary chat mode, ensuring a smooth and consistent conversation flow.
+
+## [0.5.14] - 2025-02-17
+
+### Fixed
+
+- **🔧 Critical Import Error Resolved**: Fixed a circular import issue preventing 'override_static' from being correctly imported in 'open_webui.config', ensuring smooth system initialization and stability.
+
+## [0.5.13] - 2025-02-17
+
+### Added
+
+- **🌐 Full Context Mode for Web Search**: Enable highly accurate web searches by utilizing full context mode—ideal for models with large context windows, ensuring more precise and insightful results.
+- **⚡ Optimized Asynchronous Web Search**: Web searches now load significantly faster with optimized async support, providing users with quicker, more efficient information retrieval.
+- **🔄 Auto Text Direction for RTL Languages**: Automatic text alignment based on language input, ensuring seamless conversation flow for Arabic, Hebrew, and other right-to-left scripts.
+- **🚀 Jupyter Notebook Support for Code Execution**: The "Run" button in code blocks can now use Jupyter for execution, offering a powerful, dynamic coding experience directly in the chat.
+- **🗑️ Message Delete Confirmation Dialog**: Prevent accidental deletions with a new confirmation prompt before removing messages, adding an additional layer of security to your chat history.
+- **📥 Download Button for SVG Diagrams**: SVG diagrams generated within chat can now be downloaded instantly, making it easier to save and share complex visual data.
+- **✨ General UI/UX Improvements and Backend Stability**: A refined interface with smoother interactions, improved layouts, and backend stability enhancements for a more reliable, polished experience.
+
+### Fixed
+
+- **🛠️ Temporary Chat Message Continue Button Fixed**: The "Continue Response" button for temporary chats now works as expected, ensuring an uninterrupted conversation flow.
+
+### Changed
+
+- **📝 Prompt Variable Update**: Deprecated square bracket '[]' indicators for prompt variables; now requires double curly brackets '{{}}' for consistency and clarity.
+- **🔧 Stability Enhancements**: Error handling improved in chat history, ensuring smoother operations when reviewing previous messages.
+
+## [0.5.12] - 2025-02-13
+
+### Added
+
+- **🛠️ Multiple Tool Calls Support for Native Function Mode**: Functions now can call multiple tools within a single response, unlocking better automation and workflow flexibility when using native function calling.
+
+### Fixed
+
+- **📝 Playground Text Completion Restored**: Addressed an issue where text completion in the Playground was not functioning.
+- **🔗 Direct Connections Now Work for Regular Users**: Fixed a bug where users with the 'user' role couldn't establish direct API connections, enabling seamless model usage for all user tiers.
+- **⚡ Landing Page Input No Longer Lags with Long Text**: Improved input responsiveness on the landing page, ensuring fast and smooth typing experiences even when entering long messages.
+- **🔧 Parameter in Functions Fixed**: Fixed an issue where the reserved parameters wasn’t recognized within functions, restoring full functionality for advanced task-based automation.
+
 ## [0.5.11] - 2025-02-13
 ## [0.5.11] - 2025-02-13
 
 
 ### Added
 ### Added

+ 7 - 2
README.md

@@ -13,10 +13,15 @@
 
 
 **Open WebUI is an [extensible](https://docs.openwebui.com/features/plugin/), feature-rich, and user-friendly self-hosted AI platform designed to operate entirely offline.** It supports various LLM runners like **Ollama** and **OpenAI-compatible APIs**, with **built-in inference engine** for RAG, making it a **powerful AI deployment solution**.
 **Open WebUI is an [extensible](https://docs.openwebui.com/features/plugin/), feature-rich, and user-friendly self-hosted AI platform designed to operate entirely offline.** It supports various LLM runners like **Ollama** and **OpenAI-compatible APIs**, with **built-in inference engine** for RAG, making it a **powerful AI deployment solution**.
 
 
-For more information, be sure to check out our [Open WebUI Documentation](https://docs.openwebui.com/).
-
 ![Open WebUI Demo](./demo.gif)
 ![Open WebUI Demo](./demo.gif)
 
 
+> [!TIP]  
+> **Looking for an [Enterprise Plan](https://docs.openwebui.com/enterprise)?** – **[Speak with Our Sales Team Today!](mailto:sales@openwebui.com)**
+>
+> Get **enhanced capabilities**, including **custom theming and branding**, **Service Level Agreement (SLA) support**, **Long-Term Support (LTS) versions**, and **more!**
+
+For more information, be sure to check out our [Open WebUI Documentation](https://docs.openwebui.com/).
+
 ## Key Features of Open WebUI ⭐
 ## Key Features of Open WebUI ⭐
 
 
 - 🚀 **Effortless Setup**: Install seamlessly using Docker or Kubernetes (kubectl, kustomize or helm) for a hassle-free experience with support for both `:ollama` and `:cuda` tagged images.
 - 🚀 **Effortless Setup**: Install seamlessly using Docker or Kubernetes (kubectl, kustomize or helm) for a hassle-free experience with support for both `:ollama` and `:cuda` tagged images.

+ 163 - 10
backend/open_webui/config.py

@@ -2,6 +2,8 @@ import json
 import logging
 import logging
 import os
 import os
 import shutil
 import shutil
+import base64
+
 from datetime import datetime
 from datetime import datetime
 from pathlib import Path
 from pathlib import Path
 from typing import Generic, Optional, TypeVar
 from typing import Generic, Optional, TypeVar
@@ -593,8 +595,6 @@ if frontend_favicon.exists():
         shutil.copyfile(frontend_favicon, STATIC_DIR / "favicon.png")
         shutil.copyfile(frontend_favicon, STATIC_DIR / "favicon.png")
     except Exception as e:
     except Exception as e:
         logging.error(f"An error occurred: {e}")
         logging.error(f"An error occurred: {e}")
-else:
-    logging.warning(f"Frontend favicon not found at {frontend_favicon}")
 
 
 frontend_splash = FRONTEND_BUILD_DIR / "static" / "splash.png"
 frontend_splash = FRONTEND_BUILD_DIR / "static" / "splash.png"
 
 
@@ -603,12 +603,18 @@ if frontend_splash.exists():
         shutil.copyfile(frontend_splash, STATIC_DIR / "splash.png")
         shutil.copyfile(frontend_splash, STATIC_DIR / "splash.png")
     except Exception as e:
     except Exception as e:
         logging.error(f"An error occurred: {e}")
         logging.error(f"An error occurred: {e}")
-else:
-    logging.warning(f"Frontend splash not found at {frontend_splash}")
+
+frontend_loader = FRONTEND_BUILD_DIR / "static" / "loader.js"
+
+if frontend_loader.exists():
+    try:
+        shutil.copyfile(frontend_loader, STATIC_DIR / "loader.js")
+    except Exception as e:
+        logging.error(f"An error occurred: {e}")
 
 
 
 
 ####################################
 ####################################
-# CUSTOM_NAME
+# CUSTOM_NAME (Legacy)
 ####################################
 ####################################
 
 
 CUSTOM_NAME = os.environ.get("CUSTOM_NAME", "")
 CUSTOM_NAME = os.environ.get("CUSTOM_NAME", "")
@@ -650,6 +656,16 @@ if CUSTOM_NAME:
         pass
         pass
 
 
 
 
+####################################
+# LICENSE_KEY
+####################################
+
+LICENSE_KEY = PersistentConfig(
+    "LICENSE_KEY",
+    "license.key",
+    os.environ.get("LICENSE_KEY", ""),
+)
+
 ####################################
 ####################################
 # STORAGE PROVIDER
 # STORAGE PROVIDER
 ####################################
 ####################################
@@ -668,6 +684,10 @@ GOOGLE_APPLICATION_CREDENTIALS_JSON = os.environ.get(
     "GOOGLE_APPLICATION_CREDENTIALS_JSON", None
     "GOOGLE_APPLICATION_CREDENTIALS_JSON", None
 )
 )
 
 
+AZURE_STORAGE_ENDPOINT = os.environ.get("AZURE_STORAGE_ENDPOINT", None)
+AZURE_STORAGE_CONTAINER_NAME = os.environ.get("AZURE_STORAGE_CONTAINER_NAME", None)
+AZURE_STORAGE_KEY = os.environ.get("AZURE_STORAGE_KEY", None)
+
 ####################################
 ####################################
 # File Upload DIR
 # File Upload DIR
 ####################################
 ####################################
@@ -767,6 +787,9 @@ ENABLE_OPENAI_API = PersistentConfig(
 OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "")
 OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "")
 OPENAI_API_BASE_URL = os.environ.get("OPENAI_API_BASE_URL", "")
 OPENAI_API_BASE_URL = os.environ.get("OPENAI_API_BASE_URL", "")
 
 
+GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY", "")
+GEMINI_API_BASE_URL = os.environ.get("GEMINI_API_BASE_URL", "")
+
 
 
 if OPENAI_API_BASE_URL == "":
 if OPENAI_API_BASE_URL == "":
     OPENAI_API_BASE_URL = "https://api.openai.com/v1"
     OPENAI_API_BASE_URL = "https://api.openai.com/v1"
@@ -1190,6 +1213,12 @@ ENABLE_TAGS_GENERATION = PersistentConfig(
     os.environ.get("ENABLE_TAGS_GENERATION", "True").lower() == "true",
     os.environ.get("ENABLE_TAGS_GENERATION", "True").lower() == "true",
 )
 )
 
 
+ENABLE_TITLE_GENERATION = PersistentConfig(
+    "ENABLE_TITLE_GENERATION",
+    "task.title.enable",
+    os.environ.get("ENABLE_TITLE_GENERATION", "True").lower() == "true",
+)
+
 
 
 ENABLE_SEARCH_QUERY_GENERATION = PersistentConfig(
 ENABLE_SEARCH_QUERY_GENERATION = PersistentConfig(
     "ENABLE_SEARCH_QUERY_GENERATION",
     "ENABLE_SEARCH_QUERY_GENERATION",
@@ -1341,6 +1370,44 @@ Responses from models: {{responses}}"""
 # Code Interpreter
 # Code Interpreter
 ####################################
 ####################################
 
 
+
+CODE_EXECUTION_ENGINE = PersistentConfig(
+    "CODE_EXECUTION_ENGINE",
+    "code_execution.engine",
+    os.environ.get("CODE_EXECUTION_ENGINE", "pyodide"),
+)
+
+CODE_EXECUTION_JUPYTER_URL = PersistentConfig(
+    "CODE_EXECUTION_JUPYTER_URL",
+    "code_execution.jupyter.url",
+    os.environ.get("CODE_EXECUTION_JUPYTER_URL", ""),
+)
+
+CODE_EXECUTION_JUPYTER_AUTH = PersistentConfig(
+    "CODE_EXECUTION_JUPYTER_AUTH",
+    "code_execution.jupyter.auth",
+    os.environ.get("CODE_EXECUTION_JUPYTER_AUTH", ""),
+)
+
+CODE_EXECUTION_JUPYTER_AUTH_TOKEN = PersistentConfig(
+    "CODE_EXECUTION_JUPYTER_AUTH_TOKEN",
+    "code_execution.jupyter.auth_token",
+    os.environ.get("CODE_EXECUTION_JUPYTER_AUTH_TOKEN", ""),
+)
+
+
+CODE_EXECUTION_JUPYTER_AUTH_PASSWORD = PersistentConfig(
+    "CODE_EXECUTION_JUPYTER_AUTH_PASSWORD",
+    "code_execution.jupyter.auth_password",
+    os.environ.get("CODE_EXECUTION_JUPYTER_AUTH_PASSWORD", ""),
+)
+
+CODE_EXECUTION_JUPYTER_TIMEOUT = PersistentConfig(
+    "CODE_EXECUTION_JUPYTER_TIMEOUT",
+    "code_execution.jupyter.timeout",
+    int(os.environ.get("CODE_EXECUTION_JUPYTER_TIMEOUT", "60")),
+)
+
 ENABLE_CODE_INTERPRETER = PersistentConfig(
 ENABLE_CODE_INTERPRETER = PersistentConfig(
     "ENABLE_CODE_INTERPRETER",
     "ENABLE_CODE_INTERPRETER",
     "code_interpreter.enable",
     "code_interpreter.enable",
@@ -1362,26 +1429,48 @@ CODE_INTERPRETER_PROMPT_TEMPLATE = PersistentConfig(
 CODE_INTERPRETER_JUPYTER_URL = PersistentConfig(
 CODE_INTERPRETER_JUPYTER_URL = PersistentConfig(
     "CODE_INTERPRETER_JUPYTER_URL",
     "CODE_INTERPRETER_JUPYTER_URL",
     "code_interpreter.jupyter.url",
     "code_interpreter.jupyter.url",
-    os.environ.get("CODE_INTERPRETER_JUPYTER_URL", ""),
+    os.environ.get(
+        "CODE_INTERPRETER_JUPYTER_URL", os.environ.get("CODE_EXECUTION_JUPYTER_URL", "")
+    ),
 )
 )
 
 
 CODE_INTERPRETER_JUPYTER_AUTH = PersistentConfig(
 CODE_INTERPRETER_JUPYTER_AUTH = PersistentConfig(
     "CODE_INTERPRETER_JUPYTER_AUTH",
     "CODE_INTERPRETER_JUPYTER_AUTH",
     "code_interpreter.jupyter.auth",
     "code_interpreter.jupyter.auth",
-    os.environ.get("CODE_INTERPRETER_JUPYTER_AUTH", ""),
+    os.environ.get(
+        "CODE_INTERPRETER_JUPYTER_AUTH",
+        os.environ.get("CODE_EXECUTION_JUPYTER_AUTH", ""),
+    ),
 )
 )
 
 
 CODE_INTERPRETER_JUPYTER_AUTH_TOKEN = PersistentConfig(
 CODE_INTERPRETER_JUPYTER_AUTH_TOKEN = PersistentConfig(
     "CODE_INTERPRETER_JUPYTER_AUTH_TOKEN",
     "CODE_INTERPRETER_JUPYTER_AUTH_TOKEN",
     "code_interpreter.jupyter.auth_token",
     "code_interpreter.jupyter.auth_token",
-    os.environ.get("CODE_INTERPRETER_JUPYTER_AUTH_TOKEN", ""),
+    os.environ.get(
+        "CODE_INTERPRETER_JUPYTER_AUTH_TOKEN",
+        os.environ.get("CODE_EXECUTION_JUPYTER_AUTH_TOKEN", ""),
+    ),
 )
 )
 
 
 
 
 CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD = PersistentConfig(
 CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD = PersistentConfig(
     "CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD",
     "CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD",
     "code_interpreter.jupyter.auth_password",
     "code_interpreter.jupyter.auth_password",
-    os.environ.get("CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD", ""),
+    os.environ.get(
+        "CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD",
+        os.environ.get("CODE_EXECUTION_JUPYTER_AUTH_PASSWORD", ""),
+    ),
+)
+
+CODE_INTERPRETER_JUPYTER_TIMEOUT = PersistentConfig(
+    "CODE_INTERPRETER_JUPYTER_TIMEOUT",
+    "code_interpreter.jupyter.timeout",
+    int(
+        os.environ.get(
+            "CODE_INTERPRETER_JUPYTER_TIMEOUT",
+            os.environ.get("CODE_EXECUTION_JUPYTER_TIMEOUT", "60"),
+        )
+    ),
 )
 )
 
 
 
 
@@ -1505,6 +1594,12 @@ ENABLE_RAG_HYBRID_SEARCH = PersistentConfig(
     os.environ.get("ENABLE_RAG_HYBRID_SEARCH", "").lower() == "true",
     os.environ.get("ENABLE_RAG_HYBRID_SEARCH", "").lower() == "true",
 )
 )
 
 
+RAG_FULL_CONTEXT = PersistentConfig(
+    "RAG_FULL_CONTEXT",
+    "rag.full_context",
+    os.getenv("RAG_FULL_CONTEXT", "False").lower() == "true",
+)
+
 RAG_FILE_MAX_COUNT = PersistentConfig(
 RAG_FILE_MAX_COUNT = PersistentConfig(
     "RAG_FILE_MAX_COUNT",
     "RAG_FILE_MAX_COUNT",
     "rag.file.max_count",
     "rag.file.max_count",
@@ -1619,7 +1714,7 @@ Respond to the user query using the provided context, incorporating inline citat
 - Respond in the same language as the user's query.
 - Respond in the same language as the user's query.
 - If the context is unreadable or of poor quality, inform the user and provide the best possible answer.
 - If the context is unreadable or of poor quality, inform the user and provide the best possible answer.
 - If the answer isn't present in the context but you possess the knowledge, explain this to the user and provide the answer using your own understanding.
 - If the answer isn't present in the context but you possess the knowledge, explain this to the user and provide the answer using your own understanding.
-- **Only include inline citations using [source_id] when a <source_id> tag is explicitly provided in the context.**  
+- **Only include inline citations using [source_id] (e.g., [1], [2]) when a `<source_id>` tag is explicitly provided in the context.**
 - Do not cite if the <source_id> tag is not provided in the context.  
 - Do not cite if the <source_id> tag is not provided in the context.  
 - Do not use XML tags in your response.
 - Do not use XML tags in your response.
 - Ensure citations are concise and directly related to the information provided.
 - Ensure citations are concise and directly related to the information provided.
@@ -1700,6 +1795,12 @@ RAG_WEB_SEARCH_ENGINE = PersistentConfig(
     os.getenv("RAG_WEB_SEARCH_ENGINE", ""),
     os.getenv("RAG_WEB_SEARCH_ENGINE", ""),
 )
 )
 
 
+RAG_WEB_SEARCH_FULL_CONTEXT = PersistentConfig(
+    "RAG_WEB_SEARCH_FULL_CONTEXT",
+    "rag.web.search.full_context",
+    os.getenv("RAG_WEB_SEARCH_FULL_CONTEXT", "False").lower() == "true",
+)
+
 # You can provide a list of your own websites to filter after performing a web search.
 # You can provide a list of your own websites to filter after performing a web search.
 # This ensures the highest level of safety and reliability of the information sources.
 # This ensures the highest level of safety and reliability of the information sources.
 RAG_WEB_SEARCH_DOMAIN_FILTER_LIST = PersistentConfig(
 RAG_WEB_SEARCH_DOMAIN_FILTER_LIST = PersistentConfig(
@@ -1803,6 +1904,18 @@ SEARCHAPI_ENGINE = PersistentConfig(
     os.getenv("SEARCHAPI_ENGINE", ""),
     os.getenv("SEARCHAPI_ENGINE", ""),
 )
 )
 
 
+SERPAPI_API_KEY = PersistentConfig(
+    "SERPAPI_API_KEY",
+    "rag.web.search.serpapi_api_key",
+    os.getenv("SERPAPI_API_KEY", ""),
+)
+
+SERPAPI_ENGINE = PersistentConfig(
+    "SERPAPI_ENGINE",
+    "rag.web.search.serpapi_engine",
+    os.getenv("SERPAPI_ENGINE", ""),
+)
+
 BING_SEARCH_V7_ENDPOINT = PersistentConfig(
 BING_SEARCH_V7_ENDPOINT = PersistentConfig(
     "BING_SEARCH_V7_ENDPOINT",
     "BING_SEARCH_V7_ENDPOINT",
     "rag.web.search.bing_search_v7_endpoint",
     "rag.web.search.bing_search_v7_endpoint",
@@ -1835,6 +1948,35 @@ RAG_WEB_SEARCH_CONCURRENT_REQUESTS = PersistentConfig(
     int(os.getenv("RAG_WEB_SEARCH_CONCURRENT_REQUESTS", "10")),
     int(os.getenv("RAG_WEB_SEARCH_CONCURRENT_REQUESTS", "10")),
 )
 )
 
 
+RAG_WEB_LOADER_ENGINE = PersistentConfig(
+    "RAG_WEB_LOADER_ENGINE",
+    "rag.web.loader.engine",
+    os.environ.get("RAG_WEB_LOADER_ENGINE", "safe_web"),
+)
+
+RAG_WEB_SEARCH_TRUST_ENV = PersistentConfig(
+    "RAG_WEB_SEARCH_TRUST_ENV",
+    "rag.web.search.trust_env",
+    os.getenv("RAG_WEB_SEARCH_TRUST_ENV", "False").lower() == "true",
+)
+
+PLAYWRIGHT_WS_URI = PersistentConfig(
+    "PLAYWRIGHT_WS_URI",
+    "rag.web.loader.engine.playwright.ws.uri",
+    os.environ.get("PLAYWRIGHT_WS_URI", None),
+)
+
+FIRECRAWL_API_KEY = PersistentConfig(
+    "FIRECRAWL_API_KEY",
+    "firecrawl.api_key",
+    os.environ.get("FIRECRAWL_API_KEY", ""),
+)
+
+FIRECRAWL_API_BASE_URL = PersistentConfig(
+    "FIRECRAWL_API_BASE_URL",
+    "firecrawl.api_url",
+    os.environ.get("FIRECRAWL_API_BASE_URL", "https://api.firecrawl.dev"),
+)
 
 
 ####################################
 ####################################
 # Images
 # Images
@@ -2046,6 +2188,17 @@ IMAGES_OPENAI_API_KEY = PersistentConfig(
     os.getenv("IMAGES_OPENAI_API_KEY", OPENAI_API_KEY),
     os.getenv("IMAGES_OPENAI_API_KEY", OPENAI_API_KEY),
 )
 )
 
 
+IMAGES_GEMINI_API_BASE_URL = PersistentConfig(
+    "IMAGES_GEMINI_API_BASE_URL",
+    "image_generation.gemini.api_base_url",
+    os.getenv("IMAGES_GEMINI_API_BASE_URL", GEMINI_API_BASE_URL),
+)
+IMAGES_GEMINI_API_KEY = PersistentConfig(
+    "IMAGES_GEMINI_API_KEY",
+    "image_generation.gemini.api_key",
+    os.getenv("IMAGES_GEMINI_API_KEY", GEMINI_API_KEY),
+)
+
 IMAGE_SIZE = PersistentConfig(
 IMAGE_SIZE = PersistentConfig(
     "IMAGE_SIZE", "image_generation.size", os.getenv("IMAGE_SIZE", "512x512")
     "IMAGE_SIZE", "image_generation.size", os.getenv("IMAGE_SIZE", "512x512")
 )
 )

+ 1 - 0
backend/open_webui/env.py

@@ -113,6 +113,7 @@ if WEBUI_NAME != "Open WebUI":
 
 
 WEBUI_FAVICON_URL = "https://openwebui.com/favicon.png"
 WEBUI_FAVICON_URL = "https://openwebui.com/favicon.png"
 
 
+TRUSTED_SIGNATURE_KEY = os.environ.get("TRUSTED_SIGNATURE_KEY", "")
 
 
 ####################################
 ####################################
 # ENV (dev,test,prod)
 # ENV (dev,test,prod)

+ 74 - 18
backend/open_webui/main.py

@@ -88,6 +88,7 @@ from open_webui.models.models import Models
 from open_webui.models.users import UserModel, Users
 from open_webui.models.users import UserModel, Users
 
 
 from open_webui.config import (
 from open_webui.config import (
+    LICENSE_KEY,
     # Ollama
     # Ollama
     ENABLE_OLLAMA_API,
     ENABLE_OLLAMA_API,
     OLLAMA_BASE_URLS,
     OLLAMA_BASE_URLS,
@@ -99,7 +100,13 @@ from open_webui.config import (
     OPENAI_API_CONFIGS,
     OPENAI_API_CONFIGS,
     # Direct Connections
     # Direct Connections
     ENABLE_DIRECT_CONNECTIONS,
     ENABLE_DIRECT_CONNECTIONS,
-    # Code Interpreter
+    # Code Execution
+    CODE_EXECUTION_ENGINE,
+    CODE_EXECUTION_JUPYTER_URL,
+    CODE_EXECUTION_JUPYTER_AUTH,
+    CODE_EXECUTION_JUPYTER_AUTH_TOKEN,
+    CODE_EXECUTION_JUPYTER_AUTH_PASSWORD,
+    CODE_EXECUTION_JUPYTER_TIMEOUT,
     ENABLE_CODE_INTERPRETER,
     ENABLE_CODE_INTERPRETER,
     CODE_INTERPRETER_ENGINE,
     CODE_INTERPRETER_ENGINE,
     CODE_INTERPRETER_PROMPT_TEMPLATE,
     CODE_INTERPRETER_PROMPT_TEMPLATE,
@@ -107,6 +114,7 @@ from open_webui.config import (
     CODE_INTERPRETER_JUPYTER_AUTH,
     CODE_INTERPRETER_JUPYTER_AUTH,
     CODE_INTERPRETER_JUPYTER_AUTH_TOKEN,
     CODE_INTERPRETER_JUPYTER_AUTH_TOKEN,
     CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD,
     CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD,
+    CODE_INTERPRETER_JUPYTER_TIMEOUT,
     # Image
     # Image
     AUTOMATIC1111_API_AUTH,
     AUTOMATIC1111_API_AUTH,
     AUTOMATIC1111_BASE_URL,
     AUTOMATIC1111_BASE_URL,
@@ -125,6 +133,8 @@ from open_webui.config import (
     IMAGE_STEPS,
     IMAGE_STEPS,
     IMAGES_OPENAI_API_BASE_URL,
     IMAGES_OPENAI_API_BASE_URL,
     IMAGES_OPENAI_API_KEY,
     IMAGES_OPENAI_API_KEY,
+    IMAGES_GEMINI_API_BASE_URL,
+    IMAGES_GEMINI_API_KEY,
     # Audio
     # Audio
     AUDIO_STT_ENGINE,
     AUDIO_STT_ENGINE,
     AUDIO_STT_MODEL,
     AUDIO_STT_MODEL,
@@ -139,6 +149,10 @@ from open_webui.config import (
     AUDIO_TTS_VOICE,
     AUDIO_TTS_VOICE,
     AUDIO_TTS_AZURE_SPEECH_REGION,
     AUDIO_TTS_AZURE_SPEECH_REGION,
     AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT,
     AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT,
+    PLAYWRIGHT_WS_URI,
+    FIRECRAWL_API_BASE_URL,
+    FIRECRAWL_API_KEY,
+    RAG_WEB_LOADER_ENGINE,
     WHISPER_MODEL,
     WHISPER_MODEL,
     DEEPGRAM_API_KEY,
     DEEPGRAM_API_KEY,
     WHISPER_MODEL_AUTO_UPDATE,
     WHISPER_MODEL_AUTO_UPDATE,
@@ -146,6 +160,7 @@ from open_webui.config import (
     # Retrieval
     # Retrieval
     RAG_TEMPLATE,
     RAG_TEMPLATE,
     DEFAULT_RAG_TEMPLATE,
     DEFAULT_RAG_TEMPLATE,
+    RAG_FULL_CONTEXT,
     RAG_EMBEDDING_MODEL,
     RAG_EMBEDDING_MODEL,
     RAG_EMBEDDING_MODEL_AUTO_UPDATE,
     RAG_EMBEDDING_MODEL_AUTO_UPDATE,
     RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE,
     RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE,
@@ -173,12 +188,16 @@ from open_webui.config import (
     YOUTUBE_LOADER_PROXY_URL,
     YOUTUBE_LOADER_PROXY_URL,
     # Retrieval (Web Search)
     # Retrieval (Web Search)
     RAG_WEB_SEARCH_ENGINE,
     RAG_WEB_SEARCH_ENGINE,
+    RAG_WEB_SEARCH_FULL_CONTEXT,
     RAG_WEB_SEARCH_RESULT_COUNT,
     RAG_WEB_SEARCH_RESULT_COUNT,
     RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
     RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
+    RAG_WEB_SEARCH_TRUST_ENV,
     RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
     RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
     JINA_API_KEY,
     JINA_API_KEY,
     SEARCHAPI_API_KEY,
     SEARCHAPI_API_KEY,
     SEARCHAPI_ENGINE,
     SEARCHAPI_ENGINE,
+    SERPAPI_API_KEY,
+    SERPAPI_ENGINE,
     SEARXNG_QUERY_URL,
     SEARXNG_QUERY_URL,
     SERPER_API_KEY,
     SERPER_API_KEY,
     SERPLY_API_KEY,
     SERPLY_API_KEY,
@@ -264,6 +283,7 @@ from open_webui.config import (
     TASK_MODEL,
     TASK_MODEL,
     TASK_MODEL_EXTERNAL,
     TASK_MODEL_EXTERNAL,
     ENABLE_TAGS_GENERATION,
     ENABLE_TAGS_GENERATION,
+    ENABLE_TITLE_GENERATION,
     ENABLE_SEARCH_QUERY_GENERATION,
     ENABLE_SEARCH_QUERY_GENERATION,
     ENABLE_RETRIEVAL_QUERY_GENERATION,
     ENABLE_RETRIEVAL_QUERY_GENERATION,
     ENABLE_AUTOCOMPLETE_GENERATION,
     ENABLE_AUTOCOMPLETE_GENERATION,
@@ -310,15 +330,17 @@ from open_webui.utils.middleware import process_chat_payload, process_chat_respo
 from open_webui.utils.access_control import has_access
 from open_webui.utils.access_control import has_access
 
 
 from open_webui.utils.auth import (
 from open_webui.utils.auth import (
+    get_license_data,
     decode_token,
     decode_token,
     get_admin_user,
     get_admin_user,
     get_verified_user,
     get_verified_user,
 )
 )
-from open_webui.utils.oauth import oauth_manager
+from open_webui.utils.oauth import OAuthManager
 from open_webui.utils.security_headers import SecurityHeadersMiddleware
 from open_webui.utils.security_headers import SecurityHeadersMiddleware
 
 
 from open_webui.tasks import stop_task, list_tasks  # Import from tasks.py
 from open_webui.tasks import stop_task, list_tasks  # Import from tasks.py
 
 
+
 if SAFE_MODE:
 if SAFE_MODE:
     print("SAFE MODE ENABLED")
     print("SAFE MODE ENABLED")
     Functions.deactivate_all_functions()
     Functions.deactivate_all_functions()
@@ -345,12 +367,12 @@ class SPAStaticFiles(StaticFiles):
 
 
 print(
 print(
     rf"""
     rf"""
-  ___                    __        __   _     _   _ ___
- / _ \ _ __   ___ _ __   \ \      / /__| |__ | | | |_ _|
-| | | | '_ \ / _ \ '_ \   \ \ /\ / / _ \ '_ \| | | || |
-| |_| | |_) |  __/ | | |   \ V  V /  __/ |_) | |_| || |
- \___/| .__/ \___|_| |_|    \_/\_/ \___|_.__/ \___/|___|
-      |_|
+ ██████╗ ██████╗ ███████╗███╗   ██╗    ██╗    ██╗███████╗██████╗ ██╗   ██╗██╗
+██╔═══██╗██╔══██╗██╔════╝████╗  ██║    ██║    ██║██╔════╝██╔══██╗██║   ██║██║
+██║   ██║██████╔╝█████╗  ██╔██╗ ██║    ██║ █╗ ██║█████╗  ██████╔╝██║   ██║██║
+██║   ██║██╔═══╝ ██╔══╝  ██║╚██╗██║    ██║███╗██║██╔══╝  ██╔══██╗██║   ██║██║
+╚██████╔╝██║     ███████╗██║ ╚████║    ╚███╔███╔╝███████╗██████╔╝╚██████╔╝██║
+ ╚═════╝ ╚═╝     ╚══════╝╚═╝  ╚═══╝     ╚══╝╚══╝ ╚══════╝╚═════╝  ╚═════╝ ╚═╝
 
 
 
 
 v{VERSION} - building the best open-source AI user interface.
 v{VERSION} - building the best open-source AI user interface.
@@ -365,6 +387,9 @@ async def lifespan(app: FastAPI):
     if RESET_CONFIG_ON_START:
     if RESET_CONFIG_ON_START:
         reset_config()
         reset_config()
 
 
+    if app.state.config.LICENSE_KEY:
+        get_license_data(app, app.state.config.LICENSE_KEY)
+
     asyncio.create_task(periodic_usage_pool_cleanup())
     asyncio.create_task(periodic_usage_pool_cleanup())
     yield
     yield
 
 
@@ -376,8 +401,12 @@ app = FastAPI(
     lifespan=lifespan,
     lifespan=lifespan,
 )
 )
 
 
+oauth_manager = OAuthManager(app)
+
 app.state.config = AppConfig()
 app.state.config = AppConfig()
 
 
+app.state.WEBUI_NAME = WEBUI_NAME
+app.state.config.LICENSE_KEY = LICENSE_KEY
 
 
 ########################################
 ########################################
 #
 #
@@ -479,10 +508,10 @@ app.state.config.LDAP_CIPHERS = LDAP_CIPHERS
 app.state.AUTH_TRUSTED_EMAIL_HEADER = WEBUI_AUTH_TRUSTED_EMAIL_HEADER
 app.state.AUTH_TRUSTED_EMAIL_HEADER = WEBUI_AUTH_TRUSTED_EMAIL_HEADER
 app.state.AUTH_TRUSTED_NAME_HEADER = WEBUI_AUTH_TRUSTED_NAME_HEADER
 app.state.AUTH_TRUSTED_NAME_HEADER = WEBUI_AUTH_TRUSTED_NAME_HEADER
 
 
+app.state.USER_COUNT = None
 app.state.TOOLS = {}
 app.state.TOOLS = {}
 app.state.FUNCTIONS = {}
 app.state.FUNCTIONS = {}
 
 
-
 ########################################
 ########################################
 #
 #
 # RETRIEVAL
 # RETRIEVAL
@@ -495,6 +524,8 @@ app.state.config.RELEVANCE_THRESHOLD = RAG_RELEVANCE_THRESHOLD
 app.state.config.FILE_MAX_SIZE = RAG_FILE_MAX_SIZE
 app.state.config.FILE_MAX_SIZE = RAG_FILE_MAX_SIZE
 app.state.config.FILE_MAX_COUNT = RAG_FILE_MAX_COUNT
 app.state.config.FILE_MAX_COUNT = RAG_FILE_MAX_COUNT
 
 
+
+app.state.config.RAG_FULL_CONTEXT = RAG_FULL_CONTEXT
 app.state.config.ENABLE_RAG_HYBRID_SEARCH = ENABLE_RAG_HYBRID_SEARCH
 app.state.config.ENABLE_RAG_HYBRID_SEARCH = ENABLE_RAG_HYBRID_SEARCH
 app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = (
 app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = (
     ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION
     ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION
@@ -529,6 +560,7 @@ app.state.config.YOUTUBE_LOADER_PROXY_URL = YOUTUBE_LOADER_PROXY_URL
 
 
 app.state.config.ENABLE_RAG_WEB_SEARCH = ENABLE_RAG_WEB_SEARCH
 app.state.config.ENABLE_RAG_WEB_SEARCH = ENABLE_RAG_WEB_SEARCH
 app.state.config.RAG_WEB_SEARCH_ENGINE = RAG_WEB_SEARCH_ENGINE
 app.state.config.RAG_WEB_SEARCH_ENGINE = RAG_WEB_SEARCH_ENGINE
+app.state.config.RAG_WEB_SEARCH_FULL_CONTEXT = RAG_WEB_SEARCH_FULL_CONTEXT
 app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST = RAG_WEB_SEARCH_DOMAIN_FILTER_LIST
 app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST = RAG_WEB_SEARCH_DOMAIN_FILTER_LIST
 
 
 app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION = ENABLE_GOOGLE_DRIVE_INTEGRATION
 app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION = ENABLE_GOOGLE_DRIVE_INTEGRATION
@@ -546,6 +578,8 @@ app.state.config.SERPLY_API_KEY = SERPLY_API_KEY
 app.state.config.TAVILY_API_KEY = TAVILY_API_KEY
 app.state.config.TAVILY_API_KEY = TAVILY_API_KEY
 app.state.config.SEARCHAPI_API_KEY = SEARCHAPI_API_KEY
 app.state.config.SEARCHAPI_API_KEY = SEARCHAPI_API_KEY
 app.state.config.SEARCHAPI_ENGINE = SEARCHAPI_ENGINE
 app.state.config.SEARCHAPI_ENGINE = SEARCHAPI_ENGINE
+app.state.config.SERPAPI_API_KEY = SERPAPI_API_KEY
+app.state.config.SERPAPI_ENGINE = SERPAPI_ENGINE
 app.state.config.JINA_API_KEY = JINA_API_KEY
 app.state.config.JINA_API_KEY = JINA_API_KEY
 app.state.config.BING_SEARCH_V7_ENDPOINT = BING_SEARCH_V7_ENDPOINT
 app.state.config.BING_SEARCH_V7_ENDPOINT = BING_SEARCH_V7_ENDPOINT
 app.state.config.BING_SEARCH_V7_SUBSCRIPTION_KEY = BING_SEARCH_V7_SUBSCRIPTION_KEY
 app.state.config.BING_SEARCH_V7_SUBSCRIPTION_KEY = BING_SEARCH_V7_SUBSCRIPTION_KEY
@@ -553,6 +587,11 @@ app.state.config.EXA_API_KEY = EXA_API_KEY
 
 
 app.state.config.RAG_WEB_SEARCH_RESULT_COUNT = RAG_WEB_SEARCH_RESULT_COUNT
 app.state.config.RAG_WEB_SEARCH_RESULT_COUNT = RAG_WEB_SEARCH_RESULT_COUNT
 app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS = RAG_WEB_SEARCH_CONCURRENT_REQUESTS
 app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS = RAG_WEB_SEARCH_CONCURRENT_REQUESTS
+app.state.config.RAG_WEB_LOADER_ENGINE = RAG_WEB_LOADER_ENGINE
+app.state.config.RAG_WEB_SEARCH_TRUST_ENV = RAG_WEB_SEARCH_TRUST_ENV
+app.state.config.PLAYWRIGHT_WS_URI = PLAYWRIGHT_WS_URI
+app.state.config.FIRECRAWL_API_BASE_URL = FIRECRAWL_API_BASE_URL
+app.state.config.FIRECRAWL_API_KEY = FIRECRAWL_API_KEY
 
 
 app.state.EMBEDDING_FUNCTION = None
 app.state.EMBEDDING_FUNCTION = None
 app.state.ef = None
 app.state.ef = None
@@ -596,10 +635,19 @@ app.state.EMBEDDING_FUNCTION = get_embedding_function(
 
 
 ########################################
 ########################################
 #
 #
-# CODE INTERPRETER
+# CODE EXECUTION
 #
 #
 ########################################
 ########################################
 
 
+app.state.config.CODE_EXECUTION_ENGINE = CODE_EXECUTION_ENGINE
+app.state.config.CODE_EXECUTION_JUPYTER_URL = CODE_EXECUTION_JUPYTER_URL
+app.state.config.CODE_EXECUTION_JUPYTER_AUTH = CODE_EXECUTION_JUPYTER_AUTH
+app.state.config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN = CODE_EXECUTION_JUPYTER_AUTH_TOKEN
+app.state.config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD = (
+    CODE_EXECUTION_JUPYTER_AUTH_PASSWORD
+)
+app.state.config.CODE_EXECUTION_JUPYTER_TIMEOUT = CODE_EXECUTION_JUPYTER_TIMEOUT
+
 app.state.config.ENABLE_CODE_INTERPRETER = ENABLE_CODE_INTERPRETER
 app.state.config.ENABLE_CODE_INTERPRETER = ENABLE_CODE_INTERPRETER
 app.state.config.CODE_INTERPRETER_ENGINE = CODE_INTERPRETER_ENGINE
 app.state.config.CODE_INTERPRETER_ENGINE = CODE_INTERPRETER_ENGINE
 app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE = CODE_INTERPRETER_PROMPT_TEMPLATE
 app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE = CODE_INTERPRETER_PROMPT_TEMPLATE
@@ -612,6 +660,7 @@ app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN = (
 app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD = (
 app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD = (
     CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD
     CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD
 )
 )
+app.state.config.CODE_INTERPRETER_JUPYTER_TIMEOUT = CODE_INTERPRETER_JUPYTER_TIMEOUT
 
 
 ########################################
 ########################################
 #
 #
@@ -626,6 +675,9 @@ app.state.config.ENABLE_IMAGE_PROMPT_GENERATION = ENABLE_IMAGE_PROMPT_GENERATION
 app.state.config.IMAGES_OPENAI_API_BASE_URL = IMAGES_OPENAI_API_BASE_URL
 app.state.config.IMAGES_OPENAI_API_BASE_URL = IMAGES_OPENAI_API_BASE_URL
 app.state.config.IMAGES_OPENAI_API_KEY = IMAGES_OPENAI_API_KEY
 app.state.config.IMAGES_OPENAI_API_KEY = IMAGES_OPENAI_API_KEY
 
 
+app.state.config.IMAGES_GEMINI_API_BASE_URL = IMAGES_GEMINI_API_BASE_URL
+app.state.config.IMAGES_GEMINI_API_KEY = IMAGES_GEMINI_API_KEY
+
 app.state.config.IMAGE_GENERATION_MODEL = IMAGE_GENERATION_MODEL
 app.state.config.IMAGE_GENERATION_MODEL = IMAGE_GENERATION_MODEL
 
 
 app.state.config.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
 app.state.config.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
@@ -689,6 +741,7 @@ app.state.config.ENABLE_SEARCH_QUERY_GENERATION = ENABLE_SEARCH_QUERY_GENERATION
 app.state.config.ENABLE_RETRIEVAL_QUERY_GENERATION = ENABLE_RETRIEVAL_QUERY_GENERATION
 app.state.config.ENABLE_RETRIEVAL_QUERY_GENERATION = ENABLE_RETRIEVAL_QUERY_GENERATION
 app.state.config.ENABLE_AUTOCOMPLETE_GENERATION = ENABLE_AUTOCOMPLETE_GENERATION
 app.state.config.ENABLE_AUTOCOMPLETE_GENERATION = ENABLE_AUTOCOMPLETE_GENERATION
 app.state.config.ENABLE_TAGS_GENERATION = ENABLE_TAGS_GENERATION
 app.state.config.ENABLE_TAGS_GENERATION = ENABLE_TAGS_GENERATION
+app.state.config.ENABLE_TITLE_GENERATION = ENABLE_TITLE_GENERATION
 
 
 
 
 app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE = TITLE_GENERATION_PROMPT_TEMPLATE
 app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE = TITLE_GENERATION_PROMPT_TEMPLATE
@@ -934,7 +987,7 @@ async def chat_completion(
             "files": form_data.get("files", None),
             "files": form_data.get("files", None),
             "features": form_data.get("features", None),
             "features": form_data.get("features", None),
             "variables": form_data.get("variables", None),
             "variables": form_data.get("variables", None),
-            "model": model_info,
+            "model": model_info.model_dump() if model_info else model,
             "direct": model_item.get("direct", False),
             "direct": model_item.get("direct", False),
             **(
             **(
                 {"function_calling": "native"}
                 {"function_calling": "native"}
@@ -1063,7 +1116,7 @@ async def get_app_config(request: Request):
     return {
     return {
         **({"onboarding": True} if onboarding else {}),
         **({"onboarding": True} if onboarding else {}),
         "status": True,
         "status": True,
-        "name": WEBUI_NAME,
+        "name": app.state.WEBUI_NAME,
         "version": VERSION,
         "version": VERSION,
         "default_locale": str(DEFAULT_LOCALE),
         "default_locale": str(DEFAULT_LOCALE),
         "oauth": {
         "oauth": {
@@ -1102,6 +1155,9 @@ async def get_app_config(request: Request):
             {
             {
                 "default_models": app.state.config.DEFAULT_MODELS,
                 "default_models": app.state.config.DEFAULT_MODELS,
                 "default_prompt_suggestions": app.state.config.DEFAULT_PROMPT_SUGGESTIONS,
                 "default_prompt_suggestions": app.state.config.DEFAULT_PROMPT_SUGGESTIONS,
+                "code": {
+                    "engine": app.state.config.CODE_EXECUTION_ENGINE,
+                },
                 "audio": {
                 "audio": {
                     "tts": {
                     "tts": {
                         "engine": app.state.config.TTS_ENGINE,
                         "engine": app.state.config.TTS_ENGINE,
@@ -1198,7 +1254,7 @@ if len(OAUTH_PROVIDERS) > 0:
 
 
 @app.get("/oauth/{provider}/login")
 @app.get("/oauth/{provider}/login")
 async def oauth_login(provider: str, request: Request):
 async def oauth_login(provider: str, request: Request):
-    return await oauth_manager.handle_login(provider, request)
+    return await oauth_manager.handle_login(request, provider)
 
 
 
 
 # OAuth login logic is as follows:
 # OAuth login logic is as follows:
@@ -1209,14 +1265,14 @@ async def oauth_login(provider: str, request: Request):
 #    - Email addresses are considered unique, so we fail registration if the email address is already taken
 #    - Email addresses are considered unique, so we fail registration if the email address is already taken
 @app.get("/oauth/{provider}/callback")
 @app.get("/oauth/{provider}/callback")
 async def oauth_callback(provider: str, request: Request, response: Response):
 async def oauth_callback(provider: str, request: Request, response: Response):
-    return await oauth_manager.handle_callback(provider, request, response)
+    return await oauth_manager.handle_callback(request, provider, response)
 
 
 
 
 @app.get("/manifest.json")
 @app.get("/manifest.json")
 async def get_manifest_json():
 async def get_manifest_json():
     return {
     return {
-        "name": WEBUI_NAME,
-        "short_name": WEBUI_NAME,
+        "name": app.state.WEBUI_NAME,
+        "short_name": app.state.WEBUI_NAME,
         "description": "Open WebUI is an open, extensible, user-friendly interface for AI that adapts to your workflow.",
         "description": "Open WebUI is an open, extensible, user-friendly interface for AI that adapts to your workflow.",
         "start_url": "/",
         "start_url": "/",
         "display": "standalone",
         "display": "standalone",
@@ -1243,8 +1299,8 @@ async def get_manifest_json():
 async def get_opensearch_xml():
 async def get_opensearch_xml():
     xml_content = rf"""
     xml_content = rf"""
     <OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/" xmlns:moz="http://www.mozilla.org/2006/browser/search/">
     <OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/" xmlns:moz="http://www.mozilla.org/2006/browser/search/">
-    <ShortName>{WEBUI_NAME}</ShortName>
-    <Description>Search {WEBUI_NAME}</Description>
+    <ShortName>{app.state.WEBUI_NAME}</ShortName>
+    <Description>Search {app.state.WEBUI_NAME}</Description>
     <InputEncoding>UTF-8</InputEncoding>
     <InputEncoding>UTF-8</InputEncoding>
     <Image width="16" height="16" type="image/x-icon">{app.state.config.WEBUI_URL}/static/favicon.png</Image>
     <Image width="16" height="16" type="image/x-icon">{app.state.config.WEBUI_URL}/static/favicon.png</Image>
     <Url type="text/html" method="get" template="{app.state.config.WEBUI_URL}/?q={"{searchTerms}"}"/>
     <Url type="text/html" method="get" template="{app.state.config.WEBUI_URL}/?q={"{searchTerms}"}"/>

+ 95 - 28
backend/open_webui/retrieval/utils.py

@@ -14,7 +14,8 @@ from langchain_core.documents import Document
 
 
 from open_webui.config import VECTOR_DB
 from open_webui.config import VECTOR_DB
 from open_webui.retrieval.vector.connector import VECTOR_DB_CLIENT
 from open_webui.retrieval.vector.connector import VECTOR_DB_CLIENT
-from open_webui.utils.misc import get_last_user_message
+from open_webui.utils.misc import get_last_user_message, calculate_sha256_string
+
 from open_webui.models.users import UserModel
 from open_webui.models.users import UserModel
 
 
 from open_webui.env import (
 from open_webui.env import (
@@ -84,6 +85,19 @@ def query_doc(
         raise e
         raise e
 
 
 
 
+def get_doc(collection_name: str, user: UserModel = None):
+    try:
+        result = VECTOR_DB_CLIENT.get(collection_name=collection_name)
+
+        if result:
+            log.info(f"query_doc:result {result.ids} {result.metadatas}")
+
+        return result
+    except Exception as e:
+        print(e)
+        raise e
+
+
 def query_doc_with_hybrid_search(
 def query_doc_with_hybrid_search(
     collection_name: str,
     collection_name: str,
     query: str,
     query: str,
@@ -137,6 +151,27 @@ def query_doc_with_hybrid_search(
         raise e
         raise e
 
 
 
 
+def merge_get_results(get_results: list[dict]) -> dict:
+    # Initialize lists to store combined data
+    combined_documents = []
+    combined_metadatas = []
+    combined_ids = []
+
+    for data in get_results:
+        combined_documents.extend(data["documents"][0])
+        combined_metadatas.extend(data["metadatas"][0])
+        combined_ids.extend(data["ids"][0])
+
+    # Create the output dictionary
+    result = {
+        "documents": [combined_documents],
+        "metadatas": [combined_metadatas],
+        "ids": [combined_ids],
+    }
+
+    return result
+
+
 def merge_and_sort_query_results(
 def merge_and_sort_query_results(
     query_results: list[dict], k: int, reverse: bool = False
     query_results: list[dict], k: int, reverse: bool = False
 ) -> list[dict]:
 ) -> list[dict]:
@@ -180,6 +215,23 @@ def merge_and_sort_query_results(
     return result
     return result
 
 
 
 
+def get_all_items_from_collections(collection_names: list[str]) -> dict:
+    results = []
+
+    for collection_name in collection_names:
+        if collection_name:
+            try:
+                result = get_doc(collection_name=collection_name)
+                if result is not None:
+                    results.append(result.model_dump())
+            except Exception as e:
+                log.exception(f"Error when querying the collection: {e}")
+        else:
+            pass
+
+    return merge_get_results(results)
+
+
 def query_collection(
 def query_collection(
     collection_names: list[str],
     collection_names: list[str],
     queries: list[str],
     queries: list[str],
@@ -297,14 +349,22 @@ def get_sources_from_files(
     reranking_function,
     reranking_function,
     r,
     r,
     hybrid_search,
     hybrid_search,
+    full_context=False,
 ):
 ):
-    log.debug(f"files: {files} {queries} {embedding_function} {reranking_function}")
+    log.debug(
+        f"files: {files} {queries} {embedding_function} {reranking_function} {full_context}"
+    )
 
 
     extracted_collections = []
     extracted_collections = []
     relevant_contexts = []
     relevant_contexts = []
 
 
     for file in files:
     for file in files:
-        if file.get("context") == "full":
+        if file.get("docs"):
+            context = {
+                "documents": [[doc.get("content") for doc in file.get("docs")]],
+                "metadatas": [[doc.get("metadata") for doc in file.get("docs")]],
+            }
+        elif file.get("context") == "full":
             context = {
             context = {
                 "documents": [[file.get("file").get("data", {}).get("content")]],
                 "documents": [[file.get("file").get("data", {}).get("content")]],
                 "metadatas": [[{"file_id": file.get("id"), "name": file.get("name")}]],
                 "metadatas": [[{"file_id": file.get("id"), "name": file.get("name")}]],
@@ -331,36 +391,43 @@ def get_sources_from_files(
                 log.debug(f"skipping {file} as it has already been extracted")
                 log.debug(f"skipping {file} as it has already been extracted")
                 continue
                 continue
 
 
-            try:
-                context = None
-                if file.get("type") == "text":
-                    context = file["content"]
-                else:
-                    if hybrid_search:
-                        try:
-                            context = query_collection_with_hybrid_search(
+            if full_context:
+                try:
+                    context = get_all_items_from_collections(collection_names)
+                except Exception as e:
+                    log.exception(e)
+
+            else:
+                try:
+                    context = None
+                    if file.get("type") == "text":
+                        context = file["content"]
+                    else:
+                        if hybrid_search:
+                            try:
+                                context = query_collection_with_hybrid_search(
+                                    collection_names=collection_names,
+                                    queries=queries,
+                                    embedding_function=embedding_function,
+                                    k=k,
+                                    reranking_function=reranking_function,
+                                    r=r,
+                                )
+                            except Exception as e:
+                                log.debug(
+                                    "Error when using hybrid search, using"
+                                    " non hybrid search as fallback."
+                                )
+
+                        if (not hybrid_search) or (context is None):
+                            context = query_collection(
                                 collection_names=collection_names,
                                 collection_names=collection_names,
                                 queries=queries,
                                 queries=queries,
                                 embedding_function=embedding_function,
                                 embedding_function=embedding_function,
                                 k=k,
                                 k=k,
-                                reranking_function=reranking_function,
-                                r=r,
-                            )
-                        except Exception as e:
-                            log.debug(
-                                "Error when using hybrid search, using"
-                                " non hybrid search as fallback."
                             )
                             )
-
-                    if (not hybrid_search) or (context is None):
-                        context = query_collection(
-                            collection_names=collection_names,
-                            queries=queries,
-                            embedding_function=embedding_function,
-                            k=k,
-                        )
-            except Exception as e:
-                log.exception(e)
+                except Exception as e:
+                    log.exception(e)
 
 
             extracted_collections.extend(collection_names)
             extracted_collections.extend(collection_names)
 
 

+ 10 - 14
backend/open_webui/retrieval/web/duckduckgo.py

@@ -32,19 +32,15 @@ def search_duckduckgo(
             # Convert the search results into a list
             # Convert the search results into a list
             search_results = [r for r in ddgs_gen]
             search_results = [r for r in ddgs_gen]
 
 
-    # Create an empty list to store the SearchResult objects
-    results = []
-    # Iterate over each search result
-    for result in search_results:
-        # Create a SearchResult object and append it to the results list
-        results.append(
-            SearchResult(
-                link=result["href"],
-                title=result.get("title"),
-                snippet=result.get("body"),
-            )
-        )
     if filter_list:
     if filter_list:
-        results = get_filtered_results(results, filter_list)
+        search_results = get_filtered_results(search_results, filter_list)
+
     # Return the list of search results
     # Return the list of search results
-    return results
+    return [
+        SearchResult(
+            link=result["href"],
+            title=result.get("title"),
+            snippet=result.get("body"),
+        )
+        for result in search_results
+    ]

+ 48 - 0
backend/open_webui/retrieval/web/serpapi.py

@@ -0,0 +1,48 @@
+import logging
+from typing import Optional
+from urllib.parse import urlencode
+
+import requests
+from open_webui.retrieval.web.main import SearchResult, get_filtered_results
+from open_webui.env import SRC_LOG_LEVELS
+
+log = logging.getLogger(__name__)
+log.setLevel(SRC_LOG_LEVELS["RAG"])
+
+
+def search_serpapi(
+    api_key: str,
+    engine: str,
+    query: str,
+    count: int,
+    filter_list: Optional[list[str]] = None,
+) -> list[SearchResult]:
+    """Search using serpapi.com's API and return the results as a list of SearchResult objects.
+
+    Args:
+      api_key (str): A serpapi.com API key
+      query (str): The query to search for
+    """
+    url = "https://serpapi.com/search"
+
+    engine = engine or "google"
+
+    payload = {"engine": engine, "q": query, "api_key": api_key}
+
+    url = f"{url}?{urlencode(payload)}"
+    response = requests.request("GET", url)
+
+    json_response = response.json()
+    log.info(f"results from serpapi search: {json_response}")
+
+    results = sorted(
+        json_response.get("organic_results", []), key=lambda x: x.get("position", 0)
+    )
+    if filter_list:
+        results = get_filtered_results(results, filter_list)
+    return [
+        SearchResult(
+            link=result["link"], title=result["title"], snippet=result["snippet"]
+        )
+        for result in results[:count]
+    ]

+ 8 - 2
backend/open_webui/retrieval/web/tavily.py

@@ -1,4 +1,5 @@
 import logging
 import logging
+from typing import Optional
 
 
 import requests
 import requests
 from open_webui.retrieval.web.main import SearchResult
 from open_webui.retrieval.web.main import SearchResult
@@ -8,7 +9,13 @@ log = logging.getLogger(__name__)
 log.setLevel(SRC_LOG_LEVELS["RAG"])
 log.setLevel(SRC_LOG_LEVELS["RAG"])
 
 
 
 
-def search_tavily(api_key: str, query: str, count: int) -> list[SearchResult]:
+def search_tavily(
+    api_key: str,
+    query: str,
+    count: int,
+    filter_list: Optional[list[str]] = None,
+    # **kwargs,
+) -> list[SearchResult]:
     """Search using Tavily's Search API and return the results as a list of SearchResult objects.
     """Search using Tavily's Search API and return the results as a list of SearchResult objects.
 
 
     Args:
     Args:
@@ -20,7 +27,6 @@ def search_tavily(api_key: str, query: str, count: int) -> list[SearchResult]:
     """
     """
     url = "https://api.tavily.com/search"
     url = "https://api.tavily.com/search"
     data = {"query": query, "api_key": api_key}
     data = {"query": query, "api_key": api_key}
-
     response = requests.post(url, json=data)
     response = requests.post(url, json=data)
     response.raise_for_status()
     response.raise_for_status()
 
 

+ 455 - 25
backend/open_webui/retrieval/web/utils.py

@@ -1,20 +1,39 @@
+import asyncio
+import logging
 import socket
 import socket
+import ssl
 import urllib.parse
 import urllib.parse
-import validators
-from typing import Union, Sequence, Iterator
-
-from langchain_community.document_loaders import (
-    WebBaseLoader,
+import urllib.request
+from collections import defaultdict
+from datetime import datetime, time, timedelta
+from typing import (
+    Any,
+    AsyncIterator,
+    Dict,
+    Iterator,
+    List,
+    Optional,
+    Sequence,
+    Union,
+    Literal,
 )
 )
+import aiohttp
+import certifi
+import validators
+from langchain_community.document_loaders import PlaywrightURLLoader, WebBaseLoader
+from langchain_community.document_loaders.firecrawl import FireCrawlLoader
+from langchain_community.document_loaders.base import BaseLoader
 from langchain_core.documents import Document
 from langchain_core.documents import Document
-
-
 from open_webui.constants import ERROR_MESSAGES
 from open_webui.constants import ERROR_MESSAGES
-from open_webui.config import ENABLE_RAG_LOCAL_WEB_FETCH
+from open_webui.config import (
+    ENABLE_RAG_LOCAL_WEB_FETCH,
+    PLAYWRIGHT_WS_URI,
+    RAG_WEB_LOADER_ENGINE,
+    FIRECRAWL_API_BASE_URL,
+    FIRECRAWL_API_KEY,
+)
 from open_webui.env import SRC_LOG_LEVELS
 from open_webui.env import SRC_LOG_LEVELS
 
 
-import logging
-
 log = logging.getLogger(__name__)
 log = logging.getLogger(__name__)
 log.setLevel(SRC_LOG_LEVELS["RAG"])
 log.setLevel(SRC_LOG_LEVELS["RAG"])
 
 
@@ -65,9 +84,381 @@ def resolve_hostname(hostname):
     return ipv4_addresses, ipv6_addresses
     return ipv4_addresses, ipv6_addresses
 
 
 
 
+def extract_metadata(soup, url):
+    metadata = {"source": url}
+    if title := soup.find("title"):
+        metadata["title"] = title.get_text()
+    if description := soup.find("meta", attrs={"name": "description"}):
+        metadata["description"] = description.get("content", "No description found.")
+    if html := soup.find("html"):
+        metadata["language"] = html.get("lang", "No language found.")
+    return metadata
+
+
+def verify_ssl_cert(url: str) -> bool:
+    """Verify SSL certificate for the given URL."""
+    if not url.startswith("https://"):
+        return True
+
+    try:
+        hostname = url.split("://")[-1].split("/")[0]
+        context = ssl.create_default_context(cafile=certifi.where())
+        with context.wrap_socket(ssl.socket(), server_hostname=hostname) as s:
+            s.connect((hostname, 443))
+        return True
+    except ssl.SSLError:
+        return False
+    except Exception as e:
+        log.warning(f"SSL verification failed for {url}: {str(e)}")
+        return False
+
+
+class SafeFireCrawlLoader(BaseLoader):
+    def __init__(
+        self,
+        web_paths,
+        verify_ssl: bool = True,
+        trust_env: bool = False,
+        requests_per_second: Optional[float] = None,
+        continue_on_failure: bool = True,
+        api_key: Optional[str] = None,
+        api_url: Optional[str] = None,
+        mode: Literal["crawl", "scrape", "map"] = "crawl",
+        proxy: Optional[Dict[str, str]] = None,
+        params: Optional[Dict] = None,
+    ):
+        """Concurrent document loader for FireCrawl operations.
+
+        Executes multiple FireCrawlLoader instances concurrently using thread pooling
+        to improve bulk processing efficiency.
+        Args:
+            web_paths: List of URLs/paths to process.
+            verify_ssl: If True, verify SSL certificates.
+            trust_env: If True, use proxy settings from environment variables.
+            requests_per_second: Number of requests per second to limit to.
+            continue_on_failure (bool): If True, continue loading other URLs on failure.
+            api_key: API key for FireCrawl service. Defaults to None
+                (uses FIRE_CRAWL_API_KEY environment variable if not provided).
+            api_url: Base URL for FireCrawl API. Defaults to official API endpoint.
+            mode: Operation mode selection:
+                - 'crawl': Website crawling mode (default)
+                - 'scrape': Direct page scraping
+                - 'map': Site map generation
+            proxy: Proxy override settings for the FireCrawl API.
+            params: The parameters to pass to the Firecrawl API.
+                Examples include crawlerOptions.
+                For more details, visit: https://github.com/mendableai/firecrawl-py
+        """
+        proxy_server = proxy.get("server") if proxy else None
+        if trust_env and not proxy_server:
+            env_proxies = urllib.request.getproxies()
+            env_proxy_server = env_proxies.get("https") or env_proxies.get("http")
+            if env_proxy_server:
+                if proxy:
+                    proxy["server"] = env_proxy_server
+                else:
+                    proxy = {"server": env_proxy_server}
+        self.web_paths = web_paths
+        self.verify_ssl = verify_ssl
+        self.requests_per_second = requests_per_second
+        self.last_request_time = None
+        self.trust_env = trust_env
+        self.continue_on_failure = continue_on_failure
+        self.api_key = api_key
+        self.api_url = api_url
+        self.mode = mode
+        self.params = params
+
+    def lazy_load(self) -> Iterator[Document]:
+        """Load documents concurrently using FireCrawl."""
+        for url in self.web_paths:
+            try:
+                self._safe_process_url_sync(url)
+                loader = FireCrawlLoader(
+                    url=url,
+                    api_key=self.api_key,
+                    api_url=self.api_url,
+                    mode=self.mode,
+                    params=self.params,
+                )
+                yield from loader.lazy_load()
+            except Exception as e:
+                if self.continue_on_failure:
+                    log.exception(e, "Error loading %s", url)
+                    continue
+                raise e
+
+    async def alazy_load(self):
+        """Async version of lazy_load."""
+        for url in self.web_paths:
+            try:
+                await self._safe_process_url(url)
+                loader = FireCrawlLoader(
+                    url=url,
+                    api_key=self.api_key,
+                    api_url=self.api_url,
+                    mode=self.mode,
+                    params=self.params,
+                )
+                async for document in loader.alazy_load():
+                    yield document
+            except Exception as e:
+                if self.continue_on_failure:
+                    log.exception(e, "Error loading %s", url)
+                    continue
+                raise e
+
+    def _verify_ssl_cert(self, url: str) -> bool:
+        return verify_ssl_cert(url)
+
+    async def _wait_for_rate_limit(self):
+        """Wait to respect the rate limit if specified."""
+        if self.requests_per_second and self.last_request_time:
+            min_interval = timedelta(seconds=1.0 / self.requests_per_second)
+            time_since_last = datetime.now() - self.last_request_time
+            if time_since_last < min_interval:
+                await asyncio.sleep((min_interval - time_since_last).total_seconds())
+        self.last_request_time = datetime.now()
+
+    def _sync_wait_for_rate_limit(self):
+        """Synchronous version of rate limit wait."""
+        if self.requests_per_second and self.last_request_time:
+            min_interval = timedelta(seconds=1.0 / self.requests_per_second)
+            time_since_last = datetime.now() - self.last_request_time
+            if time_since_last < min_interval:
+                time.sleep((min_interval - time_since_last).total_seconds())
+        self.last_request_time = datetime.now()
+
+    async def _safe_process_url(self, url: str) -> bool:
+        """Perform safety checks before processing a URL."""
+        if self.verify_ssl and not self._verify_ssl_cert(url):
+            raise ValueError(f"SSL certificate verification failed for {url}")
+        await self._wait_for_rate_limit()
+        return True
+
+    def _safe_process_url_sync(self, url: str) -> bool:
+        """Synchronous version of safety checks."""
+        if self.verify_ssl and not self._verify_ssl_cert(url):
+            raise ValueError(f"SSL certificate verification failed for {url}")
+        self._sync_wait_for_rate_limit()
+        return True
+
+
+class SafePlaywrightURLLoader(PlaywrightURLLoader):
+    """Load HTML pages safely with Playwright, supporting SSL verification, rate limiting, and remote browser connection.
+
+    Attributes:
+        web_paths (List[str]): List of URLs to load.
+        verify_ssl (bool): If True, verify SSL certificates.
+        trust_env (bool): If True, use proxy settings from environment variables.
+        requests_per_second (Optional[float]): Number of requests per second to limit to.
+        continue_on_failure (bool): If True, continue loading other URLs on failure.
+        headless (bool): If True, the browser will run in headless mode.
+        proxy (dict): Proxy override settings for the Playwright session.
+        playwright_ws_url (Optional[str]): WebSocket endpoint URI for remote browser connection.
+    """
+
+    def __init__(
+        self,
+        web_paths: List[str],
+        verify_ssl: bool = True,
+        trust_env: bool = False,
+        requests_per_second: Optional[float] = None,
+        continue_on_failure: bool = True,
+        headless: bool = True,
+        remove_selectors: Optional[List[str]] = None,
+        proxy: Optional[Dict[str, str]] = None,
+        playwright_ws_url: Optional[str] = None,
+    ):
+        """Initialize with additional safety parameters and remote browser support."""
+
+        proxy_server = proxy.get("server") if proxy else None
+        if trust_env and not proxy_server:
+            env_proxies = urllib.request.getproxies()
+            env_proxy_server = env_proxies.get("https") or env_proxies.get("http")
+            if env_proxy_server:
+                if proxy:
+                    proxy["server"] = env_proxy_server
+                else:
+                    proxy = {"server": env_proxy_server}
+
+        # We'll set headless to False if using playwright_ws_url since it's handled by the remote browser
+        super().__init__(
+            urls=web_paths,
+            continue_on_failure=continue_on_failure,
+            headless=headless if playwright_ws_url is None else False,
+            remove_selectors=remove_selectors,
+            proxy=proxy,
+        )
+        self.verify_ssl = verify_ssl
+        self.requests_per_second = requests_per_second
+        self.last_request_time = None
+        self.playwright_ws_url = playwright_ws_url
+        self.trust_env = trust_env
+
+    def lazy_load(self) -> Iterator[Document]:
+        """Safely load URLs synchronously with support for remote browser."""
+        from playwright.sync_api import sync_playwright
+
+        with sync_playwright() as p:
+            # Use remote browser if ws_endpoint is provided, otherwise use local browser
+            if self.playwright_ws_url:
+                browser = p.chromium.connect(self.playwright_ws_url)
+            else:
+                browser = p.chromium.launch(headless=self.headless, proxy=self.proxy)
+
+            for url in self.urls:
+                try:
+                    self._safe_process_url_sync(url)
+                    page = browser.new_page()
+                    response = page.goto(url)
+                    if response is None:
+                        raise ValueError(f"page.goto() returned None for url {url}")
+
+                    text = self.evaluator.evaluate(page, browser, response)
+                    metadata = {"source": url}
+                    yield Document(page_content=text, metadata=metadata)
+                except Exception as e:
+                    if self.continue_on_failure:
+                        log.exception(e, "Error loading %s", url)
+                        continue
+                    raise e
+            browser.close()
+
+    async def alazy_load(self) -> AsyncIterator[Document]:
+        """Safely load URLs asynchronously with support for remote browser."""
+        from playwright.async_api import async_playwright
+
+        async with async_playwright() as p:
+            # Use remote browser if ws_endpoint is provided, otherwise use local browser
+            if self.playwright_ws_url:
+                browser = await p.chromium.connect(self.playwright_ws_url)
+            else:
+                browser = await p.chromium.launch(
+                    headless=self.headless, proxy=self.proxy
+                )
+
+            for url in self.urls:
+                try:
+                    await self._safe_process_url(url)
+                    page = await browser.new_page()
+                    response = await page.goto(url)
+                    if response is None:
+                        raise ValueError(f"page.goto() returned None for url {url}")
+
+                    text = await self.evaluator.evaluate_async(page, browser, response)
+                    metadata = {"source": url}
+                    yield Document(page_content=text, metadata=metadata)
+                except Exception as e:
+                    if self.continue_on_failure:
+                        log.exception(e, "Error loading %s", url)
+                        continue
+                    raise e
+            await browser.close()
+
+    def _verify_ssl_cert(self, url: str) -> bool:
+        return verify_ssl_cert(url)
+
+    async def _wait_for_rate_limit(self):
+        """Wait to respect the rate limit if specified."""
+        if self.requests_per_second and self.last_request_time:
+            min_interval = timedelta(seconds=1.0 / self.requests_per_second)
+            time_since_last = datetime.now() - self.last_request_time
+            if time_since_last < min_interval:
+                await asyncio.sleep((min_interval - time_since_last).total_seconds())
+        self.last_request_time = datetime.now()
+
+    def _sync_wait_for_rate_limit(self):
+        """Synchronous version of rate limit wait."""
+        if self.requests_per_second and self.last_request_time:
+            min_interval = timedelta(seconds=1.0 / self.requests_per_second)
+            time_since_last = datetime.now() - self.last_request_time
+            if time_since_last < min_interval:
+                time.sleep((min_interval - time_since_last).total_seconds())
+        self.last_request_time = datetime.now()
+
+    async def _safe_process_url(self, url: str) -> bool:
+        """Perform safety checks before processing a URL."""
+        if self.verify_ssl and not self._verify_ssl_cert(url):
+            raise ValueError(f"SSL certificate verification failed for {url}")
+        await self._wait_for_rate_limit()
+        return True
+
+    def _safe_process_url_sync(self, url: str) -> bool:
+        """Synchronous version of safety checks."""
+        if self.verify_ssl and not self._verify_ssl_cert(url):
+            raise ValueError(f"SSL certificate verification failed for {url}")
+        self._sync_wait_for_rate_limit()
+        return True
+
+
 class SafeWebBaseLoader(WebBaseLoader):
 class SafeWebBaseLoader(WebBaseLoader):
     """WebBaseLoader with enhanced error handling for URLs."""
     """WebBaseLoader with enhanced error handling for URLs."""
 
 
+    def __init__(self, trust_env: bool = False, *args, **kwargs):
+        """Initialize SafeWebBaseLoader
+        Args:
+            trust_env (bool, optional): set to True if using proxy to make web requests, for example
+                using http(s)_proxy environment variables. Defaults to False.
+        """
+        super().__init__(*args, **kwargs)
+        self.trust_env = trust_env
+
+    async def _fetch(
+        self, url: str, retries: int = 3, cooldown: int = 2, backoff: float = 1.5
+    ) -> str:
+        async with aiohttp.ClientSession(trust_env=self.trust_env) as session:
+            for i in range(retries):
+                try:
+                    kwargs: Dict = dict(
+                        headers=self.session.headers,
+                        cookies=self.session.cookies.get_dict(),
+                    )
+                    if not self.session.verify:
+                        kwargs["ssl"] = False
+
+                    async with session.get(
+                        url, **(self.requests_kwargs | kwargs)
+                    ) as response:
+                        if self.raise_for_status:
+                            response.raise_for_status()
+                        return await response.text()
+                except aiohttp.ClientConnectionError as e:
+                    if i == retries - 1:
+                        raise
+                    else:
+                        log.warning(
+                            f"Error fetching {url} with attempt "
+                            f"{i + 1}/{retries}: {e}. Retrying..."
+                        )
+                        await asyncio.sleep(cooldown * backoff**i)
+        raise ValueError("retry count exceeded")
+
+    def _unpack_fetch_results(
+        self, results: Any, urls: List[str], parser: Union[str, None] = None
+    ) -> List[Any]:
+        """Unpack fetch results into BeautifulSoup objects."""
+        from bs4 import BeautifulSoup
+
+        final_results = []
+        for i, result in enumerate(results):
+            url = urls[i]
+            if parser is None:
+                if url.endswith(".xml"):
+                    parser = "xml"
+                else:
+                    parser = self.default_parser
+                self._check_parser(parser)
+            final_results.append(BeautifulSoup(result, parser, **self.bs_kwargs))
+        return final_results
+
+    async def ascrape_all(
+        self, urls: List[str], parser: Union[str, None] = None
+    ) -> List[Any]:
+        """Async fetch all urls, then return soups for all results."""
+        results = await self.fetch_all(urls)
+        return self._unpack_fetch_results(results, urls, parser=parser)
+
     def lazy_load(self) -> Iterator[Document]:
     def lazy_load(self) -> Iterator[Document]:
         """Lazy load text from the url(s) in web_path with error handling."""
         """Lazy load text from the url(s) in web_path with error handling."""
         for path in self.web_paths:
         for path in self.web_paths:
@@ -76,33 +467,72 @@ class SafeWebBaseLoader(WebBaseLoader):
                 text = soup.get_text(**self.bs_get_text_kwargs)
                 text = soup.get_text(**self.bs_get_text_kwargs)
 
 
                 # Build metadata
                 # Build metadata
-                metadata = {"source": path}
-                if title := soup.find("title"):
-                    metadata["title"] = title.get_text()
-                if description := soup.find("meta", attrs={"name": "description"}):
-                    metadata["description"] = description.get(
-                        "content", "No description found."
-                    )
-                if html := soup.find("html"):
-                    metadata["language"] = html.get("lang", "No language found.")
+                metadata = extract_metadata(soup, path)
 
 
                 yield Document(page_content=text, metadata=metadata)
                 yield Document(page_content=text, metadata=metadata)
             except Exception as e:
             except Exception as e:
                 # Log the error and continue with the next URL
                 # Log the error and continue with the next URL
-                log.error(f"Error loading {path}: {e}")
+                log.exception(e, "Error loading %s", path)
+
+    async def alazy_load(self) -> AsyncIterator[Document]:
+        """Async lazy load text from the url(s) in web_path."""
+        results = await self.ascrape_all(self.web_paths)
+        for path, soup in zip(self.web_paths, results):
+            text = soup.get_text(**self.bs_get_text_kwargs)
+            metadata = {"source": path}
+            if title := soup.find("title"):
+                metadata["title"] = title.get_text()
+            if description := soup.find("meta", attrs={"name": "description"}):
+                metadata["description"] = description.get(
+                    "content", "No description found."
+                )
+            if html := soup.find("html"):
+                metadata["language"] = html.get("lang", "No language found.")
+            yield Document(page_content=text, metadata=metadata)
+
+    async def aload(self) -> list[Document]:
+        """Load data into Document objects."""
+        return [document async for document in self.alazy_load()]
+
+
+RAG_WEB_LOADER_ENGINES = defaultdict(lambda: SafeWebBaseLoader)
+RAG_WEB_LOADER_ENGINES["playwright"] = SafePlaywrightURLLoader
+RAG_WEB_LOADER_ENGINES["safe_web"] = SafeWebBaseLoader
+RAG_WEB_LOADER_ENGINES["firecrawl"] = SafeFireCrawlLoader
 
 
 
 
 def get_web_loader(
 def get_web_loader(
     urls: Union[str, Sequence[str]],
     urls: Union[str, Sequence[str]],
     verify_ssl: bool = True,
     verify_ssl: bool = True,
     requests_per_second: int = 2,
     requests_per_second: int = 2,
+    trust_env: bool = False,
 ):
 ):
     # Check if the URLs are valid
     # Check if the URLs are valid
     safe_urls = safe_validate_urls([urls] if isinstance(urls, str) else urls)
     safe_urls = safe_validate_urls([urls] if isinstance(urls, str) else urls)
 
 
-    return SafeWebBaseLoader(
-        safe_urls,
-        verify_ssl=verify_ssl,
-        requests_per_second=requests_per_second,
-        continue_on_failure=True,
+    web_loader_args = {
+        "web_paths": safe_urls,
+        "verify_ssl": verify_ssl,
+        "requests_per_second": requests_per_second,
+        "continue_on_failure": True,
+        "trust_env": trust_env,
+    }
+
+    if PLAYWRIGHT_WS_URI.value:
+        web_loader_args["playwright_ws_url"] = PLAYWRIGHT_WS_URI.value
+
+    if RAG_WEB_LOADER_ENGINE.value == "firecrawl":
+        web_loader_args["api_key"] = FIRECRAWL_API_KEY.value
+        web_loader_args["api_url"] = FIRECRAWL_API_BASE_URL.value
+
+    # Create the appropriate WebLoader based on the configuration
+    WebLoaderClass = RAG_WEB_LOADER_ENGINES[RAG_WEB_LOADER_ENGINE.value]
+    web_loader = WebLoaderClass(**web_loader_args)
+
+    log.debug(
+        "Using RAG_WEB_LOADER_ENGINE %s for %s URLs",
+        web_loader.__class__.__name__,
+        len(safe_urls),
     )
     )
+
+    return web_loader

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

@@ -37,6 +37,7 @@ from open_webui.config import (
 
 
 from open_webui.constants import ERROR_MESSAGES
 from open_webui.constants import ERROR_MESSAGES
 from open_webui.env import (
 from open_webui.env import (
+    AIOHTTP_CLIENT_TIMEOUT,
     ENV,
     ENV,
     SRC_LOG_LEVELS,
     SRC_LOG_LEVELS,
     DEVICE_TYPE,
     DEVICE_TYPE,
@@ -266,7 +267,10 @@ async def speech(request: Request, user=Depends(get_verified_user)):
 
 
         try:
         try:
             # print(payload)
             # print(payload)
-            async with aiohttp.ClientSession() as session:
+            timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT)
+            async with aiohttp.ClientSession(
+                timeout=timeout, trust_env=True
+            ) as session:
                 async with session.post(
                 async with session.post(
                     url=f"{request.app.state.config.TTS_OPENAI_API_BASE_URL}/audio/speech",
                     url=f"{request.app.state.config.TTS_OPENAI_API_BASE_URL}/audio/speech",
                     json=payload,
                     json=payload,
@@ -323,7 +327,10 @@ async def speech(request: Request, user=Depends(get_verified_user)):
             )
             )
 
 
         try:
         try:
-            async with aiohttp.ClientSession() as session:
+            timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT)
+            async with aiohttp.ClientSession(
+                timeout=timeout, trust_env=True
+            ) as session:
                 async with session.post(
                 async with session.post(
                     f"https://api.elevenlabs.io/v1/text-to-speech/{voice_id}",
                     f"https://api.elevenlabs.io/v1/text-to-speech/{voice_id}",
                     json={
                     json={
@@ -380,7 +387,10 @@ async def speech(request: Request, user=Depends(get_verified_user)):
             data = f"""<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xml:lang="{locale}">
             data = f"""<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xml:lang="{locale}">
                 <voice name="{language}">{payload["input"]}</voice>
                 <voice name="{language}">{payload["input"]}</voice>
             </speak>"""
             </speak>"""
-            async with aiohttp.ClientSession() as session:
+            timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT)
+            async with aiohttp.ClientSession(
+                timeout=timeout, trust_env=True
+            ) as session:
                 async with session.post(
                 async with session.post(
                     f"https://{region}.tts.speech.microsoft.com/cognitiveservices/v1",
                     f"https://{region}.tts.speech.microsoft.com/cognitiveservices/v1",
                     headers={
                     headers={

+ 23 - 6
backend/open_webui/routers/auths.py

@@ -251,9 +251,19 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
             user = Users.get_user_by_email(mail)
             user = Users.get_user_by_email(mail)
             if not user:
             if not user:
                 try:
                 try:
+                    user_count = Users.get_num_users()
+                    if (
+                        request.app.state.USER_COUNT
+                        and user_count >= request.app.state.USER_COUNT
+                    ):
+                        raise HTTPException(
+                            status.HTTP_403_FORBIDDEN,
+                            detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
+                        )
+
                     role = (
                     role = (
                         "admin"
                         "admin"
-                        if Users.get_num_users() == 0
+                        if user_count == 0
                         else request.app.state.config.DEFAULT_USER_ROLE
                         else request.app.state.config.DEFAULT_USER_ROLE
                     )
                     )
 
 
@@ -413,6 +423,7 @@ async def signin(request: Request, response: Response, form_data: SigninForm):
 
 
 @router.post("/signup", response_model=SessionUserResponse)
 @router.post("/signup", response_model=SessionUserResponse)
 async def signup(request: Request, response: Response, form_data: SignupForm):
 async def signup(request: Request, response: Response, form_data: SignupForm):
+
     if WEBUI_AUTH:
     if WEBUI_AUTH:
         if (
         if (
             not request.app.state.config.ENABLE_SIGNUP
             not request.app.state.config.ENABLE_SIGNUP
@@ -427,6 +438,12 @@ async def signup(request: Request, response: Response, form_data: SignupForm):
                 status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED
                 status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED
             )
             )
 
 
+    user_count = Users.get_num_users()
+    if request.app.state.USER_COUNT and user_count >= request.app.state.USER_COUNT:
+        raise HTTPException(
+            status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED
+        )
+
     if not validate_email_format(form_data.email.lower()):
     if not validate_email_format(form_data.email.lower()):
         raise HTTPException(
         raise HTTPException(
             status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.INVALID_EMAIL_FORMAT
             status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.INVALID_EMAIL_FORMAT
@@ -437,12 +454,10 @@ async def signup(request: Request, response: Response, form_data: SignupForm):
 
 
     try:
     try:
         role = (
         role = (
-            "admin"
-            if Users.get_num_users() == 0
-            else request.app.state.config.DEFAULT_USER_ROLE
+            "admin" if user_count == 0 else request.app.state.config.DEFAULT_USER_ROLE
         )
         )
 
 
-        if Users.get_num_users() == 0:
+        if user_count == 0:
             # Disable signup after the first user is created
             # Disable signup after the first user is created
             request.app.state.config.ENABLE_SIGNUP = False
             request.app.state.config.ENABLE_SIGNUP = False
 
 
@@ -484,6 +499,7 @@ async def signup(request: Request, response: Response, form_data: SignupForm):
 
 
             if request.app.state.config.WEBHOOK_URL:
             if request.app.state.config.WEBHOOK_URL:
                 post_webhook(
                 post_webhook(
+                    request.app.state.WEBUI_NAME,
                     request.app.state.config.WEBHOOK_URL,
                     request.app.state.config.WEBHOOK_URL,
                     WEBHOOK_MESSAGES.USER_SIGNUP(user.name),
                     WEBHOOK_MESSAGES.USER_SIGNUP(user.name),
                     {
                     {
@@ -530,7 +546,8 @@ async def signout(request: Request, response: Response):
                             if logout_url:
                             if logout_url:
                                 response.delete_cookie("oauth_id_token")
                                 response.delete_cookie("oauth_id_token")
                                 return RedirectResponse(
                                 return RedirectResponse(
-                                    url=f"{logout_url}?id_token_hint={oauth_id_token}"
+                                    headers=response.headers,
+                                    url=f"{logout_url}?id_token_hint={oauth_id_token}",
                                 )
                                 )
                         else:
                         else:
                             raise HTTPException(
                             raise HTTPException(

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

@@ -192,7 +192,7 @@ async def get_channel_messages(
 ############################
 ############################
 
 
 
 
-async def send_notification(webui_url, channel, message, active_user_ids):
+async def send_notification(name, webui_url, channel, message, active_user_ids):
     users = get_users_with_access("read", channel.access_control)
     users = get_users_with_access("read", channel.access_control)
 
 
     for user in users:
     for user in users:
@@ -206,6 +206,7 @@ async def send_notification(webui_url, channel, message, active_user_ids):
 
 
                 if webhook_url:
                 if webhook_url:
                     post_webhook(
                     post_webhook(
+                        name,
                         webhook_url,
                         webhook_url,
                         f"#{channel.name} - {webui_url}/channels/{channel.id}\n\n{message.content}",
                         f"#{channel.name} - {webui_url}/channels/{channel.id}\n\n{message.content}",
                         {
                         {
@@ -302,6 +303,7 @@ async def post_new_message(
 
 
             background_tasks.add_task(
             background_tasks.add_task(
                 send_notification,
                 send_notification,
+                request.app.state.WEBUI_NAME,
                 request.app.state.config.WEBUI_URL,
                 request.app.state.config.WEBUI_URL,
                 channel,
                 channel,
                 message,
                 message,

+ 46 - 4
backend/open_webui/routers/configs.py

@@ -70,6 +70,12 @@ async def set_direct_connections_config(
 # CodeInterpreterConfig
 # CodeInterpreterConfig
 ############################
 ############################
 class CodeInterpreterConfigForm(BaseModel):
 class CodeInterpreterConfigForm(BaseModel):
+    CODE_EXECUTION_ENGINE: str
+    CODE_EXECUTION_JUPYTER_URL: Optional[str]
+    CODE_EXECUTION_JUPYTER_AUTH: Optional[str]
+    CODE_EXECUTION_JUPYTER_AUTH_TOKEN: Optional[str]
+    CODE_EXECUTION_JUPYTER_AUTH_PASSWORD: Optional[str]
+    CODE_EXECUTION_JUPYTER_TIMEOUT: Optional[int]
     ENABLE_CODE_INTERPRETER: bool
     ENABLE_CODE_INTERPRETER: bool
     CODE_INTERPRETER_ENGINE: str
     CODE_INTERPRETER_ENGINE: str
     CODE_INTERPRETER_PROMPT_TEMPLATE: Optional[str]
     CODE_INTERPRETER_PROMPT_TEMPLATE: Optional[str]
@@ -77,11 +83,18 @@ class CodeInterpreterConfigForm(BaseModel):
     CODE_INTERPRETER_JUPYTER_AUTH: Optional[str]
     CODE_INTERPRETER_JUPYTER_AUTH: Optional[str]
     CODE_INTERPRETER_JUPYTER_AUTH_TOKEN: Optional[str]
     CODE_INTERPRETER_JUPYTER_AUTH_TOKEN: Optional[str]
     CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD: Optional[str]
     CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD: Optional[str]
+    CODE_INTERPRETER_JUPYTER_TIMEOUT: Optional[int]
 
 
 
 
-@router.get("/code_interpreter", response_model=CodeInterpreterConfigForm)
-async def get_code_interpreter_config(request: Request, user=Depends(get_admin_user)):
+@router.get("/code_execution", response_model=CodeInterpreterConfigForm)
+async def get_code_execution_config(request: Request, user=Depends(get_admin_user)):
     return {
     return {
+        "CODE_EXECUTION_ENGINE": request.app.state.config.CODE_EXECUTION_ENGINE,
+        "CODE_EXECUTION_JUPYTER_URL": request.app.state.config.CODE_EXECUTION_JUPYTER_URL,
+        "CODE_EXECUTION_JUPYTER_AUTH": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH,
+        "CODE_EXECUTION_JUPYTER_AUTH_TOKEN": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN,
+        "CODE_EXECUTION_JUPYTER_AUTH_PASSWORD": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD,
+        "CODE_EXECUTION_JUPYTER_TIMEOUT": request.app.state.config.CODE_EXECUTION_JUPYTER_TIMEOUT,
         "ENABLE_CODE_INTERPRETER": request.app.state.config.ENABLE_CODE_INTERPRETER,
         "ENABLE_CODE_INTERPRETER": request.app.state.config.ENABLE_CODE_INTERPRETER,
         "CODE_INTERPRETER_ENGINE": request.app.state.config.CODE_INTERPRETER_ENGINE,
         "CODE_INTERPRETER_ENGINE": request.app.state.config.CODE_INTERPRETER_ENGINE,
         "CODE_INTERPRETER_PROMPT_TEMPLATE": request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE,
         "CODE_INTERPRETER_PROMPT_TEMPLATE": request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE,
@@ -89,13 +102,32 @@ async def get_code_interpreter_config(request: Request, user=Depends(get_admin_u
         "CODE_INTERPRETER_JUPYTER_AUTH": request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH,
         "CODE_INTERPRETER_JUPYTER_AUTH": request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH,
         "CODE_INTERPRETER_JUPYTER_AUTH_TOKEN": request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN,
         "CODE_INTERPRETER_JUPYTER_AUTH_TOKEN": request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN,
         "CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD": request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD,
         "CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD": request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD,
+        "CODE_INTERPRETER_JUPYTER_TIMEOUT": request.app.state.config.CODE_INTERPRETER_JUPYTER_TIMEOUT,
     }
     }
 
 
 
 
-@router.post("/code_interpreter", response_model=CodeInterpreterConfigForm)
-async def set_code_interpreter_config(
+@router.post("/code_execution", response_model=CodeInterpreterConfigForm)
+async def set_code_execution_config(
     request: Request, form_data: CodeInterpreterConfigForm, user=Depends(get_admin_user)
     request: Request, form_data: CodeInterpreterConfigForm, user=Depends(get_admin_user)
 ):
 ):
+
+    request.app.state.config.CODE_EXECUTION_ENGINE = form_data.CODE_EXECUTION_ENGINE
+    request.app.state.config.CODE_EXECUTION_JUPYTER_URL = (
+        form_data.CODE_EXECUTION_JUPYTER_URL
+    )
+    request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH = (
+        form_data.CODE_EXECUTION_JUPYTER_AUTH
+    )
+    request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN = (
+        form_data.CODE_EXECUTION_JUPYTER_AUTH_TOKEN
+    )
+    request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD = (
+        form_data.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD
+    )
+    request.app.state.config.CODE_EXECUTION_JUPYTER_TIMEOUT = (
+        form_data.CODE_EXECUTION_JUPYTER_TIMEOUT
+    )
+
     request.app.state.config.ENABLE_CODE_INTERPRETER = form_data.ENABLE_CODE_INTERPRETER
     request.app.state.config.ENABLE_CODE_INTERPRETER = form_data.ENABLE_CODE_INTERPRETER
     request.app.state.config.CODE_INTERPRETER_ENGINE = form_data.CODE_INTERPRETER_ENGINE
     request.app.state.config.CODE_INTERPRETER_ENGINE = form_data.CODE_INTERPRETER_ENGINE
     request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE = (
     request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE = (
@@ -116,8 +148,17 @@ async def set_code_interpreter_config(
     request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD = (
     request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD = (
         form_data.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD
         form_data.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD
     )
     )
+    request.app.state.config.CODE_INTERPRETER_JUPYTER_TIMEOUT = (
+        form_data.CODE_INTERPRETER_JUPYTER_TIMEOUT
+    )
 
 
     return {
     return {
+        "CODE_EXECUTION_ENGINE": request.app.state.config.CODE_EXECUTION_ENGINE,
+        "CODE_EXECUTION_JUPYTER_URL": request.app.state.config.CODE_EXECUTION_JUPYTER_URL,
+        "CODE_EXECUTION_JUPYTER_AUTH": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH,
+        "CODE_EXECUTION_JUPYTER_AUTH_TOKEN": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN,
+        "CODE_EXECUTION_JUPYTER_AUTH_PASSWORD": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD,
+        "CODE_EXECUTION_JUPYTER_TIMEOUT": request.app.state.config.CODE_EXECUTION_JUPYTER_TIMEOUT,
         "ENABLE_CODE_INTERPRETER": request.app.state.config.ENABLE_CODE_INTERPRETER,
         "ENABLE_CODE_INTERPRETER": request.app.state.config.ENABLE_CODE_INTERPRETER,
         "CODE_INTERPRETER_ENGINE": request.app.state.config.CODE_INTERPRETER_ENGINE,
         "CODE_INTERPRETER_ENGINE": request.app.state.config.CODE_INTERPRETER_ENGINE,
         "CODE_INTERPRETER_PROMPT_TEMPLATE": request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE,
         "CODE_INTERPRETER_PROMPT_TEMPLATE": request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE,
@@ -125,6 +166,7 @@ async def set_code_interpreter_config(
         "CODE_INTERPRETER_JUPYTER_AUTH": request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH,
         "CODE_INTERPRETER_JUPYTER_AUTH": request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH,
         "CODE_INTERPRETER_JUPYTER_AUTH_TOKEN": request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN,
         "CODE_INTERPRETER_JUPYTER_AUTH_TOKEN": request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN,
         "CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD": request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD,
         "CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD": request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD,
+        "CODE_INTERPRETER_JUPYTER_TIMEOUT": request.app.state.config.CODE_INTERPRETER_JUPYTER_TIMEOUT,
     }
     }
 
 
 
 

+ 16 - 9
backend/open_webui/routers/files.py

@@ -225,17 +225,24 @@ async def get_file_content_by_id(id: str, user=Depends(get_verified_user)):
                 filename = file.meta.get("name", file.filename)
                 filename = file.meta.get("name", file.filename)
                 encoded_filename = quote(filename)  # RFC5987 encoding
                 encoded_filename = quote(filename)  # RFC5987 encoding
 
 
+                content_type = file.meta.get("content_type")
+                filename = file.meta.get("name", file.filename)
+                encoded_filename = quote(filename)
                 headers = {}
                 headers = {}
-                if file.meta.get("content_type") not in [
-                    "application/pdf",
-                    "text/plain",
-                ]:
-                    headers = {
-                        **headers,
-                        "Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}",
-                    }
 
 
-                return FileResponse(file_path, headers=headers)
+                if content_type == "application/pdf" or filename.lower().endswith(
+                    ".pdf"
+                ):
+                    headers["Content-Disposition"] = (
+                        f"inline; filename*=UTF-8''{encoded_filename}"
+                    )
+                    content_type = "application/pdf"
+                elif content_type != "text/plain":
+                    headers["Content-Disposition"] = (
+                        f"attachment; filename*=UTF-8''{encoded_filename}"
+                    )
+
+                return FileResponse(file_path, headers=headers, media_type=content_type)
 
 
             else:
             else:
                 raise HTTPException(
                 raise HTTPException(

+ 64 - 0
backend/open_webui/routers/images.py

@@ -55,6 +55,10 @@ async def get_config(request: Request, user=Depends(get_admin_user)):
             "COMFYUI_WORKFLOW": request.app.state.config.COMFYUI_WORKFLOW,
             "COMFYUI_WORKFLOW": request.app.state.config.COMFYUI_WORKFLOW,
             "COMFYUI_WORKFLOW_NODES": request.app.state.config.COMFYUI_WORKFLOW_NODES,
             "COMFYUI_WORKFLOW_NODES": request.app.state.config.COMFYUI_WORKFLOW_NODES,
         },
         },
+        "gemini": {
+            "GEMINI_API_BASE_URL": request.app.state.config.IMAGES_GEMINI_API_BASE_URL,
+            "GEMINI_API_KEY": request.app.state.config.IMAGES_GEMINI_API_KEY,
+        },
     }
     }
 
 
 
 
@@ -78,6 +82,11 @@ class ComfyUIConfigForm(BaseModel):
     COMFYUI_WORKFLOW_NODES: list[dict]
     COMFYUI_WORKFLOW_NODES: list[dict]
 
 
 
 
+class GeminiConfigForm(BaseModel):
+    GEMINI_API_BASE_URL: str
+    GEMINI_API_KEY: str
+
+
 class ConfigForm(BaseModel):
 class ConfigForm(BaseModel):
     enabled: bool
     enabled: bool
     engine: str
     engine: str
@@ -85,6 +94,7 @@ class ConfigForm(BaseModel):
     openai: OpenAIConfigForm
     openai: OpenAIConfigForm
     automatic1111: Automatic1111ConfigForm
     automatic1111: Automatic1111ConfigForm
     comfyui: ComfyUIConfigForm
     comfyui: ComfyUIConfigForm
+    gemini: GeminiConfigForm
 
 
 
 
 @router.post("/config/update")
 @router.post("/config/update")
@@ -103,6 +113,11 @@ async def update_config(
     )
     )
     request.app.state.config.IMAGES_OPENAI_API_KEY = form_data.openai.OPENAI_API_KEY
     request.app.state.config.IMAGES_OPENAI_API_KEY = form_data.openai.OPENAI_API_KEY
 
 
+    request.app.state.config.IMAGES_GEMINI_API_BASE_URL = (
+        form_data.gemini.GEMINI_API_BASE_URL
+    )
+    request.app.state.config.IMAGES_GEMINI_API_KEY = form_data.gemini.GEMINI_API_KEY
+
     request.app.state.config.AUTOMATIC1111_BASE_URL = (
     request.app.state.config.AUTOMATIC1111_BASE_URL = (
         form_data.automatic1111.AUTOMATIC1111_BASE_URL
         form_data.automatic1111.AUTOMATIC1111_BASE_URL
     )
     )
@@ -155,6 +170,10 @@ async def update_config(
             "COMFYUI_WORKFLOW": request.app.state.config.COMFYUI_WORKFLOW,
             "COMFYUI_WORKFLOW": request.app.state.config.COMFYUI_WORKFLOW,
             "COMFYUI_WORKFLOW_NODES": request.app.state.config.COMFYUI_WORKFLOW_NODES,
             "COMFYUI_WORKFLOW_NODES": request.app.state.config.COMFYUI_WORKFLOW_NODES,
         },
         },
+        "gemini": {
+            "GEMINI_API_BASE_URL": request.app.state.config.IMAGES_GEMINI_API_BASE_URL,
+            "GEMINI_API_KEY": request.app.state.config.IMAGES_GEMINI_API_KEY,
+        },
     }
     }
 
 
 
 
@@ -224,6 +243,12 @@ def get_image_model(request):
             if request.app.state.config.IMAGE_GENERATION_MODEL
             if request.app.state.config.IMAGE_GENERATION_MODEL
             else "dall-e-2"
             else "dall-e-2"
         )
         )
+    elif request.app.state.config.IMAGE_GENERATION_ENGINE == "gemini":
+        return (
+            request.app.state.config.IMAGE_GENERATION_MODEL
+            if request.app.state.config.IMAGE_GENERATION_MODEL
+            else "imagen-3.0-generate-002"
+        )
     elif request.app.state.config.IMAGE_GENERATION_ENGINE == "comfyui":
     elif request.app.state.config.IMAGE_GENERATION_ENGINE == "comfyui":
         return (
         return (
             request.app.state.config.IMAGE_GENERATION_MODEL
             request.app.state.config.IMAGE_GENERATION_MODEL
@@ -299,6 +324,10 @@ def get_models(request: Request, user=Depends(get_verified_user)):
                 {"id": "dall-e-2", "name": "DALL·E 2"},
                 {"id": "dall-e-2", "name": "DALL·E 2"},
                 {"id": "dall-e-3", "name": "DALL·E 3"},
                 {"id": "dall-e-3", "name": "DALL·E 3"},
             ]
             ]
+        elif request.app.state.config.IMAGE_GENERATION_ENGINE == "gemini":
+            return [
+                {"id": "imagen-3-0-generate-002", "name": "imagen-3.0 generate-002"},
+            ]
         elif request.app.state.config.IMAGE_GENERATION_ENGINE == "comfyui":
         elif request.app.state.config.IMAGE_GENERATION_ENGINE == "comfyui":
             # TODO - get models from comfyui
             # TODO - get models from comfyui
             headers = {
             headers = {
@@ -483,6 +512,41 @@ async def image_generations(
                 images.append({"url": url})
                 images.append({"url": url})
             return images
             return images
 
 
+        elif request.app.state.config.IMAGE_GENERATION_ENGINE == "gemini":
+            headers = {}
+            headers["Content-Type"] = "application/json"
+            headers["x-goog-api-key"] = request.app.state.config.IMAGES_GEMINI_API_KEY
+
+            model = get_image_model(request)
+            data = {
+                "instances": {"prompt": form_data.prompt},
+                "parameters": {
+                    "sampleCount": form_data.n,
+                    "outputOptions": {"mimeType": "image/png"},
+                },
+            }
+
+            # Use asyncio.to_thread for the requests.post call
+            r = await asyncio.to_thread(
+                requests.post,
+                url=f"{request.app.state.config.IMAGES_GEMINI_API_BASE_URL}/models/{model}:predict",
+                json=data,
+                headers=headers,
+            )
+
+            r.raise_for_status()
+            res = r.json()
+
+            images = []
+            for image in res["predictions"]:
+                image_data, content_type = load_b64_image_data(
+                    image["bytesBase64Encoded"]
+                )
+                url = upload_image(request, data, image_data, content_type, user)
+                images.append({"url": url})
+
+            return images
+
         elif request.app.state.config.IMAGE_GENERATION_ENGINE == "comfyui":
         elif request.app.state.config.IMAGE_GENERATION_ENGINE == "comfyui":
             data = {
             data = {
                 "prompt": form_data.prompt,
                 "prompt": form_data.prompt,

+ 16 - 3
backend/open_webui/routers/ollama.py

@@ -31,7 +31,7 @@ from fastapi import (
 )
 )
 from fastapi.middleware.cors import CORSMiddleware
 from fastapi.middleware.cors import CORSMiddleware
 from fastapi.responses import StreamingResponse
 from fastapi.responses import StreamingResponse
-from pydantic import BaseModel, ConfigDict
+from pydantic import BaseModel, ConfigDict, validator
 from starlette.background import BackgroundTask
 from starlette.background import BackgroundTask
 
 
 
 
@@ -1047,15 +1047,28 @@ async def generate_completion(
 
 
 class ChatMessage(BaseModel):
 class ChatMessage(BaseModel):
     role: str
     role: str
-    content: str
+    content: Optional[str] = None
     tool_calls: Optional[list[dict]] = None
     tool_calls: Optional[list[dict]] = None
     images: Optional[list[str]] = None
     images: Optional[list[str]] = None
 
 
+    @validator("content", pre=True)
+    @classmethod
+    def check_at_least_one_field(cls, field_value, values, **kwargs):
+        # Raise an error if both 'content' and 'tool_calls' are None
+        if field_value is None and (
+            "tool_calls" not in values or values["tool_calls"] is None
+        ):
+            raise ValueError(
+                "At least one of 'content' or 'tool_calls' must be provided"
+            )
+
+        return field_value
+
 
 
 class GenerateChatCompletionForm(BaseModel):
 class GenerateChatCompletionForm(BaseModel):
     model: str
     model: str
     messages: list[ChatMessage]
     messages: list[ChatMessage]
-    format: Optional[dict] = None
+    format: Optional[Union[dict, str]] = None
     options: Optional[dict] = None
     options: Optional[dict] = None
     template: Optional[str] = None
     template: Optional[str] = None
     stream: Optional[bool] = True
     stream: Optional[bool] = True

+ 59 - 51
backend/open_webui/routers/pipelines.py

@@ -9,6 +9,7 @@ from fastapi import (
     status,
     status,
     APIRouter,
     APIRouter,
 )
 )
+import aiohttp
 import os
 import os
 import logging
 import logging
 import shutil
 import shutil
@@ -56,96 +57,103 @@ def get_sorted_filters(model_id, models):
     return sorted_filters
     return sorted_filters
 
 
 
 
-def process_pipeline_inlet_filter(request, payload, user, models):
+async def process_pipeline_inlet_filter(request, payload, user, models):
     user = {"id": user.id, "email": user.email, "name": user.name, "role": user.role}
     user = {"id": user.id, "email": user.email, "name": user.name, "role": user.role}
     model_id = payload["model"]
     model_id = payload["model"]
-
     sorted_filters = get_sorted_filters(model_id, models)
     sorted_filters = get_sorted_filters(model_id, models)
     model = models[model_id]
     model = models[model_id]
 
 
     if "pipeline" in model:
     if "pipeline" in model:
         sorted_filters.append(model)
         sorted_filters.append(model)
 
 
-    for filter in sorted_filters:
-        r = None
-        try:
-            urlIdx = filter["urlIdx"]
+    async with aiohttp.ClientSession() as session:
+        for filter in sorted_filters:
+            urlIdx = filter.get("urlIdx")
+            if urlIdx is None:
+                continue
 
 
             url = request.app.state.config.OPENAI_API_BASE_URLS[urlIdx]
             url = request.app.state.config.OPENAI_API_BASE_URLS[urlIdx]
             key = request.app.state.config.OPENAI_API_KEYS[urlIdx]
             key = request.app.state.config.OPENAI_API_KEYS[urlIdx]
 
 
-            if key == "":
+            if not key:
                 continue
                 continue
 
 
             headers = {"Authorization": f"Bearer {key}"}
             headers = {"Authorization": f"Bearer {key}"}
-            r = requests.post(
-                f"{url}/{filter['id']}/filter/inlet",
-                headers=headers,
-                json={
-                    "user": user,
-                    "body": payload,
-                },
-            )
-
-            r.raise_for_status()
-            payload = r.json()
-        except Exception as e:
-            # Handle connection error here
-            print(f"Connection error: {e}")
+            request_data = {
+                "user": user,
+                "body": payload,
+            }
 
 
-            if r is not None:
-                res = r.json()
+            try:
+                async with session.post(
+                    f"{url}/{filter['id']}/filter/inlet",
+                    headers=headers,
+                    json=request_data,
+                ) as response:
+                    response.raise_for_status()
+                    payload = await response.json()
+            except aiohttp.ClientResponseError as e:
+                res = (
+                    await response.json()
+                    if response.content_type == "application/json"
+                    else {}
+                )
                 if "detail" in res:
                 if "detail" in res:
-                    raise Exception(r.status_code, res["detail"])
+                    raise Exception(response.status, res["detail"])
+            except Exception as e:
+                print(f"Connection error: {e}")
 
 
     return payload
     return payload
 
 
 
 
-def process_pipeline_outlet_filter(request, payload, user, models):
+async def process_pipeline_outlet_filter(request, payload, user, models):
     user = {"id": user.id, "email": user.email, "name": user.name, "role": user.role}
     user = {"id": user.id, "email": user.email, "name": user.name, "role": user.role}
     model_id = payload["model"]
     model_id = payload["model"]
-
     sorted_filters = get_sorted_filters(model_id, models)
     sorted_filters = get_sorted_filters(model_id, models)
     model = models[model_id]
     model = models[model_id]
 
 
     if "pipeline" in model:
     if "pipeline" in model:
         sorted_filters = [model] + sorted_filters
         sorted_filters = [model] + sorted_filters
 
 
-    for filter in sorted_filters:
-        r = None
-        try:
-            urlIdx = filter["urlIdx"]
+    async with aiohttp.ClientSession() as session:
+        for filter in sorted_filters:
+            urlIdx = filter.get("urlIdx")
+            if urlIdx is None:
+                continue
 
 
             url = request.app.state.config.OPENAI_API_BASE_URLS[urlIdx]
             url = request.app.state.config.OPENAI_API_BASE_URLS[urlIdx]
             key = request.app.state.config.OPENAI_API_KEYS[urlIdx]
             key = request.app.state.config.OPENAI_API_KEYS[urlIdx]
 
 
-            if key != "":
-                r = requests.post(
-                    f"{url}/{filter['id']}/filter/outlet",
-                    headers={"Authorization": f"Bearer {key}"},
-                    json={
-                        "user": user,
-                        "body": payload,
-                    },
-                )
+            if not key:
+                continue
 
 
-                r.raise_for_status()
-                data = r.json()
-                payload = data
-        except Exception as e:
-            # Handle connection error here
-            print(f"Connection error: {e}")
+            headers = {"Authorization": f"Bearer {key}"}
+            request_data = {
+                "user": user,
+                "body": payload,
+            }
 
 
-            if r is not None:
+            try:
+                async with session.post(
+                    f"{url}/{filter['id']}/filter/outlet",
+                    headers=headers,
+                    json=request_data,
+                ) as response:
+                    response.raise_for_status()
+                    payload = await response.json()
+            except aiohttp.ClientResponseError as e:
                 try:
                 try:
-                    res = r.json()
+                    res = (
+                        await response.json()
+                        if "application/json" in response.content_type
+                        else {}
+                    )
                     if "detail" in res:
                     if "detail" in res:
-                        return Exception(r.status_code, res)
+                        raise Exception(response.status, res)
                 except Exception:
                 except Exception:
                     pass
                     pass
-
-            else:
-                pass
+            except Exception as e:
+                print(f"Connection error: {e}")
 
 
     return payload
     return payload
 
 

+ 87 - 15
backend/open_webui/routers/retrieval.py

@@ -21,6 +21,7 @@ from fastapi import (
     APIRouter,
     APIRouter,
 )
 )
 from fastapi.middleware.cors import CORSMiddleware
 from fastapi.middleware.cors import CORSMiddleware
+from fastapi.concurrency import run_in_threadpool
 from pydantic import BaseModel
 from pydantic import BaseModel
 import tiktoken
 import tiktoken
 
 
@@ -50,6 +51,7 @@ from open_webui.retrieval.web.duckduckgo import search_duckduckgo
 from open_webui.retrieval.web.google_pse import search_google_pse
 from open_webui.retrieval.web.google_pse import search_google_pse
 from open_webui.retrieval.web.jina_search import search_jina
 from open_webui.retrieval.web.jina_search import search_jina
 from open_webui.retrieval.web.searchapi import search_searchapi
 from open_webui.retrieval.web.searchapi import search_searchapi
+from open_webui.retrieval.web.serpapi import search_serpapi
 from open_webui.retrieval.web.searxng import search_searxng
 from open_webui.retrieval.web.searxng import search_searxng
 from open_webui.retrieval.web.serper import search_serper
 from open_webui.retrieval.web.serper import search_serper
 from open_webui.retrieval.web.serply import search_serply
 from open_webui.retrieval.web.serply import search_serply
@@ -349,6 +351,7 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)):
     return {
     return {
         "status": True,
         "status": True,
         "pdf_extract_images": request.app.state.config.PDF_EXTRACT_IMAGES,
         "pdf_extract_images": request.app.state.config.PDF_EXTRACT_IMAGES,
+        "RAG_FULL_CONTEXT": request.app.state.config.RAG_FULL_CONTEXT,
         "enable_google_drive_integration": request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION,
         "enable_google_drive_integration": request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION,
         "content_extraction": {
         "content_extraction": {
             "engine": request.app.state.config.CONTENT_EXTRACTION_ENGINE,
             "engine": request.app.state.config.CONTENT_EXTRACTION_ENGINE,
@@ -369,7 +372,8 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)):
             "proxy_url": request.app.state.config.YOUTUBE_LOADER_PROXY_URL,
             "proxy_url": request.app.state.config.YOUTUBE_LOADER_PROXY_URL,
         },
         },
         "web": {
         "web": {
-            "web_loader_ssl_verification": request.app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
+            "ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION": request.app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
+            "RAG_WEB_SEARCH_FULL_CONTEXT": request.app.state.config.RAG_WEB_SEARCH_FULL_CONTEXT,
             "search": {
             "search": {
                 "enabled": request.app.state.config.ENABLE_RAG_WEB_SEARCH,
                 "enabled": request.app.state.config.ENABLE_RAG_WEB_SEARCH,
                 "drive": request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION,
                 "drive": request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION,
@@ -388,6 +392,8 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)):
                 "tavily_api_key": request.app.state.config.TAVILY_API_KEY,
                 "tavily_api_key": request.app.state.config.TAVILY_API_KEY,
                 "searchapi_api_key": request.app.state.config.SEARCHAPI_API_KEY,
                 "searchapi_api_key": request.app.state.config.SEARCHAPI_API_KEY,
                 "searchapi_engine": request.app.state.config.SEARCHAPI_ENGINE,
                 "searchapi_engine": request.app.state.config.SEARCHAPI_ENGINE,
+                "serpapi_api_key": request.app.state.config.SERPAPI_API_KEY,
+                "serpapi_engine": request.app.state.config.SERPAPI_ENGINE,
                 "jina_api_key": request.app.state.config.JINA_API_KEY,
                 "jina_api_key": request.app.state.config.JINA_API_KEY,
                 "bing_search_v7_endpoint": request.app.state.config.BING_SEARCH_V7_ENDPOINT,
                 "bing_search_v7_endpoint": request.app.state.config.BING_SEARCH_V7_ENDPOINT,
                 "bing_search_v7_subscription_key": request.app.state.config.BING_SEARCH_V7_SUBSCRIPTION_KEY,
                 "bing_search_v7_subscription_key": request.app.state.config.BING_SEARCH_V7_SUBSCRIPTION_KEY,
@@ -439,21 +445,26 @@ class WebSearchConfig(BaseModel):
     tavily_api_key: Optional[str] = None
     tavily_api_key: Optional[str] = None
     searchapi_api_key: Optional[str] = None
     searchapi_api_key: Optional[str] = None
     searchapi_engine: Optional[str] = None
     searchapi_engine: Optional[str] = None
+    serpapi_api_key: Optional[str] = None
+    serpapi_engine: Optional[str] = None
     jina_api_key: Optional[str] = None
     jina_api_key: Optional[str] = None
     bing_search_v7_endpoint: Optional[str] = None
     bing_search_v7_endpoint: Optional[str] = None
     bing_search_v7_subscription_key: Optional[str] = None
     bing_search_v7_subscription_key: Optional[str] = None
     exa_api_key: Optional[str] = None
     exa_api_key: Optional[str] = None
     result_count: Optional[int] = None
     result_count: Optional[int] = None
     concurrent_requests: Optional[int] = None
     concurrent_requests: Optional[int] = None
+    trust_env: Optional[bool] = None
     domain_filter_list: Optional[List[str]] = []
     domain_filter_list: Optional[List[str]] = []
 
 
 
 
 class WebConfig(BaseModel):
 class WebConfig(BaseModel):
     search: WebSearchConfig
     search: WebSearchConfig
-    web_loader_ssl_verification: Optional[bool] = None
+    ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION: Optional[bool] = None
+    RAG_WEB_SEARCH_FULL_CONTEXT: Optional[bool] = None
 
 
 
 
 class ConfigUpdateForm(BaseModel):
 class ConfigUpdateForm(BaseModel):
+    RAG_FULL_CONTEXT: Optional[bool] = None
     pdf_extract_images: Optional[bool] = None
     pdf_extract_images: Optional[bool] = None
     enable_google_drive_integration: Optional[bool] = None
     enable_google_drive_integration: Optional[bool] = None
     file: Optional[FileConfig] = None
     file: Optional[FileConfig] = None
@@ -473,6 +484,12 @@ async def update_rag_config(
         else request.app.state.config.PDF_EXTRACT_IMAGES
         else request.app.state.config.PDF_EXTRACT_IMAGES
     )
     )
 
 
+    request.app.state.config.RAG_FULL_CONTEXT = (
+        form_data.RAG_FULL_CONTEXT
+        if form_data.RAG_FULL_CONTEXT is not None
+        else request.app.state.config.RAG_FULL_CONTEXT
+    )
+
     request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION = (
     request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION = (
         form_data.enable_google_drive_integration
         form_data.enable_google_drive_integration
         if form_data.enable_google_drive_integration is not None
         if form_data.enable_google_drive_integration is not None
@@ -505,11 +522,16 @@ async def update_rag_config(
     if form_data.web is not None:
     if form_data.web is not None:
         request.app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = (
         request.app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = (
             # Note: When UI "Bypass SSL verification for Websites"=True then ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION=False
             # Note: When UI "Bypass SSL verification for Websites"=True then ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION=False
-            form_data.web.web_loader_ssl_verification
+            form_data.web.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION
         )
         )
 
 
         request.app.state.config.ENABLE_RAG_WEB_SEARCH = form_data.web.search.enabled
         request.app.state.config.ENABLE_RAG_WEB_SEARCH = form_data.web.search.enabled
         request.app.state.config.RAG_WEB_SEARCH_ENGINE = form_data.web.search.engine
         request.app.state.config.RAG_WEB_SEARCH_ENGINE = form_data.web.search.engine
+
+        request.app.state.config.RAG_WEB_SEARCH_FULL_CONTEXT = (
+            form_data.web.RAG_WEB_SEARCH_FULL_CONTEXT
+        )
+
         request.app.state.config.SEARXNG_QUERY_URL = (
         request.app.state.config.SEARXNG_QUERY_URL = (
             form_data.web.search.searxng_query_url
             form_data.web.search.searxng_query_url
         )
         )
@@ -545,6 +567,9 @@ async def update_rag_config(
             form_data.web.search.searchapi_engine
             form_data.web.search.searchapi_engine
         )
         )
 
 
+        request.app.state.config.SERPAPI_API_KEY = form_data.web.search.serpapi_api_key
+        request.app.state.config.SERPAPI_ENGINE = form_data.web.search.serpapi_engine
+
         request.app.state.config.JINA_API_KEY = form_data.web.search.jina_api_key
         request.app.state.config.JINA_API_KEY = form_data.web.search.jina_api_key
         request.app.state.config.BING_SEARCH_V7_ENDPOINT = (
         request.app.state.config.BING_SEARCH_V7_ENDPOINT = (
             form_data.web.search.bing_search_v7_endpoint
             form_data.web.search.bing_search_v7_endpoint
@@ -561,6 +586,9 @@ async def update_rag_config(
         request.app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS = (
         request.app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS = (
             form_data.web.search.concurrent_requests
             form_data.web.search.concurrent_requests
         )
         )
+        request.app.state.config.RAG_WEB_SEARCH_TRUST_ENV = (
+            form_data.web.search.trust_env
+        )
         request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST = (
         request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST = (
             form_data.web.search.domain_filter_list
             form_data.web.search.domain_filter_list
         )
         )
@@ -568,6 +596,7 @@ async def update_rag_config(
     return {
     return {
         "status": True,
         "status": True,
         "pdf_extract_images": request.app.state.config.PDF_EXTRACT_IMAGES,
         "pdf_extract_images": request.app.state.config.PDF_EXTRACT_IMAGES,
+        "RAG_FULL_CONTEXT": request.app.state.config.RAG_FULL_CONTEXT,
         "file": {
         "file": {
             "max_size": request.app.state.config.FILE_MAX_SIZE,
             "max_size": request.app.state.config.FILE_MAX_SIZE,
             "max_count": request.app.state.config.FILE_MAX_COUNT,
             "max_count": request.app.state.config.FILE_MAX_COUNT,
@@ -587,7 +616,8 @@ async def update_rag_config(
             "translation": request.app.state.YOUTUBE_LOADER_TRANSLATION,
             "translation": request.app.state.YOUTUBE_LOADER_TRANSLATION,
         },
         },
         "web": {
         "web": {
-            "web_loader_ssl_verification": request.app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
+            "ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION": request.app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
+            "RAG_WEB_SEARCH_FULL_CONTEXT": request.app.state.config.RAG_WEB_SEARCH_FULL_CONTEXT,
             "search": {
             "search": {
                 "enabled": request.app.state.config.ENABLE_RAG_WEB_SEARCH,
                 "enabled": request.app.state.config.ENABLE_RAG_WEB_SEARCH,
                 "engine": request.app.state.config.RAG_WEB_SEARCH_ENGINE,
                 "engine": request.app.state.config.RAG_WEB_SEARCH_ENGINE,
@@ -604,6 +634,8 @@ async def update_rag_config(
                 "serply_api_key": request.app.state.config.SERPLY_API_KEY,
                 "serply_api_key": request.app.state.config.SERPLY_API_KEY,
                 "serachapi_api_key": request.app.state.config.SEARCHAPI_API_KEY,
                 "serachapi_api_key": request.app.state.config.SEARCHAPI_API_KEY,
                 "searchapi_engine": request.app.state.config.SEARCHAPI_ENGINE,
                 "searchapi_engine": request.app.state.config.SEARCHAPI_ENGINE,
+                "serpapi_api_key": request.app.state.config.SERPAPI_API_KEY,
+                "serpapi_engine": request.app.state.config.SERPAPI_ENGINE,
                 "tavily_api_key": request.app.state.config.TAVILY_API_KEY,
                 "tavily_api_key": request.app.state.config.TAVILY_API_KEY,
                 "jina_api_key": request.app.state.config.JINA_API_KEY,
                 "jina_api_key": request.app.state.config.JINA_API_KEY,
                 "bing_search_v7_endpoint": request.app.state.config.BING_SEARCH_V7_ENDPOINT,
                 "bing_search_v7_endpoint": request.app.state.config.BING_SEARCH_V7_ENDPOINT,
@@ -611,6 +643,7 @@ async def update_rag_config(
                 "exa_api_key": request.app.state.config.EXA_API_KEY,
                 "exa_api_key": request.app.state.config.EXA_API_KEY,
                 "result_count": request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
                 "result_count": request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
                 "concurrent_requests": request.app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
                 "concurrent_requests": request.app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
+                "trust_env": request.app.state.config.RAG_WEB_SEARCH_TRUST_ENV,
                 "domain_filter_list": request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
                 "domain_filter_list": request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
             },
             },
         },
         },
@@ -760,7 +793,11 @@ def save_docs_to_vector_db(
     # for meta-data so convert them to string.
     # for meta-data so convert them to string.
     for metadata in metadatas:
     for metadata in metadatas:
         for key, value in metadata.items():
         for key, value in metadata.items():
-            if isinstance(value, datetime):
+            if (
+                isinstance(value, datetime)
+                or isinstance(value, list)
+                or isinstance(value, dict)
+            ):
                 metadata[key] = str(value)
                 metadata[key] = str(value)
 
 
     try:
     try:
@@ -1127,6 +1164,7 @@ def search_web(request: Request, engine: str, query: str) -> list[SearchResult]:
     - TAVILY_API_KEY
     - TAVILY_API_KEY
     - EXA_API_KEY
     - EXA_API_KEY
     - SEARCHAPI_API_KEY + SEARCHAPI_ENGINE (by default `google`)
     - SEARCHAPI_API_KEY + SEARCHAPI_ENGINE (by default `google`)
+    - SERPAPI_API_KEY + SERPAPI_ENGINE (by default `google`)
     Args:
     Args:
         query (str): The query to search for
         query (str): The query to search for
     """
     """
@@ -1241,6 +1279,7 @@ def search_web(request: Request, engine: str, query: str) -> list[SearchResult]:
                 request.app.state.config.TAVILY_API_KEY,
                 request.app.state.config.TAVILY_API_KEY,
                 query,
                 query,
                 request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
                 request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
+                request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
             )
             )
         else:
         else:
             raise Exception("No TAVILY_API_KEY found in environment variables")
             raise Exception("No TAVILY_API_KEY found in environment variables")
@@ -1255,6 +1294,17 @@ def search_web(request: Request, engine: str, query: str) -> list[SearchResult]:
             )
             )
         else:
         else:
             raise Exception("No SEARCHAPI_API_KEY found in environment variables")
             raise Exception("No SEARCHAPI_API_KEY found in environment variables")
+    elif engine == "serpapi":
+        if request.app.state.config.SERPAPI_API_KEY:
+            return search_serpapi(
+                request.app.state.config.SERPAPI_API_KEY,
+                request.app.state.config.SERPAPI_ENGINE,
+                query,
+                request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
+                request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
+            )
+        else:
+            raise Exception("No SERPAPI_API_KEY found in environment variables")
     elif engine == "jina":
     elif engine == "jina":
         return search_jina(
         return search_jina(
             request.app.state.config.JINA_API_KEY,
             request.app.state.config.JINA_API_KEY,
@@ -1282,7 +1332,7 @@ def search_web(request: Request, engine: str, query: str) -> list[SearchResult]:
 
 
 
 
 @router.post("/process/web/search")
 @router.post("/process/web/search")
-def process_web_search(
+async def process_web_search(
     request: Request, form_data: SearchForm, user=Depends(get_verified_user)
     request: Request, form_data: SearchForm, user=Depends(get_verified_user)
 ):
 ):
     try:
     try:
@@ -1314,17 +1364,39 @@ def process_web_search(
             urls,
             urls,
             verify_ssl=request.app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
             verify_ssl=request.app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
             requests_per_second=request.app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
             requests_per_second=request.app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
+            trust_env=request.app.state.config.RAG_WEB_SEARCH_TRUST_ENV,
         )
         )
-        docs = loader.load()
-        save_docs_to_vector_db(
-            request, docs, collection_name, overwrite=True, user=user
-        )
+        docs = await loader.aload()
 
 
-        return {
-            "status": True,
-            "collection_name": collection_name,
-            "filenames": urls,
-        }
+        if request.app.state.config.RAG_WEB_SEARCH_FULL_CONTEXT:
+            return {
+                "status": True,
+                "docs": [
+                    {
+                        "content": doc.page_content,
+                        "metadata": doc.metadata,
+                    }
+                    for doc in docs
+                ],
+                "filenames": urls,
+                "loaded_count": len(docs),
+            }
+        else:
+            await run_in_threadpool(
+                save_docs_to_vector_db,
+                request,
+                docs,
+                collection_name,
+                overwrite=True,
+                user=user,
+            )
+
+            return {
+                "status": True,
+                "collection_name": collection_name,
+                "filenames": urls,
+                "loaded_count": len(docs),
+            }
     except Exception as e:
     except Exception as e:
         log.exception(e)
         log.exception(e)
         raise HTTPException(
         raise HTTPException(

+ 13 - 2
backend/open_webui/routers/tasks.py

@@ -58,6 +58,7 @@ async def get_task_config(request: Request, user=Depends(get_verified_user)):
         "AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH": request.app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH,
         "AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH": request.app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH,
         "TAGS_GENERATION_PROMPT_TEMPLATE": request.app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE,
         "TAGS_GENERATION_PROMPT_TEMPLATE": request.app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE,
         "ENABLE_TAGS_GENERATION": request.app.state.config.ENABLE_TAGS_GENERATION,
         "ENABLE_TAGS_GENERATION": request.app.state.config.ENABLE_TAGS_GENERATION,
+        "ENABLE_TITLE_GENERATION": request.app.state.config.ENABLE_TITLE_GENERATION,
         "ENABLE_SEARCH_QUERY_GENERATION": request.app.state.config.ENABLE_SEARCH_QUERY_GENERATION,
         "ENABLE_SEARCH_QUERY_GENERATION": request.app.state.config.ENABLE_SEARCH_QUERY_GENERATION,
         "ENABLE_RETRIEVAL_QUERY_GENERATION": request.app.state.config.ENABLE_RETRIEVAL_QUERY_GENERATION,
         "ENABLE_RETRIEVAL_QUERY_GENERATION": request.app.state.config.ENABLE_RETRIEVAL_QUERY_GENERATION,
         "QUERY_GENERATION_PROMPT_TEMPLATE": request.app.state.config.QUERY_GENERATION_PROMPT_TEMPLATE,
         "QUERY_GENERATION_PROMPT_TEMPLATE": request.app.state.config.QUERY_GENERATION_PROMPT_TEMPLATE,
@@ -68,6 +69,7 @@ async def get_task_config(request: Request, user=Depends(get_verified_user)):
 class TaskConfigForm(BaseModel):
 class TaskConfigForm(BaseModel):
     TASK_MODEL: Optional[str]
     TASK_MODEL: Optional[str]
     TASK_MODEL_EXTERNAL: Optional[str]
     TASK_MODEL_EXTERNAL: Optional[str]
+    ENABLE_TITLE_GENERATION: bool
     TITLE_GENERATION_PROMPT_TEMPLATE: str
     TITLE_GENERATION_PROMPT_TEMPLATE: str
     IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE: str
     IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE: str
     ENABLE_AUTOCOMPLETE_GENERATION: bool
     ENABLE_AUTOCOMPLETE_GENERATION: bool
@@ -86,6 +88,7 @@ async def update_task_config(
 ):
 ):
     request.app.state.config.TASK_MODEL = form_data.TASK_MODEL
     request.app.state.config.TASK_MODEL = form_data.TASK_MODEL
     request.app.state.config.TASK_MODEL_EXTERNAL = form_data.TASK_MODEL_EXTERNAL
     request.app.state.config.TASK_MODEL_EXTERNAL = form_data.TASK_MODEL_EXTERNAL
+    request.app.state.config.ENABLE_TITLE_GENERATION = form_data.ENABLE_TITLE_GENERATION
     request.app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE = (
     request.app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE = (
         form_data.TITLE_GENERATION_PROMPT_TEMPLATE
         form_data.TITLE_GENERATION_PROMPT_TEMPLATE
     )
     )
@@ -122,6 +125,7 @@ async def update_task_config(
     return {
     return {
         "TASK_MODEL": request.app.state.config.TASK_MODEL,
         "TASK_MODEL": request.app.state.config.TASK_MODEL,
         "TASK_MODEL_EXTERNAL": request.app.state.config.TASK_MODEL_EXTERNAL,
         "TASK_MODEL_EXTERNAL": request.app.state.config.TASK_MODEL_EXTERNAL,
+        "ENABLE_TITLE_GENERATION": request.app.state.config.ENABLE_TITLE_GENERATION,
         "TITLE_GENERATION_PROMPT_TEMPLATE": request.app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE,
         "TITLE_GENERATION_PROMPT_TEMPLATE": request.app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE,
         "IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE": request.app.state.config.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE,
         "IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE": request.app.state.config.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE,
         "ENABLE_AUTOCOMPLETE_GENERATION": request.app.state.config.ENABLE_AUTOCOMPLETE_GENERATION,
         "ENABLE_AUTOCOMPLETE_GENERATION": request.app.state.config.ENABLE_AUTOCOMPLETE_GENERATION,
@@ -139,6 +143,13 @@ async def update_task_config(
 async def generate_title(
 async def generate_title(
     request: Request, form_data: dict, user=Depends(get_verified_user)
     request: Request, form_data: dict, user=Depends(get_verified_user)
 ):
 ):
+
+    if not request.app.state.config.ENABLE_TITLE_GENERATION:
+        return JSONResponse(
+            status_code=status.HTTP_200_OK,
+            content={"detail": "Title generation is disabled"},
+        )
+
     if getattr(request.state, "direct", False) and hasattr(request.state, "model"):
     if getattr(request.state, "direct", False) and hasattr(request.state, "model"):
         models = {
         models = {
             request.state.model["id"]: request.state.model,
             request.state.model["id"]: request.state.model,
@@ -197,7 +208,7 @@ async def generate_title(
         "stream": False,
         "stream": False,
         **(
         **(
             {"max_tokens": 1000}
             {"max_tokens": 1000}
-            if models[task_model_id]["owned_by"] == "ollama"
+            if models[task_model_id].get("owned_by") == "ollama"
             else {
             else {
                 "max_completion_tokens": 1000,
                 "max_completion_tokens": 1000,
             }
             }
@@ -560,7 +571,7 @@ async def generate_emoji(
         "stream": False,
         "stream": False,
         **(
         **(
             {"max_tokens": 4}
             {"max_tokens": 4}
-            if models[task_model_id]["owned_by"] == "ollama"
+            if models[task_model_id].get("owned_by") == "ollama"
             else {
             else {
                 "max_completion_tokens": 4,
                 "max_completion_tokens": 4,
             }
             }

+ 42 - 11
backend/open_webui/routers/utils.py

@@ -4,45 +4,76 @@ import markdown
 from open_webui.models.chats import ChatTitleMessagesForm
 from open_webui.models.chats import ChatTitleMessagesForm
 from open_webui.config import DATA_DIR, ENABLE_ADMIN_EXPORT
 from open_webui.config import DATA_DIR, ENABLE_ADMIN_EXPORT
 from open_webui.constants import ERROR_MESSAGES
 from open_webui.constants import ERROR_MESSAGES
-from fastapi import APIRouter, Depends, HTTPException, Response, status
+from fastapi import APIRouter, Depends, HTTPException, Request, Response, status
 from pydantic import BaseModel
 from pydantic import BaseModel
 from starlette.responses import FileResponse
 from starlette.responses import FileResponse
+
+
 from open_webui.utils.misc import get_gravatar_url
 from open_webui.utils.misc import get_gravatar_url
 from open_webui.utils.pdf_generator import PDFGenerator
 from open_webui.utils.pdf_generator import PDFGenerator
-from open_webui.utils.auth import get_admin_user
+from open_webui.utils.auth import get_admin_user, get_verified_user
+from open_webui.utils.code_interpreter import execute_code_jupyter
+
 
 
 router = APIRouter()
 router = APIRouter()
 
 
 
 
 @router.get("/gravatar")
 @router.get("/gravatar")
-async def get_gravatar(
-    email: str,
-):
+async def get_gravatar(email: str, user=Depends(get_verified_user)):
     return get_gravatar_url(email)
     return get_gravatar_url(email)
 
 
 
 
-class CodeFormatRequest(BaseModel):
+class CodeForm(BaseModel):
     code: str
     code: str
 
 
 
 
 @router.post("/code/format")
 @router.post("/code/format")
-async def format_code(request: CodeFormatRequest):
+async def format_code(form_data: CodeForm, user=Depends(get_verified_user)):
     try:
     try:
-        formatted_code = black.format_str(request.code, mode=black.Mode())
+        formatted_code = black.format_str(form_data.code, mode=black.Mode())
         return {"code": formatted_code}
         return {"code": formatted_code}
     except black.NothingChanged:
     except black.NothingChanged:
-        return {"code": request.code}
+        return {"code": form_data.code}
     except Exception as e:
     except Exception as e:
         raise HTTPException(status_code=400, detail=str(e))
         raise HTTPException(status_code=400, detail=str(e))
 
 
 
 
+@router.post("/code/execute")
+async def execute_code(
+    request: Request, form_data: CodeForm, user=Depends(get_verified_user)
+):
+    if request.app.state.config.CODE_EXECUTION_ENGINE == "jupyter":
+        output = await execute_code_jupyter(
+            request.app.state.config.CODE_EXECUTION_JUPYTER_URL,
+            form_data.code,
+            (
+                request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN
+                if request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH == "token"
+                else None
+            ),
+            (
+                request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD
+                if request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH == "password"
+                else None
+            ),
+            request.app.state.config.CODE_EXECUTION_JUPYTER_TIMEOUT,
+        )
+
+        return output
+    else:
+        raise HTTPException(
+            status_code=400,
+            detail="Code execution engine not supported",
+        )
+
+
 class MarkdownForm(BaseModel):
 class MarkdownForm(BaseModel):
     md: str
     md: str
 
 
 
 
 @router.post("/markdown")
 @router.post("/markdown")
 async def get_html_from_markdown(
 async def get_html_from_markdown(
-    form_data: MarkdownForm,
+    form_data: MarkdownForm, user=Depends(get_verified_user)
 ):
 ):
     return {"html": markdown.markdown(form_data.md)}
     return {"html": markdown.markdown(form_data.md)}
 
 
@@ -54,7 +85,7 @@ class ChatForm(BaseModel):
 
 
 @router.post("/pdf")
 @router.post("/pdf")
 async def download_chat_as_pdf(
 async def download_chat_as_pdf(
-    form_data: ChatTitleMessagesForm,
+    form_data: ChatTitleMessagesForm, user=Depends(get_verified_user)
 ):
 ):
     try:
     try:
         pdf_bytes = PDFGenerator(form_data).generate_chat_pdf()
         pdf_bytes = PDFGenerator(form_data).generate_chat_pdf()

+ 0 - 0
backend/open_webui/static/loader.js


+ 0 - 2
backend/open_webui/static/swagger-ui/swagger-ui.css

@@ -9308,5 +9308,3 @@
 	.json-schema-2020-12__title:first-of-type {
 	.json-schema-2020-12__title:first-of-type {
 	font-size: 16px;
 	font-size: 16px;
 }
 }
-
-/*# sourceMappingURL=swagger-ui.css.map*/

+ 76 - 0
backend/open_webui/storage/provider.py

@@ -15,12 +15,18 @@ from open_webui.config import (
     S3_SECRET_ACCESS_KEY,
     S3_SECRET_ACCESS_KEY,
     GCS_BUCKET_NAME,
     GCS_BUCKET_NAME,
     GOOGLE_APPLICATION_CREDENTIALS_JSON,
     GOOGLE_APPLICATION_CREDENTIALS_JSON,
+    AZURE_STORAGE_ENDPOINT,
+    AZURE_STORAGE_CONTAINER_NAME,
+    AZURE_STORAGE_KEY,
     STORAGE_PROVIDER,
     STORAGE_PROVIDER,
     UPLOAD_DIR,
     UPLOAD_DIR,
 )
 )
 from google.cloud import storage
 from google.cloud import storage
 from google.cloud.exceptions import GoogleCloudError, NotFound
 from google.cloud.exceptions import GoogleCloudError, NotFound
 from open_webui.constants import ERROR_MESSAGES
 from open_webui.constants import ERROR_MESSAGES
+from azure.identity import DefaultAzureCredential
+from azure.storage.blob import BlobServiceClient
+from azure.core.exceptions import ResourceNotFoundError
 
 
 
 
 class StorageProvider(ABC):
 class StorageProvider(ABC):
@@ -221,6 +227,74 @@ class GCSStorageProvider(StorageProvider):
         LocalStorageProvider.delete_all_files()
         LocalStorageProvider.delete_all_files()
 
 
 
 
+class AzureStorageProvider(StorageProvider):
+    def __init__(self):
+        self.endpoint = AZURE_STORAGE_ENDPOINT
+        self.container_name = AZURE_STORAGE_CONTAINER_NAME
+        storage_key = AZURE_STORAGE_KEY
+
+        if storage_key:
+            # Configure using the Azure Storage Account Endpoint and Key
+            self.blob_service_client = BlobServiceClient(
+                account_url=self.endpoint, credential=storage_key
+            )
+        else:
+            # Configure using the Azure Storage Account Endpoint and DefaultAzureCredential
+            # If the key is not configured, then the DefaultAzureCredential will be used to support Managed Identity authentication
+            self.blob_service_client = BlobServiceClient(
+                account_url=self.endpoint, credential=DefaultAzureCredential()
+            )
+        self.container_client = self.blob_service_client.get_container_client(
+            self.container_name
+        )
+
+    def upload_file(self, file: BinaryIO, filename: str) -> Tuple[bytes, str]:
+        """Handles uploading of the file to Azure Blob Storage."""
+        contents, file_path = LocalStorageProvider.upload_file(file, filename)
+        try:
+            blob_client = self.container_client.get_blob_client(filename)
+            blob_client.upload_blob(contents, overwrite=True)
+            return contents, f"{self.endpoint}/{self.container_name}/{filename}"
+        except Exception as e:
+            raise RuntimeError(f"Error uploading file to Azure Blob Storage: {e}")
+
+    def get_file(self, file_path: str) -> str:
+        """Handles downloading of the file from Azure Blob Storage."""
+        try:
+            filename = file_path.split("/")[-1]
+            local_file_path = f"{UPLOAD_DIR}/{filename}"
+            blob_client = self.container_client.get_blob_client(filename)
+            with open(local_file_path, "wb") as download_file:
+                download_file.write(blob_client.download_blob().readall())
+            return local_file_path
+        except ResourceNotFoundError as e:
+            raise RuntimeError(f"Error downloading file from Azure Blob Storage: {e}")
+
+    def delete_file(self, file_path: str) -> None:
+        """Handles deletion of the file from Azure Blob Storage."""
+        try:
+            filename = file_path.split("/")[-1]
+            blob_client = self.container_client.get_blob_client(filename)
+            blob_client.delete_blob()
+        except ResourceNotFoundError as e:
+            raise RuntimeError(f"Error deleting file from Azure Blob Storage: {e}")
+
+        # Always delete from local storage
+        LocalStorageProvider.delete_file(file_path)
+
+    def delete_all_files(self) -> None:
+        """Handles deletion of all files from Azure Blob Storage."""
+        try:
+            blobs = self.container_client.list_blobs()
+            for blob in blobs:
+                self.container_client.delete_blob(blob.name)
+        except Exception as e:
+            raise RuntimeError(f"Error deleting all files from Azure Blob Storage: {e}")
+
+        # Always delete from local storage
+        LocalStorageProvider.delete_all_files()
+
+
 def get_storage_provider(storage_provider: str):
 def get_storage_provider(storage_provider: str):
     if storage_provider == "local":
     if storage_provider == "local":
         Storage = LocalStorageProvider()
         Storage = LocalStorageProvider()
@@ -228,6 +302,8 @@ def get_storage_provider(storage_provider: str):
         Storage = S3StorageProvider()
         Storage = S3StorageProvider()
     elif storage_provider == "gcs":
     elif storage_provider == "gcs":
         Storage = GCSStorageProvider()
         Storage = GCSStorageProvider()
+    elif storage_provider == "azure":
+        Storage = AzureStorageProvider()
     else:
     else:
         raise RuntimeError(f"Unsupported storage provider: {storage_provider}")
         raise RuntimeError(f"Unsupported storage provider: {storage_provider}")
     return Storage
     return Storage

+ 150 - 0
backend/open_webui/test/apps/webui/storage/test_provider.py

@@ -7,6 +7,8 @@ from moto import mock_aws
 from open_webui.storage import provider
 from open_webui.storage import provider
 from gcp_storage_emulator.server import create_server
 from gcp_storage_emulator.server import create_server
 from google.cloud import storage
 from google.cloud import storage
+from azure.storage.blob import BlobServiceClient, ContainerClient, BlobClient
+from unittest.mock import MagicMock
 
 
 
 
 def mock_upload_dir(monkeypatch, tmp_path):
 def mock_upload_dir(monkeypatch, tmp_path):
@@ -22,6 +24,7 @@ def test_imports():
     provider.LocalStorageProvider
     provider.LocalStorageProvider
     provider.S3StorageProvider
     provider.S3StorageProvider
     provider.GCSStorageProvider
     provider.GCSStorageProvider
+    provider.AzureStorageProvider
     provider.Storage
     provider.Storage
 
 
 
 
@@ -32,6 +35,8 @@ def test_get_storage_provider():
     assert isinstance(Storage, provider.S3StorageProvider)
     assert isinstance(Storage, provider.S3StorageProvider)
     Storage = provider.get_storage_provider("gcs")
     Storage = provider.get_storage_provider("gcs")
     assert isinstance(Storage, provider.GCSStorageProvider)
     assert isinstance(Storage, provider.GCSStorageProvider)
+    Storage = provider.get_storage_provider("azure")
+    assert isinstance(Storage, provider.AzureStorageProvider)
     with pytest.raises(RuntimeError):
     with pytest.raises(RuntimeError):
         provider.get_storage_provider("invalid")
         provider.get_storage_provider("invalid")
 
 
@@ -48,6 +53,7 @@ def test_class_instantiation():
     provider.LocalStorageProvider()
     provider.LocalStorageProvider()
     provider.S3StorageProvider()
     provider.S3StorageProvider()
     provider.GCSStorageProvider()
     provider.GCSStorageProvider()
+    provider.AzureStorageProvider()
 
 
 
 
 class TestLocalStorageProvider:
 class TestLocalStorageProvider:
@@ -272,3 +278,147 @@ class TestGCSStorageProvider:
         assert not (upload_dir / self.filename_extra).exists()
         assert not (upload_dir / self.filename_extra).exists()
         assert self.Storage.bucket.get_blob(self.filename) == None
         assert self.Storage.bucket.get_blob(self.filename) == None
         assert self.Storage.bucket.get_blob(self.filename_extra) == None
         assert self.Storage.bucket.get_blob(self.filename_extra) == None
+
+
+class TestAzureStorageProvider:
+    def __init__(self):
+        super().__init__()
+
+    @pytest.fixture(scope="class")
+    def setup_storage(self, monkeypatch):
+        # Create mock Blob Service Client and related clients
+        mock_blob_service_client = MagicMock()
+        mock_container_client = MagicMock()
+        mock_blob_client = MagicMock()
+
+        # Set up return values for the mock
+        mock_blob_service_client.get_container_client.return_value = (
+            mock_container_client
+        )
+        mock_container_client.get_blob_client.return_value = mock_blob_client
+
+        # Monkeypatch the Azure classes to return our mocks
+        monkeypatch.setattr(
+            azure.storage.blob,
+            "BlobServiceClient",
+            lambda *args, **kwargs: mock_blob_service_client,
+        )
+        monkeypatch.setattr(
+            azure.storage.blob,
+            "ContainerClient",
+            lambda *args, **kwargs: mock_container_client,
+        )
+        monkeypatch.setattr(
+            azure.storage.blob, "BlobClient", lambda *args, **kwargs: mock_blob_client
+        )
+
+        self.Storage = provider.AzureStorageProvider()
+        self.Storage.endpoint = "https://myaccount.blob.core.windows.net"
+        self.Storage.container_name = "my-container"
+        self.file_content = b"test content"
+        self.filename = "test.txt"
+        self.filename_extra = "test_extra.txt"
+        self.file_bytesio_empty = io.BytesIO()
+
+        # Apply mocks to the Storage instance
+        self.Storage.blob_service_client = mock_blob_service_client
+        self.Storage.container_client = mock_container_client
+
+    def test_upload_file(self, monkeypatch, tmp_path):
+        upload_dir = mock_upload_dir(monkeypatch, tmp_path)
+
+        # Simulate an error when container does not exist
+        self.Storage.container_client.get_blob_client.side_effect = Exception(
+            "Container does not exist"
+        )
+        with pytest.raises(Exception):
+            self.Storage.upload_file(io.BytesIO(self.file_content), self.filename)
+
+        # Reset side effect and create container
+        self.Storage.container_client.get_blob_client.side_effect = None
+        self.Storage.create_container()
+        contents, azure_file_path = self.Storage.upload_file(
+            io.BytesIO(self.file_content), self.filename
+        )
+
+        # Assertions
+        self.Storage.container_client.get_blob_client.assert_called_with(self.filename)
+        self.Storage.container_client.get_blob_client().upload_blob.assert_called_once_with(
+            self.file_content, overwrite=True
+        )
+        assert contents == self.file_content
+        assert (
+            azure_file_path
+            == f"https://myaccount.blob.core.windows.net/{self.Storage.container_name}/{self.filename}"
+        )
+        assert (upload_dir / self.filename).exists()
+        assert (upload_dir / self.filename).read_bytes() == self.file_content
+
+        with pytest.raises(ValueError):
+            self.Storage.upload_file(self.file_bytesio_empty, self.filename)
+
+    def test_get_file(self, monkeypatch, tmp_path):
+        upload_dir = mock_upload_dir(monkeypatch, tmp_path)
+        self.Storage.create_container()
+
+        # Mock upload behavior
+        self.Storage.upload_file(io.BytesIO(self.file_content), self.filename)
+        # Mock blob download behavior
+        self.Storage.container_client.get_blob_client().download_blob().readall.return_value = (
+            self.file_content
+        )
+
+        file_url = f"https://myaccount.blob.core.windows.net/{self.Storage.container_name}/{self.filename}"
+        file_path = self.Storage.get_file(file_url)
+
+        assert file_path == str(upload_dir / self.filename)
+        assert (upload_dir / self.filename).exists()
+        assert (upload_dir / self.filename).read_bytes() == self.file_content
+
+    def test_delete_file(self, monkeypatch, tmp_path):
+        upload_dir = mock_upload_dir(monkeypatch, tmp_path)
+        self.Storage.create_container()
+
+        # Mock file upload
+        self.Storage.upload_file(io.BytesIO(self.file_content), self.filename)
+        # Mock deletion
+        self.Storage.container_client.get_blob_client().delete_blob.return_value = None
+
+        file_url = f"https://myaccount.blob.core.windows.net/{self.Storage.container_name}/{self.filename}"
+        self.Storage.delete_file(file_url)
+
+        self.Storage.container_client.get_blob_client().delete_blob.assert_called_once()
+        assert not (upload_dir / self.filename).exists()
+
+    def test_delete_all_files(self, monkeypatch, tmp_path):
+        upload_dir = mock_upload_dir(monkeypatch, tmp_path)
+        self.Storage.create_container()
+
+        # Mock file uploads
+        self.Storage.upload_file(io.BytesIO(self.file_content), self.filename)
+        self.Storage.upload_file(io.BytesIO(self.file_content), self.filename_extra)
+
+        # Mock listing and deletion behavior
+        self.Storage.container_client.list_blobs.return_value = [
+            {"name": self.filename},
+            {"name": self.filename_extra},
+        ]
+        self.Storage.container_client.get_blob_client().delete_blob.return_value = None
+
+        self.Storage.delete_all_files()
+
+        self.Storage.container_client.list_blobs.assert_called_once()
+        self.Storage.container_client.get_blob_client().delete_blob.assert_any_call()
+        assert not (upload_dir / self.filename).exists()
+        assert not (upload_dir / self.filename_extra).exists()
+
+    def test_get_file_not_found(self, monkeypatch):
+        self.Storage.create_container()
+
+        file_url = f"https://myaccount.blob.core.windows.net/{self.Storage.container_name}/{self.filename}"
+        # Mock behavior to raise an error for missing blobs
+        self.Storage.container_client.get_blob_client().download_blob.side_effect = (
+            Exception("Blob not found")
+        )
+        with pytest.raises(Exception, match="Blob not found"):
+            self.Storage.get_file(file_url)

+ 67 - 1
backend/open_webui/utils/auth.py

@@ -1,6 +1,12 @@
 import logging
 import logging
 import uuid
 import uuid
 import jwt
 import jwt
+import base64
+import hmac
+import hashlib
+import requests
+import os
+
 
 
 from datetime import UTC, datetime, timedelta
 from datetime import UTC, datetime, timedelta
 from typing import Optional, Union, List, Dict
 from typing import Optional, Union, List, Dict
@@ -8,7 +14,7 @@ from typing import Optional, Union, List, Dict
 from open_webui.models.users import Users
 from open_webui.models.users import Users
 
 
 from open_webui.constants import ERROR_MESSAGES
 from open_webui.constants import ERROR_MESSAGES
-from open_webui.env import WEBUI_SECRET_KEY
+from open_webui.env import WEBUI_SECRET_KEY, TRUSTED_SIGNATURE_KEY, STATIC_DIR
 
 
 from fastapi import Depends, HTTPException, Request, Response, status
 from fastapi import Depends, HTTPException, Request, Response, status
 from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
 from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
@@ -24,6 +30,66 @@ ALGORITHM = "HS256"
 # Auth Utils
 # Auth Utils
 ##############
 ##############
 
 
+
+def verify_signature(payload: str, signature: str) -> bool:
+    """
+    Verifies the HMAC signature of the received payload.
+    """
+    try:
+        expected_signature = base64.b64encode(
+            hmac.new(TRUSTED_SIGNATURE_KEY, payload.encode(), hashlib.sha256).digest()
+        ).decode()
+
+        # Compare securely to prevent timing attacks
+        return hmac.compare_digest(expected_signature, signature)
+
+    except Exception:
+        return False
+
+
+def override_static(path: str, content: str):
+    # Ensure path is safe
+    if "/" in path or ".." in path:
+        print(f"Invalid path: {path}")
+        return
+
+    file_path = os.path.join(STATIC_DIR, path)
+    os.makedirs(os.path.dirname(file_path), exist_ok=True)
+
+    with open(file_path, "wb") as f:
+        f.write(base64.b64decode(content))  # Convert Base64 back to raw binary
+
+
+def get_license_data(app, key):
+    if key:
+        try:
+            res = requests.post(
+                "https://api.openwebui.com/api/v1/license",
+                json={"key": key, "version": "1"},
+                timeout=5,
+            )
+
+            if getattr(res, "ok", False):
+                payload = getattr(res, "json", lambda: {})()
+                for k, v in payload.items():
+                    if k == "resources":
+                        for p, c in v.items():
+                            globals().get("override_static", lambda a, b: None)(p, c)
+                    elif k == "user_count":
+                        setattr(app.state, "USER_COUNT", v)
+                    elif k == "webui_name":
+                        setattr(app.state, "WEBUI_NAME", v)
+
+                return True
+            else:
+                print(
+                    f"License: retrieval issue: {getattr(res, 'text', 'unknown error')}"
+                )
+        except Exception as ex:
+            print(f"License: Uncaught Exception: {ex}")
+    return False
+
+
 bearer_security = HTTPBearer(auto_error=False)
 bearer_security = HTTPBearer(auto_error=False)
 pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
 pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
 
 

+ 18 - 18
backend/open_webui/utils/chat.py

@@ -161,11 +161,18 @@ async def generate_chat_completion(
     user: Any,
     user: Any,
     bypass_filter: bool = False,
     bypass_filter: bool = False,
 ):
 ):
+    log.debug(f"generate_chat_completion: {form_data}")
     if BYPASS_MODEL_ACCESS_CONTROL:
     if BYPASS_MODEL_ACCESS_CONTROL:
         bypass_filter = True
         bypass_filter = True
 
 
     if hasattr(request.state, "metadata"):
     if hasattr(request.state, "metadata"):
-        form_data["metadata"] = request.state.metadata
+        if "metadata" not in form_data:
+            form_data["metadata"] = request.state.metadata
+        else:
+            form_data["metadata"] = {
+                **form_data["metadata"],
+                **request.state.metadata,
+            }
 
 
     if getattr(request.state, "direct", False) and hasattr(request.state, "model"):
     if getattr(request.state, "direct", False) and hasattr(request.state, "model"):
         models = {
         models = {
@@ -179,28 +186,21 @@ async def generate_chat_completion(
     if model_id not in models:
     if model_id not in models:
         raise Exception("Model not found")
         raise Exception("Model not found")
 
 
-    # Process the form_data through the pipeline
-    try:
-        form_data = process_pipeline_inlet_filter(request, form_data, user, models)
-    except Exception as e:
-        raise e
-
     model = models[model_id]
     model = models[model_id]
 
 
-    # Check if user has access to the model
-    if not bypass_filter and user.role == "user":
-        try:
-            check_model_access(user, model)
-        except Exception as e:
-            raise e
-
     if getattr(request.state, "direct", False):
     if getattr(request.state, "direct", False):
         return await generate_direct_chat_completion(
         return await generate_direct_chat_completion(
             request, form_data, user=user, models=models
             request, form_data, user=user, models=models
         )
         )
-
     else:
     else:
-        if model["owned_by"] == "arena":
+        # Check if user has access to the model
+        if not bypass_filter and user.role == "user":
+            try:
+                check_model_access(user, model)
+            except Exception as e:
+                raise e
+
+        if model.get("owned_by") == "arena":
             model_ids = model.get("info", {}).get("meta", {}).get("model_ids")
             model_ids = model.get("info", {}).get("meta", {}).get("model_ids")
             filter_mode = model.get("info", {}).get("meta", {}).get("filter_mode")
             filter_mode = model.get("info", {}).get("meta", {}).get("filter_mode")
             if model_ids and filter_mode == "exclude":
             if model_ids and filter_mode == "exclude":
@@ -253,7 +253,7 @@ async def generate_chat_completion(
             return await generate_function_chat_completion(
             return await generate_function_chat_completion(
                 request, form_data, user=user, models=models
                 request, form_data, user=user, models=models
             )
             )
-        if model["owned_by"] == "ollama":
+        if model.get("owned_by") == "ollama":
             # Using /ollama/api/chat endpoint
             # Using /ollama/api/chat endpoint
             form_data = convert_payload_openai_to_ollama(form_data)
             form_data = convert_payload_openai_to_ollama(form_data)
             response = await generate_ollama_chat_completion(
             response = await generate_ollama_chat_completion(
@@ -302,7 +302,7 @@ async def chat_completed(request: Request, form_data: dict, user: Any):
     model = models[model_id]
     model = models[model_id]
 
 
     try:
     try:
-        data = process_pipeline_outlet_filter(request, data, user, models)
+        data = await process_pipeline_outlet_filter(request, data, user, models)
     except Exception as e:
     except Exception as e:
         return Exception(f"Error: {e}")
         return Exception(f"Error: {e}")
 
 

+ 153 - 88
backend/open_webui/utils/middleware.py

@@ -39,7 +39,10 @@ from open_webui.routers.tasks import (
 )
 )
 from open_webui.routers.retrieval import process_web_search, SearchForm
 from open_webui.routers.retrieval import process_web_search, SearchForm
 from open_webui.routers.images import image_generations, GenerateImageForm
 from open_webui.routers.images import image_generations, GenerateImageForm
-
+from open_webui.routers.pipelines import (
+    process_pipeline_inlet_filter,
+    process_pipeline_outlet_filter,
+)
 
 
 from open_webui.utils.webhook import post_webhook
 from open_webui.utils.webhook import post_webhook
 
 
@@ -318,84 +321,94 @@ async def chat_web_search_handler(
         )
         )
         return form_data
         return form_data
 
 
-    searchQuery = queries[0]
-
-    await event_emitter(
-        {
-            "type": "status",
-            "data": {
-                "action": "web_search",
-                "description": 'Searching "{{searchQuery}}"',
-                "query": searchQuery,
-                "done": False,
-            },
-        }
-    )
+    all_results = []
 
 
-    try:
+    for searchQuery in queries:
+        await event_emitter(
+            {
+                "type": "status",
+                "data": {
+                    "action": "web_search",
+                    "description": 'Searching "{{searchQuery}}"',
+                    "query": searchQuery,
+                    "done": False,
+                },
+            }
+        )
 
 
-        # Offload process_web_search to a separate thread
-        loop = asyncio.get_running_loop()
-        with ThreadPoolExecutor() as executor:
-            results = await loop.run_in_executor(
-                executor,
-                lambda: process_web_search(
-                    request,
-                    SearchForm(
-                        **{
-                            "query": searchQuery,
-                        }
-                    ),
-                    user,
+        try:
+            results = await process_web_search(
+                request,
+                SearchForm(
+                    **{
+                        "query": searchQuery,
+                    }
                 ),
                 ),
+                user=user,
             )
             )
 
 
-        if results:
-            await event_emitter(
-                {
-                    "type": "status",
-                    "data": {
-                        "action": "web_search",
-                        "description": "Searched {{count}} sites",
-                        "query": searchQuery,
-                        "urls": results["filenames"],
-                        "done": True,
-                    },
-                }
-            )
+            if results:
+                all_results.append(results)
+                files = form_data.get("files", [])
 
 
-            files = form_data.get("files", [])
-            files.append(
-                {
-                    "collection_name": results["collection_name"],
-                    "name": searchQuery,
-                    "type": "web_search_results",
-                    "urls": results["filenames"],
-                }
-            )
-            form_data["files"] = files
-        else:
+                if request.app.state.config.RAG_WEB_SEARCH_FULL_CONTEXT:
+                    files.append(
+                        {
+                            "docs": results.get("docs", []),
+                            "name": searchQuery,
+                            "type": "web_search_docs",
+                            "urls": results["filenames"],
+                        }
+                    )
+                else:
+                    files.append(
+                        {
+                            "collection_name": results["collection_name"],
+                            "name": searchQuery,
+                            "type": "web_search_results",
+                            "urls": results["filenames"],
+                        }
+                    )
+                form_data["files"] = files
+        except Exception as e:
+            log.exception(e)
             await event_emitter(
             await event_emitter(
                 {
                 {
                     "type": "status",
                     "type": "status",
                     "data": {
                     "data": {
                         "action": "web_search",
                         "action": "web_search",
-                        "description": "No search results found",
+                        "description": 'Error searching "{{searchQuery}}"',
                         "query": searchQuery,
                         "query": searchQuery,
                         "done": True,
                         "done": True,
                         "error": True,
                         "error": True,
                     },
                     },
                 }
                 }
             )
             )
-    except Exception as e:
-        log.exception(e)
+
+    if all_results:
+        urls = []
+        for results in all_results:
+            if "filenames" in results:
+                urls.extend(results["filenames"])
+
         await event_emitter(
         await event_emitter(
             {
             {
                 "type": "status",
                 "type": "status",
                 "data": {
                 "data": {
                     "action": "web_search",
                     "action": "web_search",
-                    "description": 'Error searching "{{searchQuery}}"',
-                    "query": searchQuery,
+                    "description": "Searched {{count}} sites",
+                    "urls": urls,
+                    "done": True,
+                },
+            }
+        )
+    else:
+        await event_emitter(
+            {
+                "type": "status",
+                "data": {
+                    "action": "web_search",
+                    "description": "No search results found",
                     "done": True,
                     "done": True,
                     "error": True,
                     "error": True,
                 },
                 },
@@ -552,9 +565,9 @@ async def chat_completion_files_handler(
                         reranking_function=request.app.state.rf,
                         reranking_function=request.app.state.rf,
                         r=request.app.state.config.RELEVANCE_THRESHOLD,
                         r=request.app.state.config.RELEVANCE_THRESHOLD,
                         hybrid_search=request.app.state.config.ENABLE_RAG_HYBRID_SEARCH,
                         hybrid_search=request.app.state.config.ENABLE_RAG_HYBRID_SEARCH,
+                        full_context=request.app.state.config.RAG_FULL_CONTEXT,
                     ),
                     ),
                 )
                 )
-
         except Exception as e:
         except Exception as e:
             log.exception(e)
             log.exception(e)
 
 
@@ -682,6 +695,25 @@ async def process_chat_payload(request, form_data, metadata, user, model):
 
 
     variables = form_data.pop("variables", None)
     variables = form_data.pop("variables", None)
 
 
+    # Process the form_data through the pipeline
+    try:
+        form_data = await process_pipeline_inlet_filter(
+            request, form_data, user, models
+        )
+    except Exception as e:
+        raise e
+
+    try:
+        form_data, flags = await process_filter_functions(
+            request=request,
+            filter_ids=get_sorted_filter_ids(model),
+            filter_type="inlet",
+            form_data=form_data,
+            extra_params=extra_params,
+        )
+    except Exception as e:
+        raise Exception(f"Error: {e}")
+
     features = form_data.pop("features", None)
     features = form_data.pop("features", None)
     if features:
     if features:
         if "web_search" in features and features["web_search"]:
         if "web_search" in features and features["web_search"]:
@@ -704,17 +736,6 @@ async def process_chat_payload(request, form_data, metadata, user, model):
                 form_data["messages"],
                 form_data["messages"],
             )
             )
 
 
-    try:
-        form_data, flags = await process_filter_functions(
-            request=request,
-            filter_ids=get_sorted_filter_ids(model),
-            filter_type="inlet",
-            form_data=form_data,
-            extra_params=extra_params,
-        )
-    except Exception as e:
-        raise Exception(f"Error: {e}")
-
     tool_ids = form_data.pop("tool_ids", None)
     tool_ids = form_data.pop("tool_ids", None)
     files = form_data.pop("files", None)
     files = form_data.pop("files", None)
     # Remove files duplicates
     # Remove files duplicates
@@ -778,7 +799,7 @@ async def process_chat_payload(request, form_data, metadata, user, model):
 
 
             if "document" in source:
             if "document" in source:
                 for doc_idx, doc_context in enumerate(source["document"]):
                 for doc_idx, doc_context in enumerate(source["document"]):
-                    context_string += f"<source><source_id>{doc_idx}</source_id><source_context>{doc_context}</source_context></source>\n"
+                    context_string += f"<source><source_id>{source_idx}</source_id><source_context>{doc_context}</source_context></source>\n"
 
 
         context_string = context_string.strip()
         context_string = context_string.strip()
         prompt = get_last_user_message(form_data["messages"])
         prompt = get_last_user_message(form_data["messages"])
@@ -795,7 +816,7 @@ async def process_chat_payload(request, form_data, metadata, user, model):
 
 
         # Workaround for Ollama 2.0+ system prompt issue
         # Workaround for Ollama 2.0+ system prompt issue
         # TODO: replace with add_or_update_system_message
         # TODO: replace with add_or_update_system_message
-        if model["owned_by"] == "ollama":
+        if model.get("owned_by") == "ollama":
             form_data["messages"] = prepend_to_first_user_message_content(
             form_data["messages"] = prepend_to_first_user_message_content(
                 rag_template(
                 rag_template(
                     request.app.state.config.RAG_TEMPLATE, context_string, prompt
                     request.app.state.config.RAG_TEMPLATE, context_string, prompt
@@ -1003,6 +1024,7 @@ async def process_chat_response(
                         webhook_url = Users.get_user_webhook_url_by_id(user.id)
                         webhook_url = Users.get_user_webhook_url_by_id(user.id)
                         if webhook_url:
                         if webhook_url:
                             post_webhook(
                             post_webhook(
+                                request.app.state.WEBUI_NAME,
                                 webhook_url,
                                 webhook_url,
                                 f"{title} - {request.app.state.config.WEBUI_URL}/c/{metadata['chat_id']}\n\n{content}",
                                 f"{title} - {request.app.state.config.WEBUI_URL}/c/{metadata['chat_id']}\n\n{content}",
                                 {
                                 {
@@ -1151,6 +1173,46 @@ async def process_chat_response(
 
 
                 return content.strip()
                 return content.strip()
 
 
+            def convert_content_blocks_to_messages(content_blocks):
+                messages = []
+
+                temp_blocks = []
+                for idx, block in enumerate(content_blocks):
+                    if block["type"] == "tool_calls":
+                        messages.append(
+                            {
+                                "role": "assistant",
+                                "content": serialize_content_blocks(temp_blocks),
+                                "tool_calls": block.get("content"),
+                            }
+                        )
+
+                        results = block.get("results", [])
+
+                        for result in results:
+                            messages.append(
+                                {
+                                    "role": "tool",
+                                    "tool_call_id": result["tool_call_id"],
+                                    "content": result["content"],
+                                }
+                            )
+                        temp_blocks = []
+                    else:
+                        temp_blocks.append(block)
+
+                if temp_blocks:
+                    content = serialize_content_blocks(temp_blocks)
+                    if content:
+                        messages.append(
+                            {
+                                "role": "assistant",
+                                "content": content,
+                            }
+                        )
+
+                return messages
+
             def tag_content_handler(content_type, tags, content, content_blocks):
             def tag_content_handler(content_type, tags, content, content_blocks):
                 end_flag = False
                 end_flag = False
 
 
@@ -1301,7 +1363,22 @@ async def process_chat_response(
             )
             )
 
 
             tool_calls = []
             tool_calls = []
-            content = message.get("content", "") if message else ""
+
+            last_assistant_message = None
+            try:
+                if form_data["messages"][-1]["role"] == "assistant":
+                    last_assistant_message = get_last_assistant_message(
+                        form_data["messages"]
+                    )
+            except Exception as e:
+                pass
+
+            content = (
+                message.get("content", "")
+                if message
+                else last_assistant_message if last_assistant_message else ""
+            )
+
             content_blocks = [
             content_blocks = [
                 {
                 {
                     "type": "text",
                     "type": "text",
@@ -1542,7 +1619,6 @@ async def process_chat_response(
 
 
                     results = []
                     results = []
                     for tool_call in response_tool_calls:
                     for tool_call in response_tool_calls:
-                        print("\n\n" + str(tool_call) + "\n\n")
                         tool_call_id = tool_call.get("id", "")
                         tool_call_id = tool_call.get("id", "")
                         tool_name = tool_call.get("function", {}).get("name", "")
                         tool_name = tool_call.get("function", {}).get("name", "")
 
 
@@ -1608,23 +1684,10 @@ async def process_chat_response(
                             {
                             {
                                 "model": model_id,
                                 "model": model_id,
                                 "stream": True,
                                 "stream": True,
+                                "tools": form_data["tools"],
                                 "messages": [
                                 "messages": [
                                     *form_data["messages"],
                                     *form_data["messages"],
-                                    {
-                                        "role": "assistant",
-                                        "content": serialize_content_blocks(
-                                            content_blocks, raw=True
-                                        ),
-                                        "tool_calls": response_tool_calls,
-                                    },
-                                    *[
-                                        {
-                                            "role": "tool",
-                                            "tool_call_id": result["tool_call_id"],
-                                            "content": result["content"],
-                                        }
-                                        for result in results
-                                    ],
+                                    *convert_content_blocks_to_messages(content_blocks),
                                 ],
                                 ],
                             },
                             },
                             user,
                             user,
@@ -1698,6 +1761,7 @@ async def process_chat_response(
                                             == "password"
                                             == "password"
                                             else None
                                             else None
                                         ),
                                         ),
+                                        request.app.state.config.CODE_INTERPRETER_JUPYTER_TIMEOUT,
                                     )
                                     )
                                 else:
                                 else:
                                     output = {
                                     output = {
@@ -1842,6 +1906,7 @@ async def process_chat_response(
                     webhook_url = Users.get_user_webhook_url_by_id(user.id)
                     webhook_url = Users.get_user_webhook_url_by_id(user.id)
                     if webhook_url:
                     if webhook_url:
                         post_webhook(
                         post_webhook(
+                            request.app.state.WEBUI_NAME,
                             webhook_url,
                             webhook_url,
                             f"{title} - {request.app.state.config.WEBUI_URL}/c/{metadata['chat_id']}\n\n{content}",
                             f"{title} - {request.app.state.config.WEBUI_URL}/c/{metadata['chat_id']}\n\n{content}",
                             {
                             {

+ 5 - 4
backend/open_webui/utils/misc.py

@@ -225,10 +225,11 @@ def openai_chat_completion_message_template(
     template = openai_chat_message_template(model)
     template = openai_chat_message_template(model)
     template["object"] = "chat.completion"
     template["object"] = "chat.completion"
     if message is not None:
     if message is not None:
-        template["choices"][0]["message"] = {"content": message, "role": "assistant"}
-
-    if tool_calls:
-        template["choices"][0]["tool_calls"] = tool_calls
+        template["choices"][0]["message"] = {
+            "content": message,
+            "role": "assistant",
+            **({"tool_calls": tool_calls} if tool_calls else {}),
+        }
 
 
     template["choices"][0]["finish_reason"] = "stop"
     template["choices"][0]["finish_reason"] = "stop"
 
 

+ 1 - 1
backend/open_webui/utils/models.py

@@ -143,7 +143,7 @@ async def get_all_models(request, user: UserModel = None):
                     custom_model.base_model_id == model["id"]
                     custom_model.base_model_id == model["id"]
                     or custom_model.base_model_id == model["id"].split(":")[0]
                     or custom_model.base_model_id == model["id"].split(":")[0]
                 ):
                 ):
-                    owned_by = model["owned_by"]
+                    owned_by = model.get("owned_by", "unknown owner")
                     if "pipe" in model:
                     if "pipe" in model:
                         pipe = model["pipe"]
                         pipe = model["pipe"]
                     break
                     break

+ 68 - 14
backend/open_webui/utils/oauth.py

@@ -36,7 +36,11 @@ from open_webui.config import (
     AppConfig,
     AppConfig,
 )
 )
 from open_webui.constants import ERROR_MESSAGES, WEBHOOK_MESSAGES
 from open_webui.constants import ERROR_MESSAGES, WEBHOOK_MESSAGES
-from open_webui.env import WEBUI_AUTH_COOKIE_SAME_SITE, WEBUI_AUTH_COOKIE_SECURE
+from open_webui.env import (
+    WEBUI_NAME,
+    WEBUI_AUTH_COOKIE_SAME_SITE,
+    WEBUI_AUTH_COOKIE_SECURE,
+)
 from open_webui.utils.misc import parse_duration
 from open_webui.utils.misc import parse_duration
 from open_webui.utils.auth import get_password_hash, create_token
 from open_webui.utils.auth import get_password_hash, create_token
 from open_webui.utils.webhook import post_webhook
 from open_webui.utils.webhook import post_webhook
@@ -66,8 +70,9 @@ auth_manager_config.JWT_EXPIRES_IN = JWT_EXPIRES_IN
 
 
 
 
 class OAuthManager:
 class OAuthManager:
-    def __init__(self):
+    def __init__(self, app):
         self.oauth = OAuth()
         self.oauth = OAuth()
+        self.app = app
         for _, provider_config in OAUTH_PROVIDERS.items():
         for _, provider_config in OAUTH_PROVIDERS.items():
             provider_config["register"](self.oauth)
             provider_config["register"](self.oauth)
 
 
@@ -135,7 +140,14 @@ class OAuthManager:
         log.debug("Running OAUTH Group management")
         log.debug("Running OAUTH Group management")
         oauth_claim = auth_manager_config.OAUTH_GROUPS_CLAIM
         oauth_claim = auth_manager_config.OAUTH_GROUPS_CLAIM
 
 
-        user_oauth_groups: list[str] = user_data.get(oauth_claim, list())
+        # Nested claim search for groups claim
+        if oauth_claim:
+            claim_data = user_data
+            nested_claims = oauth_claim.split(".")
+            for nested_claim in nested_claims:
+                claim_data = claim_data.get(nested_claim, {})
+            user_oauth_groups = claim_data if isinstance(claim_data, list) else None
+
         user_current_groups: list[GroupModel] = Groups.get_groups_by_member_id(user.id)
         user_current_groups: list[GroupModel] = Groups.get_groups_by_member_id(user.id)
         all_available_groups: list[GroupModel] = Groups.get_groups()
         all_available_groups: list[GroupModel] = Groups.get_groups()
 
 
@@ -200,7 +212,7 @@ class OAuthManager:
                     id=group_model.id, form_data=update_form, overwrite=False
                     id=group_model.id, form_data=update_form, overwrite=False
                 )
                 )
 
 
-    async def handle_login(self, provider, request):
+    async def handle_login(self, request, provider):
         if provider not in OAUTH_PROVIDERS:
         if provider not in OAUTH_PROVIDERS:
             raise HTTPException(404)
             raise HTTPException(404)
         # If the provider has a custom redirect URL, use that, otherwise automatically generate one
         # If the provider has a custom redirect URL, use that, otherwise automatically generate one
@@ -212,7 +224,7 @@ class OAuthManager:
             raise HTTPException(404)
             raise HTTPException(404)
         return await client.authorize_redirect(request, redirect_uri)
         return await client.authorize_redirect(request, redirect_uri)
 
 
-    async def handle_callback(self, provider, request, response):
+    async def handle_callback(self, request, provider, response):
         if provider not in OAUTH_PROVIDERS:
         if provider not in OAUTH_PROVIDERS:
             raise HTTPException(404)
             raise HTTPException(404)
         client = self.get_client(provider)
         client = self.get_client(provider)
@@ -234,11 +246,46 @@ class OAuthManager:
             raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
             raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
         provider_sub = f"{provider}@{sub}"
         provider_sub = f"{provider}@{sub}"
         email_claim = auth_manager_config.OAUTH_EMAIL_CLAIM
         email_claim = auth_manager_config.OAUTH_EMAIL_CLAIM
-        email = user_data.get(email_claim, "").lower()
+        email = user_data.get(email_claim, "")
         # We currently mandate that email addresses are provided
         # We currently mandate that email addresses are provided
         if not email:
         if not email:
-            log.warning(f"OAuth callback failed, email is missing: {user_data}")
-            raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
+            # If the provider is GitHub,and public email is not provided, we can use the access token to fetch the user's email
+            if provider == "github":
+                try:
+                    access_token = token.get("access_token")
+                    headers = {"Authorization": f"Bearer {access_token}"}
+                    async with aiohttp.ClientSession() as session:
+                        async with session.get(
+                            "https://api.github.com/user/emails", headers=headers
+                        ) as resp:
+                            if resp.ok:
+                                emails = await resp.json()
+                                # use the primary email as the user's email
+                                primary_email = next(
+                                    (e["email"] for e in emails if e.get("primary")),
+                                    None,
+                                )
+                                if primary_email:
+                                    email = primary_email
+                                else:
+                                    log.warning(
+                                        "No primary email found in GitHub response"
+                                    )
+                                    raise HTTPException(
+                                        400, detail=ERROR_MESSAGES.INVALID_CRED
+                                    )
+                            else:
+                                log.warning("Failed to fetch GitHub email")
+                                raise HTTPException(
+                                    400, detail=ERROR_MESSAGES.INVALID_CRED
+                                )
+                except Exception as e:
+                    log.warning(f"Error fetching GitHub email: {e}")
+                    raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
+            else:
+                log.warning(f"OAuth callback failed, email is missing: {user_data}")
+                raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
+        email = email.lower()
         if (
         if (
             "*" not in auth_manager_config.OAUTH_ALLOWED_DOMAINS
             "*" not in auth_manager_config.OAUTH_ALLOWED_DOMAINS
             and email.split("@")[-1] not in auth_manager_config.OAUTH_ALLOWED_DOMAINS
             and email.split("@")[-1] not in auth_manager_config.OAUTH_ALLOWED_DOMAINS
@@ -266,12 +313,21 @@ class OAuthManager:
                 Users.update_user_role_by_id(user.id, determined_role)
                 Users.update_user_role_by_id(user.id, determined_role)
 
 
         if not user:
         if not user:
+            user_count = Users.get_num_users()
+
+            if (
+                request.app.state.USER_COUNT
+                and user_count >= request.app.state.USER_COUNT
+            ):
+                raise HTTPException(
+                    403,
+                    detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
+                )
+
             # If the user does not exist, check if signups are enabled
             # If the user does not exist, check if signups are enabled
             if auth_manager_config.ENABLE_OAUTH_SIGNUP:
             if auth_manager_config.ENABLE_OAUTH_SIGNUP:
                 # Check if an existing user with the same email already exists
                 # Check if an existing user with the same email already exists
-                existing_user = Users.get_user_by_email(
-                    user_data.get("email", "").lower()
-                )
+                existing_user = Users.get_user_by_email(email)
                 if existing_user:
                 if existing_user:
                     raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN)
                     raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN)
 
 
@@ -334,6 +390,7 @@ class OAuthManager:
 
 
                 if auth_manager_config.WEBHOOK_URL:
                 if auth_manager_config.WEBHOOK_URL:
                     post_webhook(
                     post_webhook(
+                        WEBUI_NAME,
                         auth_manager_config.WEBHOOK_URL,
                         auth_manager_config.WEBHOOK_URL,
                         WEBHOOK_MESSAGES.USER_SIGNUP(user.name),
                         WEBHOOK_MESSAGES.USER_SIGNUP(user.name),
                         {
                         {
@@ -380,6 +437,3 @@ class OAuthManager:
         # Redirect back to the frontend with the JWT token
         # Redirect back to the frontend with the JWT token
         redirect_url = f"{request.base_url}auth#token={jwt_token}"
         redirect_url = f"{request.base_url}auth#token={jwt_token}"
         return RedirectResponse(url=redirect_url, headers=response.headers)
         return RedirectResponse(url=redirect_url, headers=response.headers)
-
-
-oauth_manager = OAuthManager()

+ 77 - 49
backend/open_webui/utils/payload.py

@@ -4,6 +4,7 @@ from open_webui.utils.misc import (
 )
 )
 
 
 from typing import Callable, Optional
 from typing import Callable, Optional
+import json
 
 
 
 
 # inplace function: form_data is modified
 # inplace function: form_data is modified
@@ -66,38 +67,49 @@ def apply_model_params_to_body_openai(params: dict, form_data: dict) -> dict:
 
 
 
 
 def apply_model_params_to_body_ollama(params: dict, form_data: dict) -> dict:
 def apply_model_params_to_body_ollama(params: dict, form_data: dict) -> dict:
-    opts = [
-        "temperature",
-        "top_p",
-        "seed",
-        "mirostat",
-        "mirostat_eta",
-        "mirostat_tau",
-        "num_ctx",
-        "num_batch",
-        "num_keep",
-        "repeat_last_n",
-        "tfs_z",
-        "top_k",
-        "min_p",
-        "use_mmap",
-        "use_mlock",
-        "num_thread",
-        "num_gpu",
-    ]
-    mappings = {i: lambda x: x for i in opts}
-    form_data = apply_model_params_to_body(params, form_data, mappings)
-
+    # Convert OpenAI parameter names to Ollama parameter names if needed.
     name_differences = {
     name_differences = {
         "max_tokens": "num_predict",
         "max_tokens": "num_predict",
-        "frequency_penalty": "repeat_penalty",
     }
     }
 
 
     for key, value in name_differences.items():
     for key, value in name_differences.items():
         if (param := params.get(key, None)) is not None:
         if (param := params.get(key, None)) is not None:
-            form_data[value] = param
+            # Copy the parameter to new name then delete it, to prevent Ollama warning of invalid option provided
+            params[value] = params[key]
+            del params[key]
 
 
-    return form_data
+    # See https://github.com/ollama/ollama/blob/main/docs/api.md#request-8
+    mappings = {
+        "temperature": float,
+        "top_p": float,
+        "seed": lambda x: x,
+        "mirostat": int,
+        "mirostat_eta": float,
+        "mirostat_tau": float,
+        "num_ctx": int,
+        "num_batch": int,
+        "num_keep": int,
+        "num_predict": int,
+        "repeat_last_n": int,
+        "top_k": int,
+        "min_p": float,
+        "typical_p": float,
+        "repeat_penalty": float,
+        "presence_penalty": float,
+        "frequency_penalty": float,
+        "penalize_newline": bool,
+        "stop": lambda x: [bytes(s, "utf-8").decode("unicode_escape") for s in x],
+        "numa": bool,
+        "num_gpu": int,
+        "main_gpu": int,
+        "low_vram": bool,
+        "vocab_only": bool,
+        "use_mmap": bool,
+        "use_mlock": bool,
+        "num_thread": int,
+    }
+
+    return apply_model_params_to_body(params, form_data, mappings)
 
 
 
 
 def convert_messages_openai_to_ollama(messages: list[dict]) -> list[dict]:
 def convert_messages_openai_to_ollama(messages: list[dict]) -> list[dict]:
@@ -108,11 +120,38 @@ def convert_messages_openai_to_ollama(messages: list[dict]) -> list[dict]:
         new_message = {"role": message["role"]}
         new_message = {"role": message["role"]}
 
 
         content = message.get("content", [])
         content = message.get("content", [])
+        tool_calls = message.get("tool_calls", None)
+        tool_call_id = message.get("tool_call_id", None)
 
 
         # Check if the content is a string (just a simple message)
         # Check if the content is a string (just a simple message)
         if isinstance(content, str):
         if isinstance(content, str):
             # If the content is a string, it's pure text
             # If the content is a string, it's pure text
             new_message["content"] = content
             new_message["content"] = content
+
+            # If message is a tool call, add the tool call id to the message
+            if tool_call_id:
+                new_message["tool_call_id"] = tool_call_id
+
+        elif tool_calls:
+            # If tool calls are present, add them to the message
+            ollama_tool_calls = []
+            for tool_call in tool_calls:
+                ollama_tool_call = {
+                    "index": tool_call.get("index", 0),
+                    "id": tool_call.get("id", None),
+                    "function": {
+                        "name": tool_call.get("function", {}).get("name", ""),
+                        "arguments": json.loads(
+                            tool_call.get("function", {}).get("arguments", {})
+                        ),
+                    },
+                }
+                ollama_tool_calls.append(ollama_tool_call)
+            new_message["tool_calls"] = ollama_tool_calls
+
+            # Put the content to empty string (Ollama requires an empty string for tool calls)
+            new_message["content"] = ""
+
         else:
         else:
             # Otherwise, assume the content is a list of dicts, e.g., text followed by an image URL
             # Otherwise, assume the content is a list of dicts, e.g., text followed by an image URL
             content_text = ""
             content_text = ""
@@ -173,34 +212,23 @@ def convert_payload_openai_to_ollama(openai_payload: dict) -> dict:
         ollama_payload["format"] = openai_payload["format"]
         ollama_payload["format"] = openai_payload["format"]
 
 
     # If there are advanced parameters in the payload, format them in Ollama's options field
     # If there are advanced parameters in the payload, format them in Ollama's options field
-    ollama_options = {}
-
     if openai_payload.get("options"):
     if openai_payload.get("options"):
         ollama_payload["options"] = openai_payload["options"]
         ollama_payload["options"] = openai_payload["options"]
         ollama_options = openai_payload["options"]
         ollama_options = openai_payload["options"]
 
 
-    # Handle parameters which map directly
-    for param in ["temperature", "top_p", "seed"]:
-        if param in openai_payload:
-            ollama_options[param] = openai_payload[param]
-
-    # Mapping OpenAI's `max_tokens` -> Ollama's `num_predict`
-    if "max_completion_tokens" in openai_payload:
-        ollama_options["num_predict"] = openai_payload["max_completion_tokens"]
-    elif "max_tokens" in openai_payload:
-        ollama_options["num_predict"] = openai_payload["max_tokens"]
-
-    # Handle frequency / presence_penalty, which needs renaming and checking
-    if "frequency_penalty" in openai_payload:
-        ollama_options["repeat_penalty"] = openai_payload["frequency_penalty"]
-
-    if "presence_penalty" in openai_payload and "penalty" not in ollama_options:
-        # We are assuming presence penalty uses a similar concept in Ollama, which needs custom handling if exists.
-        ollama_options["new_topic_penalty"] = openai_payload["presence_penalty"]
-
-    # Add options to payload if any have been set
-    if ollama_options:
-        ollama_payload["options"] = ollama_options
+        # Re-Mapping OpenAI's `max_tokens` -> Ollama's `num_predict`
+        if "max_tokens" in ollama_options:
+            ollama_options["num_predict"] = ollama_options["max_tokens"]
+            del ollama_options[
+                "max_tokens"
+            ]  # To prevent Ollama warning of invalid option provided
+
+        # Ollama lacks a "system" prompt option. It has to be provided as a direct parameter, so we copy it down.
+        if "system" in ollama_options:
+            ollama_payload["system"] = ollama_options["system"]
+            del ollama_options[
+                "system"
+            ]  # To prevent Ollama warning of invalid option provided
 
 
     if "metadata" in openai_payload:
     if "metadata" in openai_payload:
         ollama_payload["metadata"] = openai_payload["metadata"]
         ollama_payload["metadata"] = openai_payload["metadata"]

+ 31 - 50
backend/open_webui/utils/response.py

@@ -24,17 +24,8 @@ def convert_ollama_tool_call_to_openai(tool_calls: dict) -> dict:
     return openai_tool_calls
     return openai_tool_calls
 
 
 
 
-def convert_response_ollama_to_openai(ollama_response: dict) -> dict:
-    model = ollama_response.get("model", "ollama")
-    message_content = ollama_response.get("message", {}).get("content", "")
-    tool_calls = ollama_response.get("message", {}).get("tool_calls", None)
-    openai_tool_calls = None
-
-    if tool_calls:
-        openai_tool_calls = convert_ollama_tool_call_to_openai(tool_calls)
-
-    data = ollama_response
-    usage = {
+def convert_ollama_usage_to_openai(data: dict) -> dict:
+    return {
         "response_token/s": (
         "response_token/s": (
             round(
             round(
                 (
                 (
@@ -66,14 +57,42 @@ def convert_response_ollama_to_openai(ollama_response: dict) -> dict:
         "total_duration": data.get("total_duration", 0),
         "total_duration": data.get("total_duration", 0),
         "load_duration": data.get("load_duration", 0),
         "load_duration": data.get("load_duration", 0),
         "prompt_eval_count": data.get("prompt_eval_count", 0),
         "prompt_eval_count": data.get("prompt_eval_count", 0),
+        "prompt_tokens": int(
+            data.get("prompt_eval_count", 0)
+        ),  # This is the OpenAI compatible key
         "prompt_eval_duration": data.get("prompt_eval_duration", 0),
         "prompt_eval_duration": data.get("prompt_eval_duration", 0),
         "eval_count": data.get("eval_count", 0),
         "eval_count": data.get("eval_count", 0),
+        "completion_tokens": int(
+            data.get("eval_count", 0)
+        ),  # This is the OpenAI compatible key
         "eval_duration": data.get("eval_duration", 0),
         "eval_duration": data.get("eval_duration", 0),
         "approximate_total": (lambda s: f"{s // 3600}h{(s % 3600) // 60}m{s % 60}s")(
         "approximate_total": (lambda s: f"{s // 3600}h{(s % 3600) // 60}m{s % 60}s")(
             (data.get("total_duration", 0) or 0) // 1_000_000_000
             (data.get("total_duration", 0) or 0) // 1_000_000_000
         ),
         ),
+        "total_tokens": int(  # This is the OpenAI compatible key
+            data.get("prompt_eval_count", 0) + data.get("eval_count", 0)
+        ),
+        "completion_tokens_details": {  # This is the OpenAI compatible key
+            "reasoning_tokens": 0,
+            "accepted_prediction_tokens": 0,
+            "rejected_prediction_tokens": 0,
+        },
     }
     }
 
 
+
+def convert_response_ollama_to_openai(ollama_response: dict) -> dict:
+    model = ollama_response.get("model", "ollama")
+    message_content = ollama_response.get("message", {}).get("content", "")
+    tool_calls = ollama_response.get("message", {}).get("tool_calls", None)
+    openai_tool_calls = None
+
+    if tool_calls:
+        openai_tool_calls = convert_ollama_tool_call_to_openai(tool_calls)
+
+    data = ollama_response
+
+    usage = convert_ollama_usage_to_openai(data)
+
     response = openai_chat_completion_message_template(
     response = openai_chat_completion_message_template(
         model, message_content, openai_tool_calls, usage
         model, message_content, openai_tool_calls, usage
     )
     )
@@ -96,45 +115,7 @@ async def convert_streaming_response_ollama_to_openai(ollama_streaming_response)
 
 
         usage = None
         usage = None
         if done:
         if done:
-            usage = {
-                "response_token/s": (
-                    round(
-                        (
-                            (
-                                data.get("eval_count", 0)
-                                / ((data.get("eval_duration", 0) / 10_000_000))
-                            )
-                            * 100
-                        ),
-                        2,
-                    )
-                    if data.get("eval_duration", 0) > 0
-                    else "N/A"
-                ),
-                "prompt_token/s": (
-                    round(
-                        (
-                            (
-                                data.get("prompt_eval_count", 0)
-                                / ((data.get("prompt_eval_duration", 0) / 10_000_000))
-                            )
-                            * 100
-                        ),
-                        2,
-                    )
-                    if data.get("prompt_eval_duration", 0) > 0
-                    else "N/A"
-                ),
-                "total_duration": data.get("total_duration", 0),
-                "load_duration": data.get("load_duration", 0),
-                "prompt_eval_count": data.get("prompt_eval_count", 0),
-                "prompt_eval_duration": data.get("prompt_eval_duration", 0),
-                "eval_count": data.get("eval_count", 0),
-                "eval_duration": data.get("eval_duration", 0),
-                "approximate_total": (
-                    lambda s: f"{s // 3600}h{(s % 3600) // 60}m{s % 60}s"
-                )((data.get("total_duration", 0) or 0) // 1_000_000_000),
-            }
+            usage = convert_ollama_usage_to_openai(data)
 
 
         data = openai_chat_chunk_message_template(
         data = openai_chat_chunk_message_template(
             model, message_content if not done else None, openai_tool_calls, usage
             model, message_content if not done else None, openai_tool_calls, usage

+ 1 - 1
backend/open_webui/utils/task.py

@@ -22,7 +22,7 @@ def get_task_model_id(
     # Set the task model
     # Set the task model
     task_model_id = default_model_id
     task_model_id = default_model_id
     # Check if the user has a custom task model and use that model
     # Check if the user has a custom task model and use that model
-    if models[task_model_id]["owned_by"] == "ollama":
+    if models[task_model_id].get("owned_by") == "ollama":
         if task_model and task_model in models:
         if task_model and task_model in models:
             task_model_id = task_model
             task_model_id = task_model
     else:
     else:

+ 3 - 3
backend/open_webui/utils/webhook.py

@@ -2,14 +2,14 @@ import json
 import logging
 import logging
 
 
 import requests
 import requests
-from open_webui.config import WEBUI_FAVICON_URL, WEBUI_NAME
+from open_webui.config import WEBUI_FAVICON_URL
 from open_webui.env import SRC_LOG_LEVELS, VERSION
 from open_webui.env import SRC_LOG_LEVELS, VERSION
 
 
 log = logging.getLogger(__name__)
 log = logging.getLogger(__name__)
 log.setLevel(SRC_LOG_LEVELS["WEBHOOK"])
 log.setLevel(SRC_LOG_LEVELS["WEBHOOK"])
 
 
 
 
-def post_webhook(url: str, message: str, event_data: dict) -> bool:
+def post_webhook(name: str, url: str, message: str, event_data: dict) -> bool:
     try:
     try:
         log.debug(f"post_webhook: {url}, {message}, {event_data}")
         log.debug(f"post_webhook: {url}, {message}, {event_data}")
         payload = {}
         payload = {}
@@ -39,7 +39,7 @@ def post_webhook(url: str, message: str, event_data: dict) -> bool:
                 "sections": [
                 "sections": [
                     {
                     {
                         "activityTitle": message,
                         "activityTitle": message,
-                        "activitySubtitle": f"{WEBUI_NAME} ({VERSION}) - {action}",
+                        "activitySubtitle": f"{name} ({VERSION}) - {action}",
                         "activityImage": WEBUI_FAVICON_URL,
                         "activityImage": WEBUI_FAVICON_URL,
                         "facts": facts,
                         "facts": facts,
                         "markdown": True,
                         "markdown": True,

+ 11 - 7
backend/requirements.txt

@@ -1,13 +1,10 @@
 fastapi==0.115.7
 fastapi==0.115.7
 uvicorn[standard]==0.30.6
 uvicorn[standard]==0.30.6
-pydantic==2.9.2
+pydantic==2.10.6
 python-multipart==0.0.18
 python-multipart==0.0.18
 
 
-Flask==3.1.0
-Flask-Cors==5.0.0
-
 python-socketio==5.11.3
 python-socketio==5.11.3
-python-jose==3.3.0
+python-jose==3.4.0
 passlib[bcrypt]==1.7.4
 passlib[bcrypt]==1.7.4
 
 
 requests==2.32.3
 requests==2.32.3
@@ -48,7 +45,7 @@ chromadb==0.6.2
 pymilvus==2.5.0
 pymilvus==2.5.0
 qdrant-client~=1.12.0
 qdrant-client~=1.12.0
 opensearch-py==2.8.0
 opensearch-py==2.8.0
-
+playwright==1.49.1 # Caution: version must match docker-compose.playwright.yaml
 
 
 transformers
 transformers
 sentence-transformers==3.3.1
 sentence-transformers==3.3.1
@@ -62,7 +59,7 @@ fpdf2==2.8.2
 pymdown-extensions==10.14.2
 pymdown-extensions==10.14.2
 docx2txt==0.8
 docx2txt==0.8
 python-pptx==1.0.0
 python-pptx==1.0.0
-unstructured==0.16.11
+unstructured==0.16.17
 nltk==3.9.1
 nltk==3.9.1
 Markdown==3.7
 Markdown==3.7
 pypandoc==1.13
 pypandoc==1.13
@@ -106,5 +103,12 @@ pytest-docker~=3.1.1
 googleapis-common-protos==1.63.2
 googleapis-common-protos==1.63.2
 google-cloud-storage==2.19.0
 google-cloud-storage==2.19.0
 
 
+azure-identity==1.20.0
+azure-storage-blob==12.24.1
+
+
 ## LDAP
 ## LDAP
 ldap3==2.9.1
 ldap3==2.9.1
+
+## Firecrawl
+firecrawl-py==1.12.0

+ 11 - 0
backend/start.sh

@@ -3,6 +3,17 @@
 SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
 SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
 cd "$SCRIPT_DIR" || exit
 cd "$SCRIPT_DIR" || exit
 
 
+# Add conditional Playwright browser installation
+if [[ "${RAG_WEB_LOADER_ENGINE,,}" == "playwright" ]]; then
+    if [[ -z "${PLAYWRIGHT_WS_URI}" ]]; then
+        echo "Installing Playwright browsers..."
+        playwright install chromium
+        playwright install-deps chromium
+    fi
+
+    python -c "import nltk; nltk.download('punkt_tab')"
+fi
+
 KEY_FILE=.webui_secret_key
 KEY_FILE=.webui_secret_key
 
 
 PORT="${PORT:-8080}"
 PORT="${PORT:-8080}"

+ 11 - 0
backend/start_windows.bat

@@ -6,6 +6,17 @@ SETLOCAL ENABLEDELAYEDEXPANSION
 SET "SCRIPT_DIR=%~dp0"
 SET "SCRIPT_DIR=%~dp0"
 cd /d "%SCRIPT_DIR%" || exit /b
 cd /d "%SCRIPT_DIR%" || exit /b
 
 
+:: Add conditional Playwright browser installation
+IF /I "%RAG_WEB_LOADER_ENGINE%" == "playwright" (
+    IF "%PLAYWRIGHT_WS_URI%" == "" (
+        echo Installing Playwright browsers...
+        playwright install chromium
+        playwright install-deps chromium
+    )
+
+    python -c "import nltk; nltk.download('punkt_tab')"
+)
+
 SET "KEY_FILE=.webui_secret_key"
 SET "KEY_FILE=.webui_secret_key"
 IF "%PORT%"=="" SET PORT=8080
 IF "%PORT%"=="" SET PORT=8080
 IF "%HOST%"=="" SET HOST=0.0.0.0
 IF "%HOST%"=="" SET HOST=0.0.0.0

+ 10 - 0
docker-compose.playwright.yaml

@@ -0,0 +1,10 @@
+services:
+  playwright:
+    image: mcr.microsoft.com/playwright:v1.49.1-noble # Version must match requirements.txt
+    container_name: playwright
+    command: npx -y playwright@1.49.1 run-server --port 3000 --host 0.0.0.0
+
+  open-webui:
+    environment:
+      - 'RAG_WEB_LOADER_ENGINE=playwright'
+      - 'PLAYWRIGHT_WS_URI=ws://playwright:3000'

File diff suppressed because it is too large
+ 525 - 309
package-lock.json


+ 4 - 3
package.json

@@ -1,6 +1,6 @@
 {
 {
 	"name": "open-webui",
 	"name": "open-webui",
-	"version": "0.5.11",
+	"version": "0.5.16",
 	"private": true,
 	"private": true,
 	"scripts": {
 	"scripts": {
 		"dev": "npm run pyodide:fetch && vite dev --host",
 		"dev": "npm run pyodide:fetch && vite dev --host",
@@ -26,10 +26,10 @@
 		"@sveltejs/kit": "^2.5.20",
 		"@sveltejs/kit": "^2.5.20",
 		"@sveltejs/vite-plugin-svelte": "^3.1.1",
 		"@sveltejs/vite-plugin-svelte": "^3.1.1",
 		"@tailwindcss/container-queries": "^0.1.1",
 		"@tailwindcss/container-queries": "^0.1.1",
+		"@tailwindcss/postcss": "^4.0.0",
 		"@tailwindcss/typography": "^0.5.13",
 		"@tailwindcss/typography": "^0.5.13",
 		"@typescript-eslint/eslint-plugin": "^6.17.0",
 		"@typescript-eslint/eslint-plugin": "^6.17.0",
 		"@typescript-eslint/parser": "^6.17.0",
 		"@typescript-eslint/parser": "^6.17.0",
-		"autoprefixer": "^10.4.16",
 		"cypress": "^13.15.0",
 		"cypress": "^13.15.0",
 		"eslint": "^8.56.0",
 		"eslint": "^8.56.0",
 		"eslint-config-prettier": "^9.1.0",
 		"eslint-config-prettier": "^9.1.0",
@@ -43,7 +43,7 @@
 		"svelte": "^4.2.18",
 		"svelte": "^4.2.18",
 		"svelte-check": "^3.8.5",
 		"svelte-check": "^3.8.5",
 		"svelte-confetti": "^1.3.2",
 		"svelte-confetti": "^1.3.2",
-		"tailwindcss": "^3.3.3",
+		"tailwindcss": "^4.0.0",
 		"tslib": "^2.4.1",
 		"tslib": "^2.4.1",
 		"typescript": "^5.5.4",
 		"typescript": "^5.5.4",
 		"vite": "^5.4.14",
 		"vite": "^5.4.14",
@@ -106,6 +106,7 @@
 		"svelte-sonner": "^0.3.19",
 		"svelte-sonner": "^0.3.19",
 		"tippy.js": "^6.3.7",
 		"tippy.js": "^6.3.7",
 		"turndown": "^7.2.0",
 		"turndown": "^7.2.0",
+		"undici": "^7.3.0",
 		"uuid": "^9.0.1",
 		"uuid": "^9.0.1",
 		"vite-plugin-static-copy": "^2.2.0"
 		"vite-plugin-static-copy": "^2.2.0"
 	},
 	},

+ 1 - 2
postcss.config.js

@@ -1,6 +1,5 @@
 export default {
 export default {
 	plugins: {
 	plugins: {
-		tailwindcss: {},
-		autoprefixer: {}
+		'@tailwindcss/postcss': {}
 	}
 	}
 };
 };

+ 10 - 6
pyproject.toml

@@ -8,14 +8,11 @@ license = { file = "LICENSE" }
 dependencies = [
 dependencies = [
     "fastapi==0.115.7",
     "fastapi==0.115.7",
     "uvicorn[standard]==0.30.6",
     "uvicorn[standard]==0.30.6",
-    "pydantic==2.9.2",
+    "pydantic==2.10.6",
     "python-multipart==0.0.18",
     "python-multipart==0.0.18",
 
 
-    "Flask==3.1.0",
-    "Flask-Cors==5.0.0",
-
     "python-socketio==5.11.3",
     "python-socketio==5.11.3",
-    "python-jose==3.3.0",
+    "python-jose==3.4.0",
     "passlib[bcrypt]==1.7.4",
     "passlib[bcrypt]==1.7.4",
 
 
     "requests==2.32.3",
     "requests==2.32.3",
@@ -56,6 +53,7 @@ dependencies = [
     "pymilvus==2.5.0",
     "pymilvus==2.5.0",
     "qdrant-client~=1.12.0",
     "qdrant-client~=1.12.0",
     "opensearch-py==2.8.0",
     "opensearch-py==2.8.0",
+    "playwright==1.49.1",
 
 
     "transformers",
     "transformers",
     "sentence-transformers==3.3.1",
     "sentence-transformers==3.3.1",
@@ -68,7 +66,7 @@ dependencies = [
     "pymdown-extensions==10.14.2",
     "pymdown-extensions==10.14.2",
     "docx2txt==0.8",
     "docx2txt==0.8",
     "python-pptx==1.0.0",
     "python-pptx==1.0.0",
-    "unstructured==0.16.11",
+    "unstructured==0.16.17",
     "nltk==3.9.1",
     "nltk==3.9.1",
     "Markdown==3.7",
     "Markdown==3.7",
     "pypandoc==1.13",
     "pypandoc==1.13",
@@ -111,7 +109,13 @@ dependencies = [
     "googleapis-common-protos==1.63.2",
     "googleapis-common-protos==1.63.2",
     "google-cloud-storage==2.19.0",
     "google-cloud-storage==2.19.0",
 
 
+    "azure-identity==1.20.0",
+    "azure-storage-blob==12.24.1",
+
     "ldap3==2.9.1",
     "ldap3==2.9.1",
+
+    "firecrawl-py==1.12.0",
+
     "gcp-storage-emulator>=2024.8.3",
     "gcp-storage-emulator>=2024.8.3",
 ]
 ]
 readme = "README.md"
 readme = "README.md"

+ 9 - 0
run-compose.sh

@@ -74,6 +74,7 @@ usage() {
     echo "  --enable-api[port=PORT]    Enable API and expose it on the specified port."
     echo "  --enable-api[port=PORT]    Enable API and expose it on the specified port."
     echo "  --webui[port=PORT]         Set the port for the web user interface."
     echo "  --webui[port=PORT]         Set the port for the web user interface."
     echo "  --data[folder=PATH]        Bind mount for ollama data folder (by default will create the 'ollama' volume)."
     echo "  --data[folder=PATH]        Bind mount for ollama data folder (by default will create the 'ollama' volume)."
+    echo "  --playwright               Enable Playwright support for web scraping."
     echo "  --build                    Build the docker image before running the compose project."
     echo "  --build                    Build the docker image before running the compose project."
     echo "  --drop                     Drop the compose project."
     echo "  --drop                     Drop the compose project."
     echo "  -q, --quiet                Run script in headless mode."
     echo "  -q, --quiet                Run script in headless mode."
@@ -100,6 +101,7 @@ webui_port=3000
 headless=false
 headless=false
 build_image=false
 build_image=false
 kill_compose=false
 kill_compose=false
+enable_playwright=false
 
 
 # Function to extract value from the parameter
 # Function to extract value from the parameter
 extract_value() {
 extract_value() {
@@ -129,6 +131,9 @@ while [[ $# -gt 0 ]]; do
             value=$(extract_value "$key")
             value=$(extract_value "$key")
             data_dir=${value:-"./ollama-data"}
             data_dir=${value:-"./ollama-data"}
             ;;
             ;;
+        --playwright)
+            enable_playwright=true
+            ;;
         --drop)
         --drop)
             kill_compose=true
             kill_compose=true
             ;;
             ;;
@@ -182,6 +187,9 @@ else
         DEFAULT_COMPOSE_COMMAND+=" -f docker-compose.data.yaml"
         DEFAULT_COMPOSE_COMMAND+=" -f docker-compose.data.yaml"
         export OLLAMA_DATA_DIR=$data_dir # Set OLLAMA_DATA_DIR environment variable
         export OLLAMA_DATA_DIR=$data_dir # Set OLLAMA_DATA_DIR environment variable
     fi
     fi
+    if [[ $enable_playwright == true ]]; then
+        DEFAULT_COMPOSE_COMMAND+=" -f docker-compose.playwright.yaml"
+    fi
     if [[ -n $webui_port ]]; then
     if [[ -n $webui_port ]]; then
         export OPEN_WEBUI_PORT=$webui_port # Set OPEN_WEBUI_PORT environment variable
         export OPEN_WEBUI_PORT=$webui_port # Set OPEN_WEBUI_PORT environment variable
     fi
     fi
@@ -201,6 +209,7 @@ echo -e "   ${GREEN}${BOLD}GPU Count:${NC} ${OLLAMA_GPU_COUNT:-Not Enabled}"
 echo -e "   ${GREEN}${BOLD}WebAPI Port:${NC} ${OLLAMA_WEBAPI_PORT:-Not Enabled}"
 echo -e "   ${GREEN}${BOLD}WebAPI Port:${NC} ${OLLAMA_WEBAPI_PORT:-Not Enabled}"
 echo -e "   ${GREEN}${BOLD}Data Folder:${NC} ${data_dir:-Using ollama volume}"
 echo -e "   ${GREEN}${BOLD}Data Folder:${NC} ${data_dir:-Using ollama volume}"
 echo -e "   ${GREEN}${BOLD}WebUI Port:${NC} $webui_port"
 echo -e "   ${GREEN}${BOLD}WebUI Port:${NC} $webui_port"
+echo -e "   ${GREEN}${BOLD}Playwright:${NC} ${enable_playwright:-false}"
 echo
 echo
 
 
 if [[ $headless == true ]]; then
 if [[ $headless == true ]]; then

+ 32 - 0
scripts/prepare-pyodide.js

@@ -16,8 +16,39 @@ const packages = [
 ];
 ];
 
 
 import { loadPyodide } from 'pyodide';
 import { loadPyodide } from 'pyodide';
+import { setGlobalDispatcher, ProxyAgent } from 'undici';
 import { writeFile, readFile, copyFile, readdir, rmdir } from 'fs/promises';
 import { writeFile, readFile, copyFile, readdir, rmdir } from 'fs/promises';
 
 
+/**
+ * Loading network proxy configurations from the environment variables.
+ * And the proxy config with lowercase name has the highest priority to use.
+ */
+function initNetworkProxyFromEnv() {
+	// we assume all subsequent requests in this script are HTTPS:
+	// https://cdn.jsdelivr.net
+	// https://pypi.org
+	// https://files.pythonhosted.org
+	const allProxy = process.env.all_proxy || process.env.ALL_PROXY;
+	const httpsProxy = process.env.https_proxy || process.env.HTTPS_PROXY;
+	const httpProxy = process.env.http_proxy || process.env.HTTP_PROXY;
+	const preferedProxy = httpsProxy || allProxy || httpProxy;
+	/**
+	 * use only http(s) proxy because socks5 proxy is not supported currently:
+	 * @see https://github.com/nodejs/undici/issues/2224
+	 */
+	if (!preferedProxy || !preferedProxy.startsWith('http')) return;
+	let preferedProxyURL;
+	try {
+		preferedProxyURL = new URL(preferedProxy).toString();
+	} catch {
+		console.warn(`Invalid network proxy URL: "${preferedProxy}"`);
+		return;
+	}
+	const dispatcher = new ProxyAgent({ uri: preferedProxyURL });
+	setGlobalDispatcher(dispatcher);
+	console.log(`Initialized network proxy "${preferedProxy}" from env`);
+}
+
 async function downloadPackages() {
 async function downloadPackages() {
 	console.log('Setting up pyodide + micropip');
 	console.log('Setting up pyodide + micropip');
 
 
@@ -84,5 +115,6 @@ async function copyPyodide() {
 	}
 	}
 }
 }
 
 
+initNetworkProxyFromEnv();
 await downloadPackages();
 await downloadPackages();
 await copyPyodide();
 await copyPyodide();

+ 17 - 5
src/app.css

@@ -1,3 +1,5 @@
+@reference "./tailwind.css";
+
 @font-face {
 @font-face {
 	font-family: 'Inter';
 	font-family: 'Inter';
 	src: url('/assets/fonts/Inter-Variable.ttf');
 	src: url('/assets/fonts/Inter-Variable.ttf');
@@ -53,11 +55,11 @@ math {
 }
 }
 
 
 .markdown-prose {
 .markdown-prose {
-	@apply prose dark:prose-invert prose-blockquote:border-gray-100 prose-blockquote:dark:border-gray-800 prose-blockquote:border-l-2 prose-blockquote:not-italic prose-blockquote:font-normal  prose-headings:font-semibold prose-hr:my-4 prose-hr:border-gray-100 prose-hr:dark:border-gray-800 prose-p:my-0 prose-img:my-1 prose-headings:my-1 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-ul:-my-0 prose-ol:-my-0 prose-li:-my-0 whitespace-pre-line;
+	@apply prose dark:prose-invert prose-blockquote:border-s-gray-100 prose-blockquote:dark:border-gray-800 prose-blockquote:border-s-2 prose-blockquote:not-italic prose-blockquote:font-normal  prose-headings:font-semibold prose-hr:my-4 prose-hr:border-gray-100 prose-hr:dark:border-gray-800 prose-p:my-0 prose-img:my-1 prose-headings:my-1 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-ul:-my-0 prose-ol:-my-0 prose-li:-my-0 whitespace-pre-line;
 }
 }
 
 
 .markdown-prose-xs {
 .markdown-prose-xs {
-	@apply text-xs prose dark:prose-invert prose-blockquote:border-gray-100 prose-blockquote:dark:border-gray-800 prose-blockquote:border-l-2 prose-blockquote:not-italic prose-blockquote:font-normal  prose-headings:font-semibold prose-hr:my-0  prose-hr:border-gray-100 prose-hr:dark:border-gray-800 prose-p:my-0 prose-img:my-1 prose-headings:my-1 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-ul:-my-0 prose-ol:-my-0 prose-li:-my-0 whitespace-pre-line;
+	@apply text-xs prose dark:prose-invert prose-blockquote:border-s-gray-100 prose-blockquote:dark:border-gray-800 prose-blockquote:border-s-2 prose-blockquote:not-italic prose-blockquote:font-normal  prose-headings:font-semibold prose-hr:my-0  prose-hr:border-gray-100 prose-hr:dark:border-gray-800 prose-p:my-0 prose-img:my-1 prose-headings:my-1 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-ul:-my-0 prose-ol:-my-0 prose-li:-my-0 whitespace-pre-line;
 }
 }
 
 
 .markdown a {
 .markdown a {
@@ -99,7 +101,7 @@ li p {
 
 
 /* Dark theme scrollbar styles */
 /* Dark theme scrollbar styles */
 .dark ::-webkit-scrollbar-thumb {
 .dark ::-webkit-scrollbar-thumb {
-	background-color: rgba(33, 33, 33, 0.8); /* Darker color for dark theme */
+	background-color: rgba(42, 42, 42, 0.8); /* Darker color for dark theme */
 	border-color: rgba(0, 0, 0, var(--tw-border-opacity));
 	border-color: rgba(0, 0, 0, var(--tw-border-opacity));
 }
 }
 
 
@@ -217,8 +219,18 @@ input[type='number'] {
 	width: 100%;
 	width: 100%;
 }
 }
 
 
-.cm-scroller {
-	@apply scrollbar-hidden;
+.cm-scroller:active::-webkit-scrollbar-thumb,
+.cm-scroller:focus::-webkit-scrollbar-thumb,
+.cm-scroller:hover::-webkit-scrollbar-thumb {
+	visibility: visible;
+}
+
+.cm-scroller::-webkit-scrollbar-thumb {
+	visibility: hidden;
+}
+
+.cm-scroller::-webkit-scrollbar-corner {
+	display: none;
 }
 }
 
 
 .cm-editor.cm-focused {
 .cm-editor.cm-focused {

+ 1 - 0
src/app.html

@@ -21,6 +21,7 @@
 			title="Open WebUI"
 			title="Open WebUI"
 			href="/opensearch.xml"
 			href="/opensearch.xml"
 		/>
 		/>
+		<script src="/static/loader.js" defer></script>
 
 
 		<script>
 		<script>
 			function resizeIframe(obj) {
 			function resizeIframe(obj) {

+ 1 - 1
src/lib/apis/chats/index.ts

@@ -459,7 +459,7 @@ export const getChatById = async (token: string, id: string) => {
 			return json;
 			return json;
 		})
 		})
 		.catch((err) => {
 		.catch((err) => {
-			error = err;
+			error = err.detail;
 
 
 			console.log(err);
 			console.log(err);
 			return null;
 			return null;

+ 4 - 4
src/lib/apis/configs/index.ts

@@ -115,10 +115,10 @@ export const setDirectConnectionsConfig = async (token: string, config: object)
 	return res;
 	return res;
 };
 };
 
 
-export const getCodeInterpreterConfig = async (token: string) => {
+export const getCodeExecutionConfig = async (token: string) => {
 	let error = null;
 	let error = null;
 
 
-	const res = await fetch(`${WEBUI_API_BASE_URL}/configs/code_interpreter`, {
+	const res = await fetch(`${WEBUI_API_BASE_URL}/configs/code_execution`, {
 		method: 'GET',
 		method: 'GET',
 		headers: {
 		headers: {
 			'Content-Type': 'application/json',
 			'Content-Type': 'application/json',
@@ -142,10 +142,10 @@ export const getCodeInterpreterConfig = async (token: string) => {
 	return res;
 	return res;
 };
 };
 
 
-export const setCodeInterpreterConfig = async (token: string, config: object) => {
+export const setCodeExecutionConfig = async (token: string, config: object) => {
 	let error = null;
 	let error = null;
 
 
-	const res = await fetch(`${WEBUI_API_BASE_URL}/configs/code_interpreter`, {
+	const res = await fetch(`${WEBUI_API_BASE_URL}/configs/code_execution`, {
 		method: 'POST',
 		method: 'POST',
 		headers: {
 		headers: {
 			'Content-Type': 'application/json',
 			'Content-Type': 'application/json',

+ 4 - 2
src/lib/apis/users/index.ts

@@ -284,14 +284,16 @@ export const updateUserInfo = async (token: string, info: object) => {
 
 
 export const getAndUpdateUserLocation = async (token: string) => {
 export const getAndUpdateUserLocation = async (token: string) => {
 	const location = await getUserPosition().catch((err) => {
 	const location = await getUserPosition().catch((err) => {
-		throw err;
+		console.log(err);
+		return null;
 	});
 	});
 
 
 	if (location) {
 	if (location) {
 		await updateUserInfo(token, { location: location });
 		await updateUserInfo(token, { location: location });
 		return location;
 		return location;
 	} else {
 	} else {
-		throw new Error('Failed to get user location');
+		console.log('Failed to get user location');
+		return null;
 	}
 	}
 };
 };
 
 

+ 46 - 8
src/lib/apis/utils/index.ts

@@ -1,12 +1,13 @@
 import { WEBUI_API_BASE_URL } from '$lib/constants';
 import { WEBUI_API_BASE_URL } from '$lib/constants';
 
 
-export const getGravatarUrl = async (email: string) => {
+export const getGravatarUrl = async (token: string, email: string) => {
 	let error = null;
 	let error = null;
 
 
 	const res = await fetch(`${WEBUI_API_BASE_URL}/utils/gravatar?email=${email}`, {
 	const res = await fetch(`${WEBUI_API_BASE_URL}/utils/gravatar?email=${email}`, {
 		method: 'GET',
 		method: 'GET',
 		headers: {
 		headers: {
-			'Content-Type': 'application/json'
+			'Content-Type': 'application/json',
+			Authorization: `Bearer ${token}`
 		}
 		}
 	})
 	})
 		.then(async (res) => {
 		.then(async (res) => {
@@ -22,13 +23,48 @@ export const getGravatarUrl = async (email: string) => {
 	return res;
 	return res;
 };
 };
 
 
-export const formatPythonCode = async (code: string) => {
+export const executeCode = async (token: string, code: string) => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/utils/code/execute`, {
+		method: 'POST',
+		headers: {
+			'Content-Type': 'application/json',
+			Authorization: `Bearer ${token}`
+		},
+		body: JSON.stringify({
+			code: code
+		})
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.catch((err) => {
+			console.log(err);
+
+			error = err;
+			if (err.detail) {
+				error = err.detail;
+			}
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res;
+};
+
+export const formatPythonCode = async (token: string, code: string) => {
 	let error = null;
 	let error = null;
 
 
 	const res = await fetch(`${WEBUI_API_BASE_URL}/utils/code/format`, {
 	const res = await fetch(`${WEBUI_API_BASE_URL}/utils/code/format`, {
 		method: 'POST',
 		method: 'POST',
 		headers: {
 		headers: {
-			'Content-Type': 'application/json'
+			'Content-Type': 'application/json',
+			Authorization: `Bearer ${token}`
 		},
 		},
 		body: JSON.stringify({
 		body: JSON.stringify({
 			code: code
 			code: code
@@ -55,13 +91,14 @@ export const formatPythonCode = async (code: string) => {
 	return res;
 	return res;
 };
 };
 
 
-export const downloadChatAsPDF = async (title: string, messages: object[]) => {
+export const downloadChatAsPDF = async (token: string, title: string, messages: object[]) => {
 	let error = null;
 	let error = null;
 
 
 	const blob = await fetch(`${WEBUI_API_BASE_URL}/utils/pdf`, {
 	const blob = await fetch(`${WEBUI_API_BASE_URL}/utils/pdf`, {
 		method: 'POST',
 		method: 'POST',
 		headers: {
 		headers: {
-			'Content-Type': 'application/json'
+			'Content-Type': 'application/json',
+			Authorization: `Bearer ${token}`
 		},
 		},
 		body: JSON.stringify({
 		body: JSON.stringify({
 			title: title,
 			title: title,
@@ -81,13 +118,14 @@ export const downloadChatAsPDF = async (title: string, messages: object[]) => {
 	return blob;
 	return blob;
 };
 };
 
 
-export const getHTMLFromMarkdown = async (md: string) => {
+export const getHTMLFromMarkdown = async (token: string, md: string) => {
 	let error = null;
 	let error = null;
 
 
 	const res = await fetch(`${WEBUI_API_BASE_URL}/utils/markdown`, {
 	const res = await fetch(`${WEBUI_API_BASE_URL}/utils/markdown`, {
 		method: 'POST',
 		method: 'POST',
 		headers: {
 		headers: {
-			'Content-Type': 'application/json'
+			'Content-Type': 'application/json',
+			Authorization: `Bearer ${token}`
 		},
 		},
 		body: JSON.stringify({
 		body: JSON.stringify({
 			md: md
 			md: md

+ 6 - 6
src/lib/components/AddConnectionModal.svelte

@@ -169,7 +169,7 @@
 
 
 								<div class="flex-1">
 								<div class="flex-1">
 									<input
 									<input
-										class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none"
+										class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
 										type="text"
 										type="text"
 										bind:value={url}
 										bind:value={url}
 										placeholder={$i18n.t('API Base URL')}
 										placeholder={$i18n.t('API Base URL')}
@@ -202,7 +202,7 @@
 								</button>
 								</button>
 							</Tooltip>
 							</Tooltip>
 
 
-							<div class="flex flex-col flex-shrink-0 self-end">
+							<div class="flex flex-col shrink-0 self-end">
 								<Tooltip content={enable ? $i18n.t('Enabled') : $i18n.t('Disabled')}>
 								<Tooltip content={enable ? $i18n.t('Enabled') : $i18n.t('Disabled')}>
 									<Switch bind:state={enable} />
 									<Switch bind:state={enable} />
 								</Tooltip>
 								</Tooltip>
@@ -215,7 +215,7 @@
 
 
 								<div class="flex-1">
 								<div class="flex-1">
 									<SensitiveInput
 									<SensitiveInput
-										className="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none"
+										className="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
 										bind:value={key}
 										bind:value={key}
 										placeholder={$i18n.t('API Key')}
 										placeholder={$i18n.t('API Key')}
 										required={!ollama}
 										required={!ollama}
@@ -233,7 +233,7 @@
 										)}
 										)}
 									>
 									>
 										<input
 										<input
-											class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none"
+											class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
 											type="text"
 											type="text"
 											bind:value={prefixId}
 											bind:value={prefixId}
 											placeholder={$i18n.t('Prefix ID')}
 											placeholder={$i18n.t('Prefix ID')}
@@ -258,7 +258,7 @@
 											<div class=" text-sm flex-1 py-1 rounded-lg">
 											<div class=" text-sm flex-1 py-1 rounded-lg">
 												{modelId}
 												{modelId}
 											</div>
 											</div>
-											<div class="flex-shrink-0">
+											<div class="shrink-0">
 												<button
 												<button
 													type="button"
 													type="button"
 													on:click={() => {
 													on:click={() => {
@@ -292,7 +292,7 @@
 							<input
 							<input
 								class="w-full py-1 text-sm rounded-lg bg-transparent {modelId
 								class="w-full py-1 text-sm rounded-lg bg-transparent {modelId
 									? ''
 									? ''
-									: 'text-gray-500'} placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none"
+									: 'text-gray-500'} placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
 								bind:value={modelId}
 								bind:value={modelId}
 								placeholder={$i18n.t('Add a model ID')}
 								placeholder={$i18n.t('Add a model ID')}
 							/>
 							/>

+ 1 - 1
src/lib/components/ChangelogModal.svelte

@@ -68,7 +68,7 @@
 								v{version} - {changelog[version].date}
 								v{version} - {changelog[version].date}
 							</div>
 							</div>
 
 
-							<hr class=" dark:border-gray-800 my-2" />
+							<hr class="border-gray-100 dark:border-gray-850 my-2" />
 
 
 							{#each Object.keys(changelog[version]).filter((section) => section !== 'date') as section}
 							{#each Object.keys(changelog[version]).filter((section) => section !== 'date') as section}
 								<div class="">
 								<div class="">

+ 2 - 2
src/lib/components/NotificationToast.svelte

@@ -31,13 +31,13 @@
 </script>
 </script>
 
 
 <button
 <button
-	class="flex gap-2.5 text-left min-w-[var(--width)] w-full dark:bg-gray-850 dark:text-white bg-white text-black border border-gray-50 dark:border-gray-800 rounded-xl px-3.5 py-3.5"
+	class="flex gap-2.5 text-left min-w-[var(--width)] w-full dark:bg-gray-850 dark:text-white bg-white text-black border border-gray-100 dark:border-gray-850 rounded-xl px-3.5 py-3.5"
 	on:click={() => {
 	on:click={() => {
 		onClick();
 		onClick();
 		dispatch('closeToast');
 		dispatch('closeToast');
 	}}
 	}}
 >
 >
-	<div class="flex-shrink-0 self-top -translate-y-0.5">
+	<div class="shrink-0 self-top -translate-y-0.5">
 		<img src={'/static/favicon.png'} alt="favicon" class="size-7 rounded-full" />
 		<img src={'/static/favicon.png'} alt="favicon" class="size-7 rounded-full" />
 	</div>
 	</div>
 
 

+ 2 - 2
src/lib/components/OnBoarding.svelte

@@ -30,10 +30,10 @@
 		<SlideShow duration={5000} />
 		<SlideShow duration={5000} />
 
 
 		<div
 		<div
-			class="w-full h-full absolute top-0 left-0 bg-gradient-to-t from-20% from-black to-transparent"
+			class="w-full h-full absolute top-0 left-0 bg-linear-to-t from-20% from-black to-transparent"
 		></div>
 		></div>
 
 
-		<div class="w-full h-full absolute top-0 left-0 backdrop-blur-sm bg-black/50"></div>
+		<div class="w-full h-full absolute top-0 left-0 backdrop-blur-xs bg-black/50"></div>
 
 
 		<div class="relative bg-transparent w-full min-h-screen flex z-10">
 		<div class="relative bg-transparent w-full min-h-screen flex z-10">
 			<div class="flex flex-col justify-end w-full items-center pb-10 text-center">
 			<div class="flex flex-col justify-end w-full items-center pb-10 text-center">

+ 5 - 3
src/lib/components/admin/Evaluations/Feedbacks.svelte

@@ -131,14 +131,16 @@
 	</div>
 	</div>
 </div>
 </div>
 
 
-<div class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full rounded pt-0.5">
+<div
+	class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full rounded-sm pt-0.5"
+>
 	{#if (feedbacks ?? []).length === 0}
 	{#if (feedbacks ?? []).length === 0}
 		<div class="text-center text-xs text-gray-500 dark:text-gray-400 py-1">
 		<div class="text-center text-xs text-gray-500 dark:text-gray-400 py-1">
 			{$i18n.t('No feedbacks found')}
 			{$i18n.t('No feedbacks found')}
 		</div>
 		</div>
 	{:else}
 	{:else}
 		<table
 		<table
-			class="w-full text-sm text-left text-gray-500 dark:text-gray-400 table-auto max-w-full rounded"
+			class="w-full text-sm text-left text-gray-500 dark:text-gray-400 table-auto max-w-full rounded-sm"
 		>
 		>
 			<thead
 			<thead
 				class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-850 dark:text-gray-400 -translate-y-0.5"
 				class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-850 dark:text-gray-400 -translate-y-0.5"
@@ -169,7 +171,7 @@
 						<td class=" py-0.5 text-right font-semibold">
 						<td class=" py-0.5 text-right font-semibold">
 							<div class="flex justify-center">
 							<div class="flex justify-center">
 								<Tooltip content={feedback?.user?.name}>
 								<Tooltip content={feedback?.user?.name}>
-									<div class="flex-shrink-0">
+									<div class="shrink-0">
 										<img
 										<img
 											src={feedback?.user?.profile_image_url ?? '/user.png'}
 											src={feedback?.user?.profile_image_url ?? '/user.png'}
 											alt={feedback?.user?.name}
 											alt={feedback?.user?.name}

+ 5 - 3
src/lib/components/admin/Evaluations/Leaderboard.svelte

@@ -288,7 +288,7 @@
 					<MagnifyingGlass className="size-3" />
 					<MagnifyingGlass className="size-3" />
 				</div>
 				</div>
 				<input
 				<input
-					class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-none bg-transparent"
+					class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-hidden bg-transparent"
 					bind:value={query}
 					bind:value={query}
 					placeholder={$i18n.t('Search')}
 					placeholder={$i18n.t('Search')}
 					on:focus={() => {
 					on:focus={() => {
@@ -300,7 +300,9 @@
 	</div>
 	</div>
 </div>
 </div>
 
 
-<div class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full rounded pt-0.5">
+<div
+	class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full rounded-sm pt-0.5"
+>
 	{#if loadingLeaderboard}
 	{#if loadingLeaderboard}
 		<div class=" absolute top-0 bottom-0 left-0 right-0 flex">
 		<div class=" absolute top-0 bottom-0 left-0 right-0 flex">
 			<div class="m-auto">
 			<div class="m-auto">
@@ -349,7 +351,7 @@
 						</td>
 						</td>
 						<td class="px-3 py-1.5 flex flex-col justify-center">
 						<td class="px-3 py-1.5 flex flex-col justify-center">
 							<div class="flex items-center gap-2">
 							<div class="flex items-center gap-2">
-								<div class="flex-shrink-0">
+								<div class="shrink-0">
 									<img
 									<img
 										src={model?.info?.meta?.profile_image_url ?? '/favicon.png'}
 										src={model?.info?.meta?.profile_image_url ?? '/favicon.png'}
 										alt={model.name}
 										alt={model.name}

+ 6 - 6
src/lib/components/admin/Functions.svelte

@@ -180,12 +180,12 @@
 
 
 		window.addEventListener('keydown', onKeyDown);
 		window.addEventListener('keydown', onKeyDown);
 		window.addEventListener('keyup', onKeyUp);
 		window.addEventListener('keyup', onKeyUp);
-		window.addEventListener('blur', onBlur);
+		window.addEventListener('blur-sm', onBlur);
 
 
 		return () => {
 		return () => {
 			window.removeEventListener('keydown', onKeyDown);
 			window.removeEventListener('keydown', onKeyDown);
 			window.removeEventListener('keyup', onKeyUp);
 			window.removeEventListener('keyup', onKeyUp);
-			window.removeEventListener('blur', onBlur);
+			window.removeEventListener('blur-sm', onBlur);
 		};
 		};
 	});
 	});
 </script>
 </script>
@@ -211,7 +211,7 @@
 				<Search className="size-3.5" />
 				<Search className="size-3.5" />
 			</div>
 			</div>
 			<input
 			<input
-				class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-none bg-transparent"
+				class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-hidden bg-transparent"
 				bind:value={query}
 				bind:value={query}
 				placeholder={$i18n.t('Search Functions')}
 				placeholder={$i18n.t('Search Functions')}
 			/>
 			/>
@@ -241,14 +241,14 @@
 					<div class=" flex-1 self-center pl-1">
 					<div class=" flex-1 self-center pl-1">
 						<div class=" font-semibold flex items-center gap-1.5">
 						<div class=" font-semibold flex items-center gap-1.5">
 							<div
 							<div
-								class=" text-xs font-bold px-1 rounded uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
+								class=" text-xs font-bold px-1 rounded-sm uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
 							>
 							>
 								{func.type}
 								{func.type}
 							</div>
 							</div>
 
 
 							{#if func?.meta?.manifest?.version}
 							{#if func?.meta?.manifest?.version}
 								<div
 								<div
-									class="text-xs font-bold px-1 rounded line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
+									class="text-xs font-bold px-1 rounded-sm line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
 								>
 								>
 									v{func?.meta?.manifest?.version ?? ''}
 									v{func?.meta?.manifest?.version ?? ''}
 								</div>
 								</div>
@@ -260,7 +260,7 @@
 						</div>
 						</div>
 
 
 						<div class="flex gap-1.5 px-1">
 						<div class="flex gap-1.5 px-1">
-							<div class=" text-gray-500 text-xs font-medium flex-shrink-0">{func.id}</div>
+							<div class=" text-gray-500 text-xs font-medium shrink-0">{func.id}</div>
 
 
 							<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
 							<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
 								{func.meta.description}
 								{func.meta.description}

+ 5 - 5
src/lib/components/admin/Functions/FunctionEditor.svelte

@@ -300,7 +300,7 @@ class Pipe:
 			<div class="flex flex-col flex-1 overflow-auto h-0 rounded-lg">
 			<div class="flex flex-col flex-1 overflow-auto h-0 rounded-lg">
 				<div class="w-full mb-2 flex flex-col gap-0.5">
 				<div class="w-full mb-2 flex flex-col gap-0.5">
 					<div class="flex w-full items-center">
 					<div class="flex w-full items-center">
-						<div class=" flex-shrink-0 mr-2">
+						<div class=" shrink-0 mr-2">
 							<Tooltip content={$i18n.t('Back')}>
 							<Tooltip content={$i18n.t('Back')}>
 								<button
 								<button
 									class="w-full text-left text-sm py-1.5 px-1 rounded-lg dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-gray-850"
 									class="w-full text-left text-sm py-1.5 px-1 rounded-lg dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-gray-850"
@@ -317,7 +317,7 @@ class Pipe:
 						<div class="flex-1">
 						<div class="flex-1">
 							<Tooltip content={$i18n.t('e.g. My Filter')} placement="top-start">
 							<Tooltip content={$i18n.t('e.g. My Filter')} placement="top-start">
 								<input
 								<input
-									class="w-full text-2xl font-medium bg-transparent outline-none font-primary"
+									class="w-full text-2xl font-medium bg-transparent outline-hidden font-primary"
 									type="text"
 									type="text"
 									placeholder={$i18n.t('Function Name')}
 									placeholder={$i18n.t('Function Name')}
 									bind:value={name}
 									bind:value={name}
@@ -333,13 +333,13 @@ class Pipe:
 
 
 					<div class=" flex gap-2 px-1 items-center">
 					<div class=" flex gap-2 px-1 items-center">
 						{#if edit}
 						{#if edit}
-							<div class="text-sm text-gray-500 flex-shrink-0">
+							<div class="text-sm text-gray-500 shrink-0">
 								{id}
 								{id}
 							</div>
 							</div>
 						{:else}
 						{:else}
 							<Tooltip className="w-full" content={$i18n.t('e.g. my_filter')} placement="top-start">
 							<Tooltip className="w-full" content={$i18n.t('e.g. my_filter')} placement="top-start">
 								<input
 								<input
-									class="w-full text-sm disabled:text-gray-500 bg-transparent outline-none"
+									class="w-full text-sm disabled:text-gray-500 bg-transparent outline-hidden"
 									type="text"
 									type="text"
 									placeholder={$i18n.t('Function ID')}
 									placeholder={$i18n.t('Function ID')}
 									bind:value={id}
 									bind:value={id}
@@ -355,7 +355,7 @@ class Pipe:
 							placement="top-start"
 							placement="top-start"
 						>
 						>
 							<input
 							<input
-								class="w-full text-sm bg-transparent outline-none"
+								class="w-full text-sm bg-transparent outline-hidden"
 								type="text"
 								type="text"
 								placeholder={$i18n.t('Function Description')}
 								placeholder={$i18n.t('Function Description')}
 								bind:value={meta.description}
 								bind:value={meta.description}

+ 3 - 3
src/lib/components/admin/Functions/FunctionMenu.svelte

@@ -42,7 +42,7 @@
 
 
 	<div slot="content">
 	<div slot="content">
 		<DropdownMenu.Content
 		<DropdownMenu.Content
-			class="w-full max-w-[180px] rounded-xl px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow"
+			class="w-full max-w-[180px] rounded-xl px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-sm"
 			sideOffset={-2}
 			sideOffset={-2}
 			side="bottom"
 			side="bottom"
 			align="start"
 			align="start"
@@ -63,7 +63,7 @@
 					</div>
 					</div>
 				</div>
 				</div>
 
 
-				<hr class="border-gray-100 dark:border-gray-800 my-1" />
+				<hr class="border-gray-100 dark:border-gray-850 my-1" />
 			{/if}
 			{/if}
 
 
 			<DropdownMenu.Item
 			<DropdownMenu.Item
@@ -122,7 +122,7 @@
 				<div class="flex items-center">{$i18n.t('Export')}</div>
 				<div class="flex items-center">{$i18n.t('Export')}</div>
 			</DropdownMenu.Item>
 			</DropdownMenu.Item>
 
 
-			<hr class="border-gray-100 dark:border-gray-800 my-1" />
+			<hr class="border-gray-100 dark:border-gray-850 my-1" />
 
 
 			<DropdownMenu.Item
 			<DropdownMenu.Item
 				class="flex  gap-2  items-center px-3 py-2 text-sm  font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
 				class="flex  gap-2  items-center px-3 py-2 text-sm  font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"

+ 6 - 6
src/lib/components/admin/Settings.svelte

@@ -19,7 +19,7 @@
 	import ChartBar from '../icons/ChartBar.svelte';
 	import ChartBar from '../icons/ChartBar.svelte';
 	import DocumentChartBar from '../icons/DocumentChartBar.svelte';
 	import DocumentChartBar from '../icons/DocumentChartBar.svelte';
 	import Evaluations from './Settings/Evaluations.svelte';
 	import Evaluations from './Settings/Evaluations.svelte';
-	import CodeInterpreter from './Settings/CodeInterpreter.svelte';
+	import CodeExecution from './Settings/CodeExecution.svelte';
 
 
 	const i18n = getContext('i18n');
 	const i18n = getContext('i18n');
 
 
@@ -191,11 +191,11 @@
 
 
 		<button
 		<button
 			class="px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
 			class="px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
-			'code-interpreter'
+			'code-execution'
 				? ''
 				? ''
 				: ' text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}"
 				: ' text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}"
 			on:click={() => {
 			on:click={() => {
-				selectedTab = 'code-interpreter';
+				selectedTab = 'code-execution';
 			}}
 			}}
 		>
 		>
 			<div class=" self-center mr-2">
 			<div class=" self-center mr-2">
@@ -212,7 +212,7 @@
 					/>
 					/>
 				</svg>
 				</svg>
 			</div>
 			</div>
-			<div class=" self-center">{$i18n.t('Code Interpreter')}</div>
+			<div class=" self-center">{$i18n.t('Code Execution')}</div>
 		</button>
 		</button>
 
 
 		<button
 		<button
@@ -391,8 +391,8 @@
 					await config.set(await getBackendConfig());
 					await config.set(await getBackendConfig());
 				}}
 				}}
 			/>
 			/>
-		{:else if selectedTab === 'code-interpreter'}
-			<CodeInterpreter
+		{:else if selectedTab === 'code-execution'}
+			<CodeExecution
 				saveHandler={async () => {
 				saveHandler={async () => {
 					toast.success($i18n.t('Settings saved successfully!'));
 					toast.success($i18n.t('Settings saved successfully!'));
 
 

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

@@ -172,7 +172,7 @@
 					<div class=" self-center text-xs font-medium">{$i18n.t('Speech-to-Text Engine')}</div>
 					<div class=" self-center text-xs font-medium">{$i18n.t('Speech-to-Text Engine')}</div>
 					<div class="flex items-center relative">
 					<div class="flex items-center relative">
 						<select
 						<select
-							class="dark:bg-gray-900 cursor-pointer w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
+							class="dark:bg-gray-900 cursor-pointer w-fit pr-8 rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right"
 							bind:value={STT_ENGINE}
 							bind:value={STT_ENGINE}
 							placeholder="Select an engine"
 							placeholder="Select an engine"
 						>
 						>
@@ -188,7 +188,7 @@
 					<div>
 					<div>
 						<div class="mt-1 flex gap-2 mb-1">
 						<div class="mt-1 flex gap-2 mb-1">
 							<input
 							<input
-								class="flex-1 w-full bg-transparent outline-none"
+								class="flex-1 w-full bg-transparent outline-hidden"
 								placeholder={$i18n.t('API Base URL')}
 								placeholder={$i18n.t('API Base URL')}
 								bind:value={STT_OPENAI_API_BASE_URL}
 								bind:value={STT_OPENAI_API_BASE_URL}
 								required
 								required
@@ -198,7 +198,7 @@
 						</div>
 						</div>
 					</div>
 					</div>
 
 
-					<hr class=" dark:border-gray-850 my-2" />
+					<hr class="border-gray-100 dark:border-gray-850 my-2" />
 
 
 					<div>
 					<div>
 						<div class=" mb-1.5 text-sm font-medium">{$i18n.t('STT Model')}</div>
 						<div class=" mb-1.5 text-sm font-medium">{$i18n.t('STT Model')}</div>
@@ -206,7 +206,7 @@
 							<div class="flex-1">
 							<div class="flex-1">
 								<input
 								<input
 									list="model-list"
 									list="model-list"
-									class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+									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_MODEL}
 									bind:value={STT_MODEL}
 									placeholder="Select a model"
 									placeholder="Select a model"
 								/>
 								/>
@@ -224,14 +224,14 @@
 						</div>
 						</div>
 					</div>
 					</div>
 
 
-					<hr class=" dark:border-gray-850 my-2" />
+					<hr class="border-gray-100 dark:border-gray-850 my-2" />
 
 
 					<div>
 					<div>
 						<div class=" mb-1.5 text-sm font-medium">{$i18n.t('STT Model')}</div>
 						<div class=" mb-1.5 text-sm font-medium">{$i18n.t('STT Model')}</div>
 						<div class="flex w-full">
 						<div class="flex w-full">
 							<div class="flex-1">
 							<div class="flex-1">
 								<input
 								<input
-									class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+									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_MODEL}
 									bind:value={STT_MODEL}
 									placeholder="Select a model (optional)"
 									placeholder="Select a model (optional)"
 								/>
 								/>
@@ -255,7 +255,7 @@
 						<div class="flex w-full">
 						<div class="flex w-full">
 							<div class="flex-1 mr-2">
 							<div class="flex-1 mr-2">
 								<input
 								<input
-									class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+									class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
 									placeholder={$i18n.t('Set whisper model')}
 									placeholder={$i18n.t('Set whisper model')}
 									bind:value={STT_WHISPER_MODEL}
 									bind:value={STT_WHISPER_MODEL}
 								/>
 								/>
@@ -333,7 +333,7 @@
 				{/if}
 				{/if}
 			</div>
 			</div>
 
 
-			<hr class=" dark:border-gray-800" />
+			<hr class="border-gray-100 dark:border-gray-850" />
 
 
 			<div>
 			<div>
 				<div class=" mb-1 text-sm font-medium">{$i18n.t('TTS Settings')}</div>
 				<div class=" mb-1 text-sm font-medium">{$i18n.t('TTS Settings')}</div>
@@ -342,7 +342,7 @@
 					<div class=" self-center text-xs font-medium">{$i18n.t('Text-to-Speech Engine')}</div>
 					<div class=" self-center text-xs font-medium">{$i18n.t('Text-to-Speech Engine')}</div>
 					<div class="flex items-center relative">
 					<div class="flex items-center relative">
 						<select
 						<select
-							class=" dark:bg-gray-900 w-fit pr-8 cursor-pointer rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
+							class=" dark:bg-gray-900 w-fit pr-8 cursor-pointer rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right"
 							bind:value={TTS_ENGINE}
 							bind:value={TTS_ENGINE}
 							placeholder="Select a mode"
 							placeholder="Select a mode"
 							on:change={async (e) => {
 							on:change={async (e) => {
@@ -372,7 +372,7 @@
 					<div>
 					<div>
 						<div class="mt-1 flex gap-2 mb-1">
 						<div class="mt-1 flex gap-2 mb-1">
 							<input
 							<input
-								class="flex-1 w-full bg-transparent outline-none"
+								class="flex-1 w-full bg-transparent outline-hidden"
 								placeholder={$i18n.t('API Base URL')}
 								placeholder={$i18n.t('API Base URL')}
 								bind:value={TTS_OPENAI_API_BASE_URL}
 								bind:value={TTS_OPENAI_API_BASE_URL}
 								required
 								required
@@ -385,7 +385,7 @@
 					<div>
 					<div>
 						<div class="mt-1 flex gap-2 mb-1">
 						<div class="mt-1 flex gap-2 mb-1">
 							<input
 							<input
-								class="flex-1 w-full rounded-lg py-2 pl-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+								class="flex-1 w-full rounded-lg py-2 pl-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
 								placeholder={$i18n.t('API Key')}
 								placeholder={$i18n.t('API Key')}
 								bind:value={TTS_API_KEY}
 								bind:value={TTS_API_KEY}
 								required
 								required
@@ -396,13 +396,13 @@
 					<div>
 					<div>
 						<div class="mt-1 flex gap-2 mb-1">
 						<div class="mt-1 flex gap-2 mb-1">
 							<input
 							<input
-								class="flex-1 w-full rounded-lg py-2 pl-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+								class="flex-1 w-full rounded-lg py-2 pl-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
 								placeholder={$i18n.t('API Key')}
 								placeholder={$i18n.t('API Key')}
 								bind:value={TTS_API_KEY}
 								bind:value={TTS_API_KEY}
 								required
 								required
 							/>
 							/>
 							<input
 							<input
-								class="flex-1 w-full rounded-lg py-2 pl-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+								class="flex-1 w-full rounded-lg py-2 pl-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
 								placeholder={$i18n.t('Azure Region')}
 								placeholder={$i18n.t('Azure Region')}
 								bind:value={TTS_AZURE_SPEECH_REGION}
 								bind:value={TTS_AZURE_SPEECH_REGION}
 								required
 								required
@@ -411,7 +411,7 @@
 					</div>
 					</div>
 				{/if}
 				{/if}
 
 
-				<hr class=" dark:border-gray-850 my-2" />
+				<hr class="border-gray-100 dark:border-gray-850 my-2" />
 
 
 				{#if TTS_ENGINE === ''}
 				{#if TTS_ENGINE === ''}
 					<div>
 					<div>
@@ -419,7 +419,7 @@
 						<div class="flex w-full">
 						<div class="flex w-full">
 							<div class="flex-1">
 							<div class="flex-1">
 								<select
 								<select
-									class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+									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={TTS_VOICE}
 									bind:value={TTS_VOICE}
 								>
 								>
 									<option value="" selected={TTS_VOICE !== ''}>{$i18n.t('Default')}</option>
 									<option value="" selected={TTS_VOICE !== ''}>{$i18n.t('Default')}</option>
@@ -442,7 +442,7 @@
 							<div class="flex-1">
 							<div class="flex-1">
 								<input
 								<input
 									list="model-list"
 									list="model-list"
-									class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+									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={TTS_MODEL}
 									bind:value={TTS_MODEL}
 									placeholder="CMU ARCTIC speaker embedding name"
 									placeholder="CMU ARCTIC speaker embedding name"
 								/>
 								/>
@@ -484,7 +484,7 @@
 								<div class="flex-1">
 								<div class="flex-1">
 									<input
 									<input
 										list="voice-list"
 										list="voice-list"
-										class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+										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={TTS_VOICE}
 										bind:value={TTS_VOICE}
 										placeholder="Select a voice"
 										placeholder="Select a voice"
 									/>
 									/>
@@ -503,7 +503,7 @@
 								<div class="flex-1">
 								<div class="flex-1">
 									<input
 									<input
 										list="tts-model-list"
 										list="tts-model-list"
-										class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+										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={TTS_MODEL}
 										bind:value={TTS_MODEL}
 										placeholder="Select a model"
 										placeholder="Select a model"
 									/>
 									/>
@@ -525,7 +525,7 @@
 								<div class="flex-1">
 								<div class="flex-1">
 									<input
 									<input
 										list="voice-list"
 										list="voice-list"
-										class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+										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={TTS_VOICE}
 										bind:value={TTS_VOICE}
 										placeholder="Select a voice"
 										placeholder="Select a voice"
 									/>
 									/>
@@ -544,7 +544,7 @@
 								<div class="flex-1">
 								<div class="flex-1">
 									<input
 									<input
 										list="tts-model-list"
 										list="tts-model-list"
-										class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+										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={TTS_MODEL}
 										bind:value={TTS_MODEL}
 										placeholder="Select a model"
 										placeholder="Select a model"
 									/>
 									/>
@@ -566,7 +566,7 @@
 								<div class="flex-1">
 								<div class="flex-1">
 									<input
 									<input
 										list="voice-list"
 										list="voice-list"
-										class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+										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={TTS_VOICE}
 										bind:value={TTS_VOICE}
 										placeholder="Select a voice"
 										placeholder="Select a voice"
 									/>
 									/>
@@ -593,7 +593,7 @@
 								<div class="flex-1">
 								<div class="flex-1">
 									<input
 									<input
 										list="tts-model-list"
 										list="tts-model-list"
-										class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+										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={TTS_AZURE_SPEECH_OUTPUT_FORMAT}
 										bind:value={TTS_AZURE_SPEECH_OUTPUT_FORMAT}
 										placeholder="Select a output format"
 										placeholder="Select a output format"
 									/>
 									/>
@@ -603,13 +603,13 @@
 					</div>
 					</div>
 				{/if}
 				{/if}
 
 
-				<hr class="dark:border-gray-850 my-2" />
+				<hr class="border-gray-100 dark:border-gray-850 my-2" />
 
 
 				<div class="pt-0.5 flex w-full justify-between">
 				<div class="pt-0.5 flex w-full justify-between">
 					<div class="self-center text-xs font-medium">{$i18n.t('Response splitting')}</div>
 					<div class="self-center text-xs font-medium">{$i18n.t('Response splitting')}</div>
 					<div class="flex items-center relative">
 					<div class="flex items-center relative">
 						<select
 						<select
-							class="dark:bg-gray-900 w-fit pr-8 cursor-pointer rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
+							class="dark:bg-gray-900 w-fit pr-8 cursor-pointer rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right"
 							aria-label="Select how to split message text for TTS requests"
 							aria-label="Select how to split message text for TTS requests"
 							bind:value={TTS_SPLIT_ON}
 							bind:value={TTS_SPLIT_ON}
 						>
 						>

+ 317 - 0
src/lib/components/admin/Settings/CodeExecution.svelte

@@ -0,0 +1,317 @@
+<script lang="ts">
+	import { toast } from 'svelte-sonner';
+	import { onMount, getContext } from 'svelte';
+	import { getCodeExecutionConfig, setCodeExecutionConfig } from '$lib/apis/configs';
+
+	import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
+
+	import Tooltip from '$lib/components/common/Tooltip.svelte';
+	import Textarea from '$lib/components/common/Textarea.svelte';
+	import Switch from '$lib/components/common/Switch.svelte';
+
+	const i18n = getContext('i18n');
+
+	export let saveHandler: Function;
+
+	let config = null;
+
+	let engines = ['pyodide', 'jupyter'];
+
+	const submitHandler = async () => {
+		const res = await setCodeExecutionConfig(localStorage.token, config);
+	};
+
+	onMount(async () => {
+		const res = await getCodeExecutionConfig(localStorage.token);
+
+		if (res) {
+			config = res;
+		}
+	});
+</script>
+
+<form
+	class="flex flex-col h-full justify-between space-y-3 text-sm"
+	on:submit|preventDefault={async () => {
+		await submitHandler();
+		saveHandler();
+	}}
+>
+	<div class=" space-y-3 overflow-y-scroll scrollbar-hidden h-full">
+		{#if config}
+			<div>
+				<div class="mb-3.5">
+					<div class=" mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
+
+					<hr class=" border-gray-100 dark:border-gray-850 my-2" />
+
+					<div class="mb-2.5">
+						<div class="flex w-full justify-between">
+							<div class=" self-center text-xs font-medium">{$i18n.t('Code Execution Engine')}</div>
+							<div class="flex items-center relative">
+								<select
+									class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right"
+									bind:value={config.CODE_EXECUTION_ENGINE}
+									placeholder={$i18n.t('Select a engine')}
+									required
+								>
+									<option disabled selected value="">{$i18n.t('Select a engine')}</option>
+									{#each engines as engine}
+										<option value={engine}>{engine}</option>
+									{/each}
+								</select>
+							</div>
+						</div>
+
+						{#if config.CODE_EXECUTION_ENGINE === 'jupyter'}
+							<div class="text-gray-500 text-xs">
+								{$i18n.t(
+									'Warning: Jupyter execution enables arbitrary code execution, posing severe security risks—proceed with extreme caution.'
+								)}
+							</div>
+						{/if}
+					</div>
+
+					{#if config.CODE_EXECUTION_ENGINE === 'jupyter'}
+						<div class="mb-2.5 flex flex-col gap-1.5 w-full">
+							<div class="text-xs font-medium">
+								{$i18n.t('Jupyter URL')}
+							</div>
+
+							<div class="flex w-full">
+								<div class="flex-1">
+									<input
+										class="w-full text-sm py-0.5 placeholder:text-gray-300 dark:placeholder:text-gray-700 bg-transparent outline-hidden"
+										type="text"
+										placeholder={$i18n.t('Enter Jupyter URL')}
+										bind:value={config.CODE_EXECUTION_JUPYTER_URL}
+										autocomplete="off"
+									/>
+								</div>
+							</div>
+						</div>
+
+						<div class="mb-2.5 flex flex-col gap-1.5 w-full">
+							<div class=" flex gap-2 w-full items-center justify-between">
+								<div class="text-xs font-medium">
+									{$i18n.t('Jupyter Auth')}
+								</div>
+
+								<div>
+									<select
+										class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-left"
+										bind:value={config.CODE_EXECUTION_JUPYTER_AUTH}
+										placeholder={$i18n.t('Select an auth method')}
+									>
+										<option selected value="">{$i18n.t('None')}</option>
+										<option value="token">{$i18n.t('Token')}</option>
+										<option value="password">{$i18n.t('Password')}</option>
+									</select>
+								</div>
+							</div>
+
+							{#if config.CODE_EXECUTION_JUPYTER_AUTH}
+								<div class="flex w-full gap-2">
+									<div class="flex-1">
+										{#if config.CODE_EXECUTION_JUPYTER_AUTH === 'password'}
+											<SensitiveInput
+												type="text"
+												placeholder={$i18n.t('Enter Jupyter Password')}
+												bind:value={config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD}
+												autocomplete="off"
+											/>
+										{:else}
+											<SensitiveInput
+												type="text"
+												placeholder={$i18n.t('Enter Jupyter Token')}
+												bind:value={config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN}
+												autocomplete="off"
+											/>
+										{/if}
+									</div>
+								</div>
+							{/if}
+						</div>
+
+						<div class="flex gap-2 w-full items-center justify-between">
+							<div class="text-xs font-medium">
+								{$i18n.t('Code Execution Timeout')}
+							</div>
+
+							<div class="">
+								<Tooltip content={$i18n.t('Enter timeout in seconds')}>
+									<input
+										class="dark:bg-gray-900 w-fit rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right"
+										type="number"
+										bind:value={config.CODE_EXECUTION_JUPYTER_TIMEOUT}
+										placeholder={$i18n.t('e.g. 60')}
+										autocomplete="off"
+									/>
+								</Tooltip>
+							</div>
+						</div>
+					{/if}
+				</div>
+
+				<div class="mb-3.5">
+					<div class=" mb-2.5 text-base font-medium">{$i18n.t('Code Interpreter')}</div>
+
+					<hr class=" border-gray-100 dark:border-gray-850 my-2" />
+
+					<div class="mb-2.5">
+						<div class=" flex w-full justify-between">
+							<div class=" self-center text-xs font-medium">
+								{$i18n.t('Enable Code Interpreter')}
+							</div>
+
+							<Switch bind:state={config.ENABLE_CODE_INTERPRETER} />
+						</div>
+					</div>
+
+					{#if config.ENABLE_CODE_INTERPRETER}
+						<div class="mb-2.5">
+							<div class="  flex w-full justify-between">
+								<div class=" self-center text-xs font-medium">
+									{$i18n.t('Code Interpreter Engine')}
+								</div>
+								<div class="flex items-center relative">
+									<select
+										class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right"
+										bind:value={config.CODE_INTERPRETER_ENGINE}
+										placeholder={$i18n.t('Select a engine')}
+										required
+									>
+										<option disabled selected value="">{$i18n.t('Select a engine')}</option>
+										{#each engines as engine}
+											<option value={engine}>{engine}</option>
+										{/each}
+									</select>
+								</div>
+							</div>
+
+							{#if config.CODE_INTERPRETER_ENGINE === 'jupyter'}
+								<div class="text-gray-500 text-xs">
+									{$i18n.t(
+										'Warning: Jupyter execution enables arbitrary code execution, posing severe security risks—proceed with extreme caution.'
+									)}
+								</div>
+							{/if}
+						</div>
+
+						{#if config.CODE_INTERPRETER_ENGINE === 'jupyter'}
+							<div class="mb-2.5 flex flex-col gap-1.5 w-full">
+								<div class="text-xs font-medium">
+									{$i18n.t('Jupyter URL')}
+								</div>
+
+								<div class="flex w-full">
+									<div class="flex-1">
+										<input
+											class="w-full text-sm py-0.5 placeholder:text-gray-300 dark:placeholder:text-gray-700 bg-transparent outline-hidden"
+											type="text"
+											placeholder={$i18n.t('Enter Jupyter URL')}
+											bind:value={config.CODE_INTERPRETER_JUPYTER_URL}
+											autocomplete="off"
+										/>
+									</div>
+								</div>
+							</div>
+
+							<div class="mb-2.5 flex flex-col gap-1.5 w-full">
+								<div class="flex gap-2 w-full items-center justify-between">
+									<div class="text-xs font-medium">
+										{$i18n.t('Jupyter Auth')}
+									</div>
+
+									<div>
+										<select
+											class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-left"
+											bind:value={config.CODE_INTERPRETER_JUPYTER_AUTH}
+											placeholder={$i18n.t('Select an auth method')}
+										>
+											<option selected value="">{$i18n.t('None')}</option>
+											<option value="token">{$i18n.t('Token')}</option>
+											<option value="password">{$i18n.t('Password')}</option>
+										</select>
+									</div>
+								</div>
+
+								{#if config.CODE_INTERPRETER_JUPYTER_AUTH}
+									<div class="flex w-full gap-2">
+										<div class="flex-1">
+											{#if config.CODE_INTERPRETER_JUPYTER_AUTH === 'password'}
+												<SensitiveInput
+													type="text"
+													placeholder={$i18n.t('Enter Jupyter Password')}
+													bind:value={config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD}
+													autocomplete="off"
+												/>
+											{:else}
+												<SensitiveInput
+													type="text"
+													placeholder={$i18n.t('Enter Jupyter Token')}
+													bind:value={config.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN}
+													autocomplete="off"
+												/>
+											{/if}
+										</div>
+									</div>
+								{/if}
+							</div>
+
+							<div class="flex gap-2 w-full items-center justify-between">
+								<div class="text-xs font-medium">
+									{$i18n.t('Code Execution Timeout')}
+								</div>
+
+								<div class="">
+									<Tooltip content={$i18n.t('Enter timeout in seconds')}>
+										<input
+											class="dark:bg-gray-900 w-fit rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right"
+											type="number"
+											bind:value={config.CODE_INTERPRETER_JUPYTER_TIMEOUT}
+											placeholder={$i18n.t('e.g. 60')}
+											autocomplete="off"
+										/>
+									</Tooltip>
+								</div>
+							</div>
+						{/if}
+
+						<hr class="border-gray-100 dark:border-gray-850 my-2" />
+
+						<div>
+							<div class="py-0.5 w-full">
+								<div class=" mb-2.5 text-xs font-medium">
+									{$i18n.t('Code Interpreter Prompt Template')}
+								</div>
+
+								<Tooltip
+									content={$i18n.t(
+										'Leave empty to use the default prompt, or enter a custom prompt'
+									)}
+									placement="top-start"
+								>
+									<Textarea
+										bind:value={config.CODE_INTERPRETER_PROMPT_TEMPLATE}
+										placeholder={$i18n.t(
+											'Leave empty to use the default prompt, or enter a custom prompt'
+										)}
+									/>
+								</Tooltip>
+							</div>
+						</div>
+					{/if}
+				</div>
+			</div>
+		{/if}
+	</div>
+	<div class="flex justify-end pt-3 text-sm font-medium">
+		<button
+			class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
+			type="submit"
+		>
+			{$i18n.t('Save')}
+		</button>
+	</div>
+</form>

+ 0 - 166
src/lib/components/admin/Settings/CodeInterpreter.svelte

@@ -1,166 +0,0 @@
-<script lang="ts">
-	import { toast } from 'svelte-sonner';
-	import { onMount, getContext } from 'svelte';
-	import { getCodeInterpreterConfig, setCodeInterpreterConfig } from '$lib/apis/configs';
-
-	import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
-
-	import Tooltip from '$lib/components/common/Tooltip.svelte';
-	import Textarea from '$lib/components/common/Textarea.svelte';
-	import Switch from '$lib/components/common/Switch.svelte';
-
-	const i18n = getContext('i18n');
-
-	export let saveHandler: Function;
-
-	let config = null;
-
-	let engines = ['pyodide', 'jupyter'];
-
-	const submitHandler = async () => {
-		const res = await setCodeInterpreterConfig(localStorage.token, config);
-	};
-
-	onMount(async () => {
-		const res = await getCodeInterpreterConfig(localStorage.token);
-
-		if (res) {
-			config = res;
-		}
-	});
-</script>
-
-<form
-	class="flex flex-col h-full justify-between space-y-3 text-sm"
-	on:submit|preventDefault={async () => {
-		await submitHandler();
-		saveHandler();
-	}}
->
-	<div class=" space-y-3 overflow-y-scroll scrollbar-hidden h-full">
-		{#if config}
-			<div>
-				<div class=" mb-1 text-sm font-medium">
-					{$i18n.t('Code Interpreter')}
-				</div>
-
-				<div>
-					<div class=" py-0.5 flex w-full justify-between">
-						<div class=" self-center text-xs font-medium">
-							{$i18n.t('Enable Code Interpreter')}
-						</div>
-
-						<Switch bind:state={config.ENABLE_CODE_INTERPRETER} />
-					</div>
-				</div>
-
-				<div class=" py-0.5 flex w-full justify-between">
-					<div class=" self-center text-xs font-medium">{$i18n.t('Code Interpreter Engine')}</div>
-					<div class="flex items-center relative">
-						<select
-							class="dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
-							bind:value={config.CODE_INTERPRETER_ENGINE}
-							placeholder={$i18n.t('Select a engine')}
-							required
-						>
-							<option disabled selected value="">{$i18n.t('Select a engine')}</option>
-							{#each engines as engine}
-								<option value={engine}>{engine}</option>
-							{/each}
-						</select>
-					</div>
-				</div>
-
-				{#if config.CODE_INTERPRETER_ENGINE === 'jupyter'}
-					<div class="mt-1 flex flex-col gap-1.5 mb-1 w-full">
-						<div class="text-xs font-medium">
-							{$i18n.t('Jupyter URL')}
-						</div>
-
-						<div class="flex w-full">
-							<div class="flex-1">
-								<input
-									class="w-full text-sm py-0.5 placeholder:text-gray-300 dark:placeholder:text-gray-700 bg-transparent outline-none"
-									type="text"
-									placeholder={$i18n.t('Enter Jupyter URL')}
-									bind:value={config.CODE_INTERPRETER_JUPYTER_URL}
-									autocomplete="off"
-								/>
-							</div>
-						</div>
-					</div>
-
-					<div class="mt-1 flex gap-2 mb-1 w-full items-center justify-between">
-						<div class="text-xs font-medium">
-							{$i18n.t('Jupyter Auth')}
-						</div>
-
-						<div>
-							<select
-								class="dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-left"
-								bind:value={config.CODE_INTERPRETER_JUPYTER_AUTH}
-								placeholder={$i18n.t('Select an auth method')}
-							>
-								<option selected value="">{$i18n.t('None')}</option>
-								<option value="token">{$i18n.t('Token')}</option>
-								<option value="password">{$i18n.t('Password')}</option>
-							</select>
-						</div>
-					</div>
-
-					{#if config.CODE_INTERPRETER_JUPYTER_AUTH}
-						<div class="flex w-full gap-2">
-							<div class="flex-1">
-								{#if config.CODE_INTERPRETER_JUPYTER_AUTH === 'password'}
-									<SensitiveInput
-										type="text"
-										placeholder={$i18n.t('Enter Jupyter Password')}
-										bind:value={config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD}
-										autocomplete="off"
-									/>
-								{:else}
-									<SensitiveInput
-										type="text"
-										placeholder={$i18n.t('Enter Jupyter Token')}
-										bind:value={config.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN}
-										autocomplete="off"
-									/>
-								{/if}
-							</div>
-						</div>
-					{/if}
-				{/if}
-			</div>
-
-			<hr class=" dark:border-gray-850 my-2" />
-
-			<div>
-				<div class="py-0.5 w-full">
-					<div class=" mb-2.5 text-xs font-medium">
-						{$i18n.t('Code Interpreter Prompt Template')}
-					</div>
-
-					<Tooltip
-						content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
-						placement="top-start"
-					>
-						<Textarea
-							bind:value={config.CODE_INTERPRETER_PROMPT_TEMPLATE}
-							placeholder={$i18n.t(
-								'Leave empty to use the default prompt, or enter a custom prompt'
-							)}
-						/>
-					</Tooltip>
-				</div>
-			</div>
-		{/if}
-	</div>
-	<div class="flex justify-end pt-3 text-sm font-medium">
-		<button
-			class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
-			type="submit"
-		>
-			{$i18n.t('Save')}
-		</button>
-	</div>
-</form>

+ 4 - 4
src/lib/components/admin/Settings/Connections.svelte

@@ -234,7 +234,7 @@
 					</div>
 					</div>
 
 
 					{#if ENABLE_OPENAI_API}
 					{#if ENABLE_OPENAI_API}
-						<hr class=" border-gray-50 dark:border-gray-850" />
+						<hr class=" border-gray-100 dark:border-gray-850" />
 
 
 						<div class="">
 						<div class="">
 							<div class="flex justify-between items-center">
 							<div class="flex justify-between items-center">
@@ -283,7 +283,7 @@
 				</div>
 				</div>
 			</div>
 			</div>
 
 
-			<hr class=" border-gray-50 dark:border-gray-850" />
+			<hr class=" border-gray-100 dark:border-gray-850" />
 
 
 			<div class="pr-1.5 my-2">
 			<div class="pr-1.5 my-2">
 				<div class="flex justify-between items-center text-sm mb-2">
 				<div class="flex justify-between items-center text-sm mb-2">
@@ -300,7 +300,7 @@
 				</div>
 				</div>
 
 
 				{#if ENABLE_OLLAMA_API}
 				{#if ENABLE_OLLAMA_API}
-					<hr class=" border-gray-50 dark:border-gray-850 my-2" />
+					<hr class=" border-gray-100 dark:border-gray-850 my-2" />
 
 
 					<div class="">
 					<div class="">
 						<div class="flex justify-between items-center">
 						<div class="flex justify-between items-center">
@@ -357,7 +357,7 @@
 				{/if}
 				{/if}
 			</div>
 			</div>
 
 
-			<hr class=" border-gray-50 dark:border-gray-850" />
+			<hr class=" border-gray-100 dark:border-gray-850" />
 
 
 			<div class="pr-1.5 my-2">
 			<div class="pr-1.5 my-2">
 				<div class="flex justify-between items-center text-sm">
 				<div class="flex justify-between items-center text-sm">

+ 1 - 1
src/lib/components/admin/Settings/Connections/ManageOllamaModal.svelte

@@ -16,7 +16,7 @@
 			<div
 			<div
 				class="flex w-full justify-between items-center text-lg font-medium self-center font-primary"
 				class="flex w-full justify-between items-center text-lg font-medium self-center font-primary"
 			>
 			>
-				<div class=" flex-shrink-0">
+				<div class=" shrink-0">
 					{$i18n.t('Manage Ollama')}
 					{$i18n.t('Manage Ollama')}
 				</div>
 				</div>
 			</div>
 			</div>

+ 1 - 1
src/lib/components/admin/Settings/Connections/OllamaConnection.svelte

@@ -56,7 +56,7 @@
 		{/if}
 		{/if}
 
 
 		<input
 		<input
-			class="w-full text-sm bg-transparent outline-none"
+			class="w-full text-sm bg-transparent outline-hidden"
 			placeholder={$i18n.t('Enter URL (e.g. http://localhost:11434)')}
 			placeholder={$i18n.t('Enter URL (e.g. http://localhost:11434)')}
 			bind:value={url}
 			bind:value={url}
 		/>
 		/>

+ 2 - 2
src/lib/components/admin/Settings/Connections/OpenAIConnection.svelte

@@ -54,7 +54,7 @@
 		<div class="flex w-full">
 		<div class="flex w-full">
 			<div class="flex-1 relative">
 			<div class="flex-1 relative">
 				<input
 				<input
-					class=" outline-none w-full bg-transparent {pipeline ? 'pr-8' : ''}"
+					class=" outline-hidden w-full bg-transparent {pipeline ? 'pr-8' : ''}"
 					placeholder={$i18n.t('API Base URL')}
 					placeholder={$i18n.t('API Base URL')}
 					bind:value={url}
 					bind:value={url}
 					autocomplete="off"
 					autocomplete="off"
@@ -85,7 +85,7 @@
 			</div>
 			</div>
 
 
 			<SensitiveInput
 			<SensitiveInput
-				inputClassName=" outline-none bg-transparent w-full"
+				inputClassName=" outline-hidden bg-transparent w-full"
 				placeholder={$i18n.t('API Key')}
 				placeholder={$i18n.t('API Key')}
 				bind:value={key}
 				bind:value={key}
 			/>
 			/>

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

@@ -119,7 +119,7 @@
 				</div>
 				</div>
 			</button>
 			</button>
 
 
-			<hr class=" dark:border-gray-850 my-1" />
+			<hr class="border-gray-100 dark:border-gray-850 my-1" />
 
 
 			{#if $config?.features.enable_admin_export ?? true}
 			{#if $config?.features.enable_admin_export ?? true}
 				<div class="  flex w-full justify-between">
 				<div class="  flex w-full justify-between">

+ 41 - 24
src/lib/components/admin/Settings/Documents.svelte

@@ -27,7 +27,6 @@
 	import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
 	import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
 	import Switch from '$lib/components/common/Switch.svelte';
 	import Switch from '$lib/components/common/Switch.svelte';
-	import { text } from '@sveltejs/kit';
 	import Textarea from '$lib/components/common/Textarea.svelte';
 	import Textarea from '$lib/components/common/Textarea.svelte';
 
 
 	const i18n = getContext('i18n');
 	const i18n = getContext('i18n');
@@ -56,6 +55,8 @@
 	let chunkOverlap = 0;
 	let chunkOverlap = 0;
 	let pdfExtractImages = true;
 	let pdfExtractImages = true;
 
 
+	let RAG_FULL_CONTEXT = false;
+
 	let enableGoogleDriveIntegration = false;
 	let enableGoogleDriveIntegration = false;
 
 
 	let OpenAIUrl = '';
 	let OpenAIUrl = '';
@@ -182,6 +183,7 @@
 				max_size: fileMaxSize === '' ? null : fileMaxSize,
 				max_size: fileMaxSize === '' ? null : fileMaxSize,
 				max_count: fileMaxCount === '' ? null : fileMaxCount
 				max_count: fileMaxCount === '' ? null : fileMaxCount
 			},
 			},
+			RAG_FULL_CONTEXT: RAG_FULL_CONTEXT,
 			chunk: {
 			chunk: {
 				text_splitter: textSplitter,
 				text_splitter: textSplitter,
 				chunk_overlap: chunkOverlap,
 				chunk_overlap: chunkOverlap,
@@ -242,6 +244,8 @@
 			chunkSize = res.chunk.chunk_size;
 			chunkSize = res.chunk.chunk_size;
 			chunkOverlap = res.chunk.chunk_overlap;
 			chunkOverlap = res.chunk.chunk_overlap;
 
 
+			RAG_FULL_CONTEXT = res.RAG_FULL_CONTEXT;
+
 			contentExtractionEngine = res.content_extraction.engine;
 			contentExtractionEngine = res.content_extraction.engine;
 			tikaServerUrl = res.content_extraction.tika_server_url;
 			tikaServerUrl = res.content_extraction.tika_server_url;
 			showTikaServerUrl = contentExtractionEngine === 'tika';
 			showTikaServerUrl = contentExtractionEngine === 'tika';
@@ -296,7 +300,7 @@
 				<div class=" self-center text-xs font-medium">{$i18n.t('Embedding Model Engine')}</div>
 				<div class=" self-center text-xs font-medium">{$i18n.t('Embedding Model Engine')}</div>
 				<div class="flex items-center relative">
 				<div class="flex items-center relative">
 					<select
 					<select
-						class="dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
+						class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right"
 						bind:value={embeddingEngine}
 						bind:value={embeddingEngine}
 						placeholder="Select an embedding model engine"
 						placeholder="Select an embedding model engine"
 						on:change={(e) => {
 						on:change={(e) => {
@@ -319,7 +323,7 @@
 			{#if embeddingEngine === 'openai'}
 			{#if embeddingEngine === 'openai'}
 				<div class="my-0.5 flex gap-2 pr-2">
 				<div class="my-0.5 flex gap-2 pr-2">
 					<input
 					<input
-						class="flex-1 w-full rounded-lg text-sm bg-transparent outline-none"
+						class="flex-1 w-full rounded-lg text-sm bg-transparent outline-hidden"
 						placeholder={$i18n.t('API Base URL')}
 						placeholder={$i18n.t('API Base URL')}
 						bind:value={OpenAIUrl}
 						bind:value={OpenAIUrl}
 						required
 						required
@@ -330,7 +334,7 @@
 			{:else if embeddingEngine === 'ollama'}
 			{:else if embeddingEngine === 'ollama'}
 				<div class="my-0.5 flex gap-2 pr-2">
 				<div class="my-0.5 flex gap-2 pr-2">
 					<input
 					<input
-						class="flex-1 w-full rounded-lg text-sm bg-transparent outline-none"
+						class="flex-1 w-full rounded-lg text-sm bg-transparent outline-hidden"
 						placeholder={$i18n.t('API Base URL')}
 						placeholder={$i18n.t('API Base URL')}
 						bind:value={OllamaUrl}
 						bind:value={OllamaUrl}
 						required
 						required
@@ -375,7 +379,7 @@
 				<div class=" self-center text-xs font-medium">{$i18n.t('Hybrid Search')}</div>
 				<div class=" self-center text-xs font-medium">{$i18n.t('Hybrid Search')}</div>
 
 
 				<button
 				<button
-					class="p-1 px-3 text-xs flex rounded transition"
+					class="p-1 px-3 text-xs flex rounded-sm transition"
 					on:click={() => {
 					on:click={() => {
 						toggleHybridSearch();
 						toggleHybridSearch();
 					}}
 					}}
@@ -388,9 +392,22 @@
 					{/if}
 					{/if}
 				</button>
 				</button>
 			</div>
 			</div>
+
+			<div class=" py-0.5 flex w-full justify-between">
+				<div class=" self-center text-xs font-medium">{$i18n.t('Full Context Mode')}</div>
+				<div class="flex items-center relative">
+					<Tooltip
+						content={RAG_FULL_CONTEXT
+							? 'Inject entire contents as context for comprehensive processing, this is recommended for complex queries.'
+							: 'Default to segmented retrieval for focused and relevant content extraction, this is recommended for most cases.'}
+					>
+						<Switch bind:state={RAG_FULL_CONTEXT} />
+					</Tooltip>
+				</div>
+			</div>
 		</div>
 		</div>
 
 
-		<hr class="dark:border-gray-850" />
+		<hr class="border-gray-100 dark:border-gray-850" />
 
 
 		<div class="space-y-2" />
 		<div class="space-y-2" />
 		<div>
 		<div>
@@ -400,7 +417,7 @@
 				<div class="flex w-full">
 				<div class="flex w-full">
 					<div class="flex-1 mr-2">
 					<div class="flex-1 mr-2">
 						<input
 						<input
-							class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+							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={embeddingModel}
 							bind:value={embeddingModel}
 							placeholder={$i18n.t('Set embedding model')}
 							placeholder={$i18n.t('Set embedding model')}
 							required
 							required
@@ -411,7 +428,7 @@
 				<div class="flex w-full">
 				<div class="flex w-full">
 					<div class="flex-1 mr-2">
 					<div class="flex-1 mr-2">
 						<input
 						<input
-							class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+							class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
 							placeholder={$i18n.t('Set embedding model (e.g. {{model}})', {
 							placeholder={$i18n.t('Set embedding model (e.g. {{model}})', {
 								model: embeddingModel.slice(-40)
 								model: embeddingModel.slice(-40)
 							})}
 							})}
@@ -490,7 +507,7 @@
 					<div class="flex w-full">
 					<div class="flex w-full">
 						<div class="flex-1 mr-2">
 						<div class="flex-1 mr-2">
 							<input
 							<input
-								class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+								class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
 								placeholder={$i18n.t('Set reranking model (e.g. {{model}})', {
 								placeholder={$i18n.t('Set reranking model (e.g. {{model}})', {
 									model: 'BAAI/bge-reranker-v2-m3'
 									model: 'BAAI/bge-reranker-v2-m3'
 								})}
 								})}
@@ -555,7 +572,7 @@
 			{/if}
 			{/if}
 		</div>
 		</div>
 
 
-		<hr class=" dark:border-gray-850" />
+		<hr class=" border-gray-100 dark:border-gray-850" />
 
 
 		<div class="">
 		<div class="">
 			<div class="text-sm font-medium mb-1">{$i18n.t('Content Extraction')}</div>
 			<div class="text-sm font-medium mb-1">{$i18n.t('Content Extraction')}</div>
@@ -564,7 +581,7 @@
 				<div class="self-center text-xs font-medium">{$i18n.t('Engine')}</div>
 				<div class="self-center text-xs font-medium">{$i18n.t('Engine')}</div>
 				<div class="flex items-center relative">
 				<div class="flex items-center relative">
 					<select
 					<select
-						class="dark:bg-gray-900 w-fit pr-8 rounded px-2 text-xs bg-transparent outline-none text-right"
+						class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 text-xs bg-transparent outline-hidden text-right"
 						bind:value={contentExtractionEngine}
 						bind:value={contentExtractionEngine}
 						on:change={(e) => {
 						on:change={(e) => {
 							showTikaServerUrl = e.target.value === 'tika';
 							showTikaServerUrl = e.target.value === 'tika';
@@ -580,7 +597,7 @@
 				<div class="flex w-full mt-1">
 				<div class="flex w-full mt-1">
 					<div class="flex-1 mr-2">
 					<div class="flex-1 mr-2">
 						<input
 						<input
-							class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+							class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
 							placeholder={$i18n.t('Enter Tika Server URL')}
 							placeholder={$i18n.t('Enter Tika Server URL')}
 							bind:value={tikaServerUrl}
 							bind:value={tikaServerUrl}
 						/>
 						/>
@@ -589,7 +606,7 @@
 			{/if}
 			{/if}
 		</div>
 		</div>
 
 
-		<hr class=" dark:border-gray-850" />
+		<hr class=" border-gray-100 dark:border-gray-850" />
 
 
 		<div class="text-sm font-medium mb-1">{$i18n.t('Google Drive')}</div>
 		<div class="text-sm font-medium mb-1">{$i18n.t('Google Drive')}</div>
 
 
@@ -602,7 +619,7 @@
 			</div>
 			</div>
 		</div>
 		</div>
 
 
-		<hr class=" dark:border-gray-850" />
+		<hr class=" border-gray-100 dark:border-gray-850" />
 
 
 		<div class=" ">
 		<div class=" ">
 			<div class=" text-sm font-medium mb-1">{$i18n.t('Query Params')}</div>
 			<div class=" text-sm font-medium mb-1">{$i18n.t('Query Params')}</div>
@@ -613,7 +630,7 @@
 
 
 					<div class="w-full">
 					<div class="w-full">
 						<input
 						<input
-							class=" w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+							class=" w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
 							type="number"
 							type="number"
 							placeholder={$i18n.t('Enter Top K')}
 							placeholder={$i18n.t('Enter Top K')}
 							bind:value={querySettings.k}
 							bind:value={querySettings.k}
@@ -631,7 +648,7 @@
 
 
 						<div class="w-full">
 						<div class="w-full">
 							<input
 							<input
-								class=" w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+								class=" w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
 								type="number"
 								type="number"
 								step="0.01"
 								step="0.01"
 								placeholder={$i18n.t('Enter Score')}
 								placeholder={$i18n.t('Enter Score')}
@@ -667,7 +684,7 @@
 			</div>
 			</div>
 		</div>
 		</div>
 
 
-		<hr class=" dark:border-gray-850" />
+		<hr class=" border-gray-100 dark:border-gray-850" />
 
 
 		<div class=" ">
 		<div class=" ">
 			<div class="mb-1 text-sm font-medium">{$i18n.t('Chunk Params')}</div>
 			<div class="mb-1 text-sm font-medium">{$i18n.t('Chunk Params')}</div>
@@ -676,7 +693,7 @@
 				<div class="self-center text-xs font-medium">{$i18n.t('Text Splitter')}</div>
 				<div class="self-center text-xs font-medium">{$i18n.t('Text Splitter')}</div>
 				<div class="flex items-center relative">
 				<div class="flex items-center relative">
 					<select
 					<select
-						class="dark:bg-gray-900 w-fit pr-8 rounded px-2 text-xs bg-transparent outline-none text-right"
+						class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 text-xs bg-transparent outline-hidden text-right"
 						bind:value={textSplitter}
 						bind:value={textSplitter}
 					>
 					>
 						<option value="">{$i18n.t('Default')} ({$i18n.t('Character')})</option>
 						<option value="">{$i18n.t('Default')} ({$i18n.t('Character')})</option>
@@ -692,7 +709,7 @@
 					</div>
 					</div>
 					<div class="self-center">
 					<div class="self-center">
 						<input
 						<input
-							class=" w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+							class=" w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
 							type="number"
 							type="number"
 							placeholder={$i18n.t('Enter Chunk Size')}
 							placeholder={$i18n.t('Enter Chunk Size')}
 							bind:value={chunkSize}
 							bind:value={chunkSize}
@@ -709,7 +726,7 @@
 
 
 					<div class="self-center">
 					<div class="self-center">
 						<input
 						<input
-							class="w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+							class="w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
 							type="number"
 							type="number"
 							placeholder={$i18n.t('Enter Chunk Overlap')}
 							placeholder={$i18n.t('Enter Chunk Overlap')}
 							bind:value={chunkOverlap}
 							bind:value={chunkOverlap}
@@ -731,7 +748,7 @@
 			</div>
 			</div>
 		</div>
 		</div>
 
 
-		<hr class=" dark:border-gray-850" />
+		<hr class=" border-gray-100 dark:border-gray-850" />
 
 
 		<div class="">
 		<div class="">
 			<div class="text-sm font-medium mb-1">{$i18n.t('Files')}</div>
 			<div class="text-sm font-medium mb-1">{$i18n.t('Files')}</div>
@@ -750,7 +767,7 @@
 							placement="top-start"
 							placement="top-start"
 						>
 						>
 							<input
 							<input
-								class="w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+								class="w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
 								type="number"
 								type="number"
 								placeholder={$i18n.t('Leave empty for unlimited')}
 								placeholder={$i18n.t('Leave empty for unlimited')}
 								bind:value={fileMaxSize}
 								bind:value={fileMaxSize}
@@ -773,7 +790,7 @@
 							placement="top-start"
 							placement="top-start"
 						>
 						>
 							<input
 							<input
-								class=" w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+								class=" w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
 								type="number"
 								type="number"
 								placeholder={$i18n.t('Leave empty for unlimited')}
 								placeholder={$i18n.t('Leave empty for unlimited')}
 								bind:value={fileMaxCount}
 								bind:value={fileMaxCount}
@@ -786,7 +803,7 @@
 			</div>
 			</div>
 		</div>
 		</div>
 
 
-		<hr class=" dark:border-gray-850" />
+		<hr class=" border-gray-100 dark:border-gray-850" />
 
 
 		<div>
 		<div>
 			<button
 			<button

+ 5 - 5
src/lib/components/admin/Settings/Evaluations/ArenaModelModal.svelte

@@ -245,7 +245,7 @@
 
 
 								<div class="flex-1">
 								<div class="flex-1">
 									<input
 									<input
-										class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none"
+										class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
 										type="text"
 										type="text"
 										bind:value={name}
 										bind:value={name}
 										placeholder={$i18n.t('Model Name')}
 										placeholder={$i18n.t('Model Name')}
@@ -260,7 +260,7 @@
 
 
 								<div class="flex-1">
 								<div class="flex-1">
 									<input
 									<input
-										class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none"
+										class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
 										type="text"
 										type="text"
 										bind:value={id}
 										bind:value={id}
 										placeholder={$i18n.t('Model ID')}
 										placeholder={$i18n.t('Model ID')}
@@ -277,7 +277,7 @@
 
 
 							<div class="flex-1">
 							<div class="flex-1">
 								<input
 								<input
-									class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none"
+									class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
 									type="text"
 									type="text"
 									bind:value={description}
 									bind:value={description}
 									placeholder={$i18n.t('Enter description')}
 									placeholder={$i18n.t('Enter description')}
@@ -324,7 +324,7 @@
 											<div class=" text-sm flex-1 py-1 rounded-lg">
 											<div class=" text-sm flex-1 py-1 rounded-lg">
 												{$models.find((model) => model.id === modelId)?.name}
 												{$models.find((model) => model.id === modelId)?.name}
 											</div>
 											</div>
-											<div class="flex-shrink-0">
+											<div class="shrink-0">
 												<button
 												<button
 													type="button"
 													type="button"
 													on:click={() => {
 													on:click={() => {
@@ -350,7 +350,7 @@
 							<select
 							<select
 								class="w-full py-1 text-sm rounded-lg bg-transparent {selectedModelId
 								class="w-full py-1 text-sm rounded-lg bg-transparent {selectedModelId
 									? ''
 									? ''
-									: 'text-gray-500'} placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none"
+									: 'text-gray-500'} placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
 								bind:value={selectedModelId}
 								bind:value={selectedModelId}
 							>
 							>
 								<option value="">{$i18n.t('Select a model')}</option>
 								<option value="">{$i18n.t('Select a model')}</option>

+ 1 - 1
src/lib/components/admin/Settings/Evaluations/Model.svelte

@@ -34,7 +34,7 @@
 
 
 				<div class="w-full flex flex-col">
 				<div class="w-full flex flex-col">
 					<div class="flex items-center gap-1">
 					<div class="flex items-center gap-1">
-						<div class="flex-shrink-0 line-clamp-1">
+						<div class="shrink-0 line-clamp-1">
 							{model.name}
 							{model.name}
 						</div>
 						</div>
 					</div>
 					</div>

+ 471 - 316
src/lib/components/admin/Settings/General.svelte

@@ -1,5 +1,5 @@
 <script lang="ts">
 <script lang="ts">
-	import { getBackendConfig, getWebhookUrl, updateWebhookUrl } from '$lib/apis';
+	import { getBackendConfig, getVersionUpdates, getWebhookUrl, updateWebhookUrl } from '$lib/apis';
 	import {
 	import {
 		getAdminConfig,
 		getAdminConfig,
 		getLdapConfig,
 		getLdapConfig,
@@ -11,7 +11,9 @@
 	import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
 	import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
 	import Switch from '$lib/components/common/Switch.svelte';
 	import Switch from '$lib/components/common/Switch.svelte';
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
-	import { config } from '$lib/stores';
+	import { WEBUI_BUILD_HASH, WEBUI_VERSION } from '$lib/constants';
+	import { config, showChangelog } from '$lib/stores';
+	import { compareVersion } from '$lib/utils';
 	import { onMount, getContext } from 'svelte';
 	import { onMount, getContext } from 'svelte';
 	import { toast } from 'svelte-sonner';
 	import { toast } from 'svelte-sonner';
 
 
@@ -19,6 +21,12 @@
 
 
 	export let saveHandler: Function;
 	export let saveHandler: Function;
 
 
+	let updateAvailable = null;
+	let version = {
+		current: '',
+		latest: ''
+	};
+
 	let adminConfig = null;
 	let adminConfig = null;
 	let webhookUrl = '';
 	let webhookUrl = '';
 
 
@@ -39,6 +47,21 @@
 		ciphers: ''
 		ciphers: ''
 	};
 	};
 
 
+	const checkForVersionUpdates = async () => {
+		updateAvailable = null;
+		version = await getVersionUpdates(localStorage.token).catch((error) => {
+			return {
+				current: WEBUI_VERSION,
+				latest: WEBUI_VERSION
+			};
+		});
+
+		console.log(version);
+
+		updateAvailable = compareVersion(version.latest, version.current);
+		console.log(updateAvailable);
+	};
+
 	const updateLdapServerHandler = async () => {
 	const updateLdapServerHandler = async () => {
 		if (!ENABLE_LDAP) return;
 		if (!ENABLE_LDAP) return;
 		const res = await updateLdapServer(localStorage.token, LDAP_SERVER).catch((error) => {
 		const res = await updateLdapServer(localStorage.token, LDAP_SERVER).catch((error) => {
@@ -63,6 +86,8 @@
 	};
 	};
 
 
 	onMount(async () => {
 	onMount(async () => {
+		checkForVersionUpdates();
+
 		await Promise.all([
 		await Promise.all([
 			(async () => {
 			(async () => {
 				adminConfig = await getAdminConfig(localStorage.token);
 				adminConfig = await getAdminConfig(localStorage.token);
@@ -87,381 +112,511 @@
 		updateHandler();
 		updateHandler();
 	}}
 	}}
 >
 >
-	<div class=" space-y-3 overflow-y-scroll scrollbar-hidden h-full">
+	<div class="mt-0.5 space-y-3 overflow-y-scroll scrollbar-hidden h-full">
 		{#if adminConfig !== null}
 		{#if adminConfig !== null}
-			<div>
-				<div class=" mb-3 text-sm font-medium">{$i18n.t('General Settings')}</div>
+			<div class="">
+				<div class="mb-3.5">
+					<div class=" mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
 
 
-				<div class="  flex w-full justify-between pr-2">
-					<div class=" self-center text-xs font-medium">{$i18n.t('Enable New Sign Ups')}</div>
+					<hr class=" border-gray-100 dark:border-gray-850 my-2" />
 
 
-					<Switch bind:state={adminConfig.ENABLE_SIGNUP} />
-				</div>
+					<div class="mb-2.5">
+						<div class=" mb-1 text-xs font-medium flex space-x-2 items-center">
+							<div>
+								{$i18n.t('Version')}
+							</div>
+						</div>
+						<div class="flex w-full justify-between items-center">
+							<div class="flex flex-col text-xs text-gray-700 dark:text-gray-200">
+								<div class="flex gap-1">
+									<Tooltip content={WEBUI_BUILD_HASH}>
+										v{WEBUI_VERSION}
+									</Tooltip>
+
+									<a
+										href="https://github.com/open-webui/open-webui/releases/tag/v{version.latest}"
+										target="_blank"
+									>
+										{updateAvailable === null
+											? $i18n.t('Checking for updates...')
+											: updateAvailable
+												? `(v${version.latest} ${$i18n.t('available!')})`
+												: $i18n.t('(latest)')}
+									</a>
+								</div>
 
 
-				<div class="  my-3 flex w-full justify-between">
-					<div class=" self-center text-xs font-medium">{$i18n.t('Default User Role')}</div>
-					<div class="flex items-center relative">
-						<select
-							class="dark:bg-gray-900 w-fit pr-8 rounded px-2 text-xs bg-transparent outline-none text-right"
-							bind:value={adminConfig.DEFAULT_USER_ROLE}
-							placeholder="Select a role"
-						>
-							<option value="pending">{$i18n.t('pending')}</option>
-							<option value="user">{$i18n.t('user')}</option>
-							<option value="admin">{$i18n.t('admin')}</option>
-						</select>
-					</div>
-				</div>
+								<button
+									class=" underline flex items-center space-x-1 text-xs text-gray-500 dark:text-gray-500"
+									type="button"
+									on:click={() => {
+										showChangelog.set(true);
+									}}
+								>
+									<div>{$i18n.t("See what's new")}</div>
+								</button>
+							</div>
 
 
-				<div class=" flex w-full justify-between pr-2 my-3">
-					<div class=" self-center text-xs font-medium">{$i18n.t('Enable API Key')}</div>
+							<button
+								class=" text-xs px-3 py-1.5 bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-lg font-medium"
+								type="button"
+								on:click={() => {
+									checkForVersionUpdates();
+								}}
+							>
+								{$i18n.t('Check for updates')}
+							</button>
+						</div>
+					</div>
 
 
-					<Switch bind:state={adminConfig.ENABLE_API_KEY} />
-				</div>
+					<div class="mb-2.5">
+						<div class="flex w-full justify-between items-center">
+							<div class="text-xs pr-2">
+								<div class="">
+									{$i18n.t('Help')}
+								</div>
+								<div class=" text-xs text-gray-500">
+									{$i18n.t('Discover how to use Open WebUI and seek support from the community.')}
+								</div>
+							</div>
 
 
-				{#if adminConfig?.ENABLE_API_KEY}
-					<div class=" flex w-full justify-between pr-2 my-3">
-						<div class=" self-center text-xs font-medium">
-							{$i18n.t('API Key Endpoint Restrictions')}
+							<a
+								class="flex-shrink-0 text-xs font-medium underline"
+								href="https://docs.openwebui.com/"
+								target="_blank"
+							>
+								{$i18n.t('Documentation')}
+							</a>
 						</div>
 						</div>
 
 
-						<Switch bind:state={adminConfig.ENABLE_API_KEY_ENDPOINT_RESTRICTIONS} />
-					</div>
+						<div class="mt-1">
+							<div class="flex space-x-1">
+								<a href="https://discord.gg/5rJgQTnV4s" target="_blank">
+									<img
+										alt="Discord"
+										src="https://img.shields.io/badge/Discord-Open_WebUI-blue?logo=discord&logoColor=white"
+									/>
+								</a>
 
 
-					{#if adminConfig?.ENABLE_API_KEY_ENDPOINT_RESTRICTIONS}
-						<div class=" flex w-full flex-col pr-2">
-							<div class=" text-xs font-medium">
-								{$i18n.t('Allowed Endpoints')}
-							</div>
+								<a href="https://twitter.com/OpenWebUI" target="_blank">
+									<img
+										alt="X (formerly Twitter) Follow"
+										src="https://img.shields.io/twitter/follow/OpenWebUI"
+									/>
+								</a>
 
 
-							<input
-								class="w-full mt-1 rounded-lg text-sm dark:text-gray-300 bg-transparent outline-none"
-								type="text"
-								placeholder={`e.g.) /api/v1/messages, /api/v1/channels`}
-								bind:value={adminConfig.API_KEY_ALLOWED_ENDPOINTS}
-							/>
+								<a href="https://github.com/open-webui/open-webui" target="_blank">
+									<img
+										alt="Github Repo"
+										src="https://img.shields.io/github/stars/open-webui/open-webui?style=social&label=Star us on Github"
+									/>
+								</a>
+							</div>
+						</div>
+					</div>
 
 
-							<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
-								<!-- https://docs.openwebui.com/getting-started/advanced-topics/api-endpoints -->
+					<div class="mb-2.5">
+						<div class="flex w-full justify-between items-center">
+							<div class="text-xs pr-2">
+								<div class="">
+									{$i18n.t('License')}
+								</div>
 								<a
 								<a
-									href="https://docs.openwebui.com/getting-started/api-endpoints"
+									class=" text-xs text-gray-500 hover:underline"
+									href="https://docs.openwebui.com/enterprise"
 									target="_blank"
 									target="_blank"
-									class=" text-gray-300 font-medium underline"
 								>
 								>
-									{$i18n.t('To learn more about available endpoints, visit our documentation.')}
+									{$i18n.t(
+										'Upgrade to a licensed plan for enhanced capabilities, including custom theming and branding, and dedicated support.'
+									)}
 								</a>
 								</a>
 							</div>
 							</div>
-						</div>
-					{/if}
-				{/if}
-
-				<hr class=" border-gray-50 dark:border-gray-850 my-2" />
-
-				<div class="my-3 flex w-full items-center justify-between pr-2">
-					<div class=" self-center text-xs font-medium">
-						{$i18n.t('Show Admin Details in Account Pending Overlay')}
-					</div>
-
-					<Switch bind:state={adminConfig.SHOW_ADMIN_DETAILS} />
-				</div>
-
-				<div class="my-3 flex w-full items-center justify-between pr-2">
-					<div class=" self-center text-xs font-medium">{$i18n.t('Enable Community Sharing')}</div>
-
-					<Switch bind:state={adminConfig.ENABLE_COMMUNITY_SHARING} />
-				</div>
-
-				<div class="my-3 flex w-full items-center justify-between pr-2">
-					<div class=" self-center text-xs font-medium">{$i18n.t('Enable Message Rating')}</div>
-
-					<Switch bind:state={adminConfig.ENABLE_MESSAGE_RATING} />
-				</div>
-
-				<hr class=" border-gray-50 dark:border-gray-850 my-2" />
-
-				<div class=" w-full justify-between">
-					<div class="flex w-full justify-between">
-						<div class=" self-center text-xs font-medium">{$i18n.t('WebUI URL')}</div>
-					</div>
 
 
-					<div class="flex mt-2 space-x-2">
-						<input
-							class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
-							type="text"
-							placeholder={`e.g.) "http://localhost:3000"`}
-							bind:value={adminConfig.WEBUI_URL}
-						/>
-					</div>
-
-					<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
-						{$i18n.t(
-							'Enter the public URL of your WebUI. This URL will be used to generate links in the notifications.'
-						)}
+							<!-- <button
+								class="flex-shrink-0 text-xs px-3 py-1.5 bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-lg font-medium"
+							>
+								{$i18n.t('Activate')}
+							</button> -->
+						</div>
 					</div>
 					</div>
 				</div>
 				</div>
 
 
-				<hr class=" border-gray-50 dark:border-gray-850 my-2" />
-
-				<div class=" w-full justify-between">
-					<div class="flex w-full justify-between">
-						<div class=" self-center text-xs font-medium">{$i18n.t('JWT Expiration')}</div>
-					</div>
+				<div class="mb-3">
+					<div class=" mb-2.5 text-base font-medium">{$i18n.t('Authentication')}</div>
 
 
-					<div class="flex mt-2 space-x-2">
-						<input
-							class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
-							type="text"
-							placeholder={`e.g.) "30m","1h", "10d". `}
-							bind:value={adminConfig.JWT_EXPIRES_IN}
-						/>
-					</div>
+					<hr class=" border-gray-100 dark:border-gray-850 my-2" />
 
 
-					<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
-						{$i18n.t('Valid time units:')}
-						<span class=" text-gray-300 font-medium"
-							>{$i18n.t("'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.")}</span
-						>
+					<div class="  mb-2.5 flex w-full justify-between">
+						<div class=" self-center text-xs font-medium">{$i18n.t('Default User Role')}</div>
+						<div class="flex items-center relative">
+							<select
+								class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 text-xs bg-transparent outline-hidden text-right"
+								bind:value={adminConfig.DEFAULT_USER_ROLE}
+								placeholder="Select a role"
+							>
+								<option value="pending">{$i18n.t('pending')}</option>
+								<option value="user">{$i18n.t('user')}</option>
+								<option value="admin">{$i18n.t('admin')}</option>
+							</select>
+						</div>
 					</div>
 					</div>
-				</div>
-
-				<hr class=" border-gray-50 dark:border-gray-850 my-2" />
 
 
-				<div class=" w-full justify-between">
-					<div class="flex w-full justify-between">
-						<div class=" self-center text-xs font-medium">{$i18n.t('Webhook URL')}</div>
-					</div>
+					<div class=" mb-2.5 flex w-full justify-between pr-2">
+						<div class=" self-center text-xs font-medium">{$i18n.t('Enable New Sign Ups')}</div>
 
 
-					<div class="flex mt-2 space-x-2">
-						<input
-							class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
-							type="text"
-							placeholder={`https://example.com/webhook`}
-							bind:value={webhookUrl}
-						/>
+						<Switch bind:state={adminConfig.ENABLE_SIGNUP} />
 					</div>
 					</div>
-				</div>
 
 
-				<hr class=" border-gray-50 dark:border-gray-850 my-2" />
+					<div class="mb-2.5 flex w-full items-center justify-between pr-2">
+						<div class=" self-center text-xs font-medium">
+							{$i18n.t('Show Admin Details in Account Pending Overlay')}
+						</div>
 
 
-				<div class="pt-1 flex w-full justify-between pr-2">
-					<div class=" self-center text-sm font-medium">
-						{$i18n.t('Channels')} ({$i18n.t('Beta')})
+						<Switch bind:state={adminConfig.SHOW_ADMIN_DETAILS} />
 					</div>
 					</div>
 
 
-					<Switch bind:state={adminConfig.ENABLE_CHANNELS} />
-				</div>
-			</div>
-		{/if}
-
-		<hr class=" border-gray-50 dark:border-gray-850" />
-
-		<div class=" space-y-3">
-			<div class="mt-2 space-y-2 pr-1.5">
-				<div class="flex justify-between items-center text-sm">
-					<div class="  font-medium">{$i18n.t('LDAP')}</div>
+					<div class="mb-2.5 flex w-full justify-between pr-2">
+						<div class=" self-center text-xs font-medium">{$i18n.t('Enable API Key')}</div>
 
 
-					<div class="mt-1">
-						<Switch
-							bind:state={ENABLE_LDAP}
-							on:change={async () => {
-								updateLdapConfig(localStorage.token, ENABLE_LDAP);
-							}}
-						/>
+						<Switch bind:state={adminConfig.ENABLE_API_KEY} />
 					</div>
 					</div>
-				</div>
 
 
-				{#if ENABLE_LDAP}
-					<div class="flex flex-col gap-1">
-						<div class="flex w-full gap-2">
-							<div class="w-full">
-								<div class=" self-center text-xs font-medium min-w-fit mb-1">
-									{$i18n.t('Label')}
-								</div>
-								<input
-									class="w-full bg-transparent outline-none py-0.5"
-									required
-									placeholder={$i18n.t('Enter server label')}
-									bind:value={LDAP_SERVER.label}
-								/>
+					{#if adminConfig?.ENABLE_API_KEY}
+						<div class="mb-2.5 flex w-full justify-between pr-2">
+							<div class=" self-center text-xs font-medium">
+								{$i18n.t('API Key Endpoint Restrictions')}
 							</div>
 							</div>
-							<div class="w-full"></div>
+
+							<Switch bind:state={adminConfig.ENABLE_API_KEY_ENDPOINT_RESTRICTIONS} />
 						</div>
 						</div>
-						<div class="flex w-full gap-2">
-							<div class="w-full">
-								<div class=" self-center text-xs font-medium min-w-fit mb-1">
-									{$i18n.t('Host')}
+
+						{#if adminConfig?.ENABLE_API_KEY_ENDPOINT_RESTRICTIONS}
+							<div class=" flex w-full flex-col pr-2">
+								<div class=" text-xs font-medium">
+									{$i18n.t('Allowed Endpoints')}
 								</div>
 								</div>
+
 								<input
 								<input
-									class="w-full bg-transparent outline-none py-0.5"
-									required
-									placeholder={$i18n.t('Enter server host')}
-									bind:value={LDAP_SERVER.host}
+									class="w-full mt-1 rounded-lg text-sm dark:text-gray-300 bg-transparent outline-hidden"
+									type="text"
+									placeholder={`e.g.) /api/v1/messages, /api/v1/channels`}
+									bind:value={adminConfig.API_KEY_ALLOWED_ENDPOINTS}
 								/>
 								/>
-							</div>
-							<div class="w-full">
-								<div class=" self-center text-xs font-medium min-w-fit mb-1">
-									{$i18n.t('Port')}
-								</div>
-								<Tooltip
-									placement="top-start"
-									content={$i18n.t('Default to 389 or 636 if TLS is enabled')}
-									className="w-full"
-								>
-									<input
-										class="w-full bg-transparent outline-none py-0.5"
-										type="number"
-										placeholder={$i18n.t('Enter server port')}
-										bind:value={LDAP_SERVER.port}
-									/>
-								</Tooltip>
-							</div>
-						</div>
-						<div class="flex w-full gap-2">
-							<div class="w-full">
-								<div class=" self-center text-xs font-medium min-w-fit mb-1">
-									{$i18n.t('Application DN')}
-								</div>
-								<Tooltip
-									content={$i18n.t('The Application Account DN you bind with for search')}
-									placement="top-start"
-								>
-									<input
-										class="w-full bg-transparent outline-none py-0.5"
-										required
-										placeholder={$i18n.t('Enter Application DN')}
-										bind:value={LDAP_SERVER.app_dn}
-									/>
-								</Tooltip>
-							</div>
-							<div class="w-full">
-								<div class=" self-center text-xs font-medium min-w-fit mb-1">
-									{$i18n.t('Application DN Password')}
-								</div>
-								<SensitiveInput
-									placeholder={$i18n.t('Enter Application DN Password')}
-									bind:value={LDAP_SERVER.app_dn_password}
-								/>
-							</div>
-						</div>
-						<div class="flex w-full gap-2">
-							<div class="w-full">
-								<div class=" self-center text-xs font-medium min-w-fit mb-1">
-									{$i18n.t('Attribute for Mail')}
-								</div>
-								<Tooltip
-									content={$i18n.t(
-										'The LDAP attribute that maps to the mail that users use to sign in.'
-									)}
-									placement="top-start"
-								>
-									<input
-										class="w-full bg-transparent outline-none py-0.5"
-										required
-										placeholder={$i18n.t('Example: mail')}
-										bind:value={LDAP_SERVER.attribute_for_mail}
-									/>
-								</Tooltip>
-							</div>
-						</div>
-						<div class="flex w-full gap-2">
-							<div class="w-full">
-								<div class=" self-center text-xs font-medium min-w-fit mb-1">
-									{$i18n.t('Attribute for Username')}
-								</div>
-								<Tooltip
-									content={$i18n.t(
-										'The LDAP attribute that maps to the username that users use to sign in.'
-									)}
-									placement="top-start"
-								>
-									<input
-										class="w-full bg-transparent outline-none py-0.5"
-										required
-										placeholder={$i18n.t('Example: sAMAccountName or uid or userPrincipalName')}
-										bind:value={LDAP_SERVER.attribute_for_username}
-									/>
-								</Tooltip>
-							</div>
-						</div>
-						<div class="flex w-full gap-2">
-							<div class="w-full">
-								<div class=" self-center text-xs font-medium min-w-fit mb-1">
-									{$i18n.t('Search Base')}
+
+								<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
+									<!-- https://docs.openwebui.com/getting-started/advanced-topics/api-endpoints -->
+									<a
+										href="https://docs.openwebui.com/getting-started/api-endpoints"
+										target="_blank"
+										class=" text-gray-300 font-medium underline"
+									>
+										{$i18n.t('To learn more about available endpoints, visit our documentation.')}
+									</a>
 								</div>
 								</div>
-								<Tooltip content={$i18n.t('The base to search for users')} placement="top-start">
-									<input
-										class="w-full bg-transparent outline-none py-0.5"
-										required
-										placeholder={$i18n.t('Example: ou=users,dc=foo,dc=example')}
-										bind:value={LDAP_SERVER.search_base}
-									/>
-								</Tooltip>
 							</div>
 							</div>
+						{/if}
+					{/if}
+
+					<div class=" mb-2.5 w-full justify-between">
+						<div class="flex w-full justify-between">
+							<div class=" self-center text-xs font-medium">{$i18n.t('JWT Expiration')}</div>
 						</div>
 						</div>
-						<div class="flex w-full gap-2">
-							<div class="w-full">
-								<div class=" self-center text-xs font-medium min-w-fit mb-1">
-									{$i18n.t('Search Filters')}
-								</div>
-								<input
-									class="w-full bg-transparent outline-none py-0.5"
-									placeholder={$i18n.t('Example: (&(objectClass=inetOrgPerson)(uid=%s))')}
-									bind:value={LDAP_SERVER.search_filters}
-								/>
-							</div>
+
+						<div class="flex mt-2 space-x-2">
+							<input
+								class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
+								type="text"
+								placeholder={`e.g.) "30m","1h", "10d". `}
+								bind:value={adminConfig.JWT_EXPIRES_IN}
+							/>
 						</div>
 						</div>
-						<div class="text-xs text-gray-400 dark:text-gray-500">
-							<a
-								class=" text-gray-300 font-medium underline"
-								href="https://ldap.com/ldap-filters/"
-								target="_blank"
+
+						<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
+							{$i18n.t('Valid time units:')}
+							<span class=" text-gray-300 font-medium"
+								>{$i18n.t("'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.")}</span
 							>
 							>
-								{$i18n.t('Click here for filter guides.')}
-							</a>
 						</div>
 						</div>
-						<div>
+					</div>
+
+					<div class=" space-y-3">
+						<div class="mt-2 space-y-2 pr-1.5">
 							<div class="flex justify-between items-center text-sm">
 							<div class="flex justify-between items-center text-sm">
-								<div class="  font-medium">{$i18n.t('TLS')}</div>
+								<div class="  font-medium">{$i18n.t('LDAP')}</div>
 
 
 								<div class="mt-1">
 								<div class="mt-1">
-									<Switch bind:state={LDAP_SERVER.use_tls} />
+									<Switch
+										bind:state={ENABLE_LDAP}
+										on:change={async () => {
+											updateLdapConfig(localStorage.token, ENABLE_LDAP);
+										}}
+									/>
 								</div>
 								</div>
 							</div>
 							</div>
-							{#if LDAP_SERVER.use_tls}
-								<div class="flex w-full gap-2">
-									<div class="w-full">
-										<div class=" self-center text-xs font-medium min-w-fit mb-1 mt-1">
-											{$i18n.t('Certificate Path')}
+
+							{#if ENABLE_LDAP}
+								<div class="flex flex-col gap-1">
+									<div class="flex w-full gap-2">
+										<div class="w-full">
+											<div class=" self-center text-xs font-medium min-w-fit mb-1">
+												{$i18n.t('Label')}
+											</div>
+											<input
+												class="w-full bg-transparent outline-hidden py-0.5"
+												required
+												placeholder={$i18n.t('Enter server label')}
+												bind:value={LDAP_SERVER.label}
+											/>
 										</div>
 										</div>
-										<input
-											class="w-full bg-transparent outline-none py-0.5"
-											required
-											placeholder={$i18n.t('Enter certificate path')}
-											bind:value={LDAP_SERVER.certificate_path}
-										/>
+										<div class="w-full"></div>
 									</div>
 									</div>
-								</div>
-								<div class="flex w-full gap-2">
-									<div class="w-full">
-										<div class=" self-center text-xs font-medium min-w-fit mb-1">
-											{$i18n.t('Ciphers')}
+									<div class="flex w-full gap-2">
+										<div class="w-full">
+											<div class=" self-center text-xs font-medium min-w-fit mb-1">
+												{$i18n.t('Host')}
+											</div>
+											<input
+												class="w-full bg-transparent outline-hidden py-0.5"
+												required
+												placeholder={$i18n.t('Enter server host')}
+												bind:value={LDAP_SERVER.host}
+											/>
 										</div>
 										</div>
-										<Tooltip content={$i18n.t('Default to ALL')} placement="top-start">
+										<div class="w-full">
+											<div class=" self-center text-xs font-medium min-w-fit mb-1">
+												{$i18n.t('Port')}
+											</div>
+											<Tooltip
+												placement="top-start"
+												content={$i18n.t('Default to 389 or 636 if TLS is enabled')}
+												className="w-full"
+											>
+												<input
+													class="w-full bg-transparent outline-hidden py-0.5"
+													type="number"
+													placeholder={$i18n.t('Enter server port')}
+													bind:value={LDAP_SERVER.port}
+												/>
+											</Tooltip>
+										</div>
+									</div>
+									<div class="flex w-full gap-2">
+										<div class="w-full">
+											<div class=" self-center text-xs font-medium min-w-fit mb-1">
+												{$i18n.t('Application DN')}
+											</div>
+											<Tooltip
+												content={$i18n.t('The Application Account DN you bind with for search')}
+												placement="top-start"
+											>
+												<input
+													class="w-full bg-transparent outline-hidden py-0.5"
+													required
+													placeholder={$i18n.t('Enter Application DN')}
+													bind:value={LDAP_SERVER.app_dn}
+												/>
+											</Tooltip>
+										</div>
+										<div class="w-full">
+											<div class=" self-center text-xs font-medium min-w-fit mb-1">
+												{$i18n.t('Application DN Password')}
+											</div>
+											<SensitiveInput
+												placeholder={$i18n.t('Enter Application DN Password')}
+												bind:value={LDAP_SERVER.app_dn_password}
+											/>
+										</div>
+									</div>
+									<div class="flex w-full gap-2">
+										<div class="w-full">
+											<div class=" self-center text-xs font-medium min-w-fit mb-1">
+												{$i18n.t('Attribute for Mail')}
+											</div>
+											<Tooltip
+												content={$i18n.t(
+													'The LDAP attribute that maps to the mail that users use to sign in.'
+												)}
+												placement="top-start"
+											>
+												<input
+													class="w-full bg-transparent outline-hidden py-0.5"
+													required
+													placeholder={$i18n.t('Example: mail')}
+													bind:value={LDAP_SERVER.attribute_for_mail}
+												/>
+											</Tooltip>
+										</div>
+									</div>
+									<div class="flex w-full gap-2">
+										<div class="w-full">
+											<div class=" self-center text-xs font-medium min-w-fit mb-1">
+												{$i18n.t('Attribute for Username')}
+											</div>
+											<Tooltip
+												content={$i18n.t(
+													'The LDAP attribute that maps to the username that users use to sign in.'
+												)}
+												placement="top-start"
+											>
+												<input
+													class="w-full bg-transparent outline-hidden py-0.5"
+													required
+													placeholder={$i18n.t(
+														'Example: sAMAccountName or uid or userPrincipalName'
+													)}
+													bind:value={LDAP_SERVER.attribute_for_username}
+												/>
+											</Tooltip>
+										</div>
+									</div>
+									<div class="flex w-full gap-2">
+										<div class="w-full">
+											<div class=" self-center text-xs font-medium min-w-fit mb-1">
+												{$i18n.t('Search Base')}
+											</div>
+											<Tooltip
+												content={$i18n.t('The base to search for users')}
+												placement="top-start"
+											>
+												<input
+													class="w-full bg-transparent outline-hidden py-0.5"
+													required
+													placeholder={$i18n.t('Example: ou=users,dc=foo,dc=example')}
+													bind:value={LDAP_SERVER.search_base}
+												/>
+											</Tooltip>
+										</div>
+									</div>
+									<div class="flex w-full gap-2">
+										<div class="w-full">
+											<div class=" self-center text-xs font-medium min-w-fit mb-1">
+												{$i18n.t('Search Filters')}
+											</div>
 											<input
 											<input
-												class="w-full bg-transparent outline-none py-0.5"
-												placeholder={$i18n.t('Example: ALL')}
-												bind:value={LDAP_SERVER.ciphers}
+												class="w-full bg-transparent outline-hidden py-0.5"
+												placeholder={$i18n.t('Example: (&(objectClass=inetOrgPerson)(uid=%s))')}
+												bind:value={LDAP_SERVER.search_filters}
 											/>
 											/>
-										</Tooltip>
+										</div>
+									</div>
+									<div class="text-xs text-gray-400 dark:text-gray-500">
+										<a
+											class=" text-gray-300 font-medium underline"
+											href="https://ldap.com/ldap-filters/"
+											target="_blank"
+										>
+											{$i18n.t('Click here for filter guides.')}
+										</a>
+									</div>
+									<div>
+										<div class="flex justify-between items-center text-sm">
+											<div class="  font-medium">{$i18n.t('TLS')}</div>
+
+											<div class="mt-1">
+												<Switch bind:state={LDAP_SERVER.use_tls} />
+											</div>
+										</div>
+										{#if LDAP_SERVER.use_tls}
+											<div class="flex w-full gap-2">
+												<div class="w-full">
+													<div class=" self-center text-xs font-medium min-w-fit mb-1 mt-1">
+														{$i18n.t('Certificate Path')}
+													</div>
+													<input
+														class="w-full bg-transparent outline-hidden py-0.5"
+														required
+														placeholder={$i18n.t('Enter certificate path')}
+														bind:value={LDAP_SERVER.certificate_path}
+													/>
+												</div>
+											</div>
+											<div class="flex w-full gap-2">
+												<div class="w-full">
+													<div class=" self-center text-xs font-medium min-w-fit mb-1">
+														{$i18n.t('Ciphers')}
+													</div>
+													<Tooltip content={$i18n.t('Default to ALL')} placement="top-start">
+														<input
+															class="w-full bg-transparent outline-hidden py-0.5"
+															placeholder={$i18n.t('Example: ALL')}
+															bind:value={LDAP_SERVER.ciphers}
+														/>
+													</Tooltip>
+												</div>
+												<div class="w-full"></div>
+											</div>
+										{/if}
 									</div>
 									</div>
-									<div class="w-full"></div>
 								</div>
 								</div>
 							{/if}
 							{/if}
 						</div>
 						</div>
 					</div>
 					</div>
-				{/if}
+				</div>
+
+				<div class="mb-3">
+					<div class=" mb-2.5 text-base font-medium">{$i18n.t('Features')}</div>
+
+					<hr class=" border-gray-100 dark:border-gray-850 my-2" />
+
+					<div class="mb-2.5 flex w-full items-center justify-between pr-2">
+						<div class=" self-center text-xs font-medium">
+							{$i18n.t('Enable Community Sharing')}
+						</div>
+
+						<Switch bind:state={adminConfig.ENABLE_COMMUNITY_SHARING} />
+					</div>
+
+					<div class="mb-2.5 flex w-full items-center justify-between pr-2">
+						<div class=" self-center text-xs font-medium">{$i18n.t('Enable Message Rating')}</div>
+
+						<Switch bind:state={adminConfig.ENABLE_MESSAGE_RATING} />
+					</div>
+
+					<div class="mb-2.5 flex w-full items-center justify-between pr-2">
+						<div class=" self-center text-xs font-medium">
+							{$i18n.t('Channels')} ({$i18n.t('Beta')})
+						</div>
+
+						<Switch bind:state={adminConfig.ENABLE_CHANNELS} />
+					</div>
+
+					<div class="mb-2.5 w-full justify-between">
+						<div class="flex w-full justify-between">
+							<div class=" self-center text-xs font-medium">{$i18n.t('WebUI URL')}</div>
+						</div>
+
+						<div class="flex mt-2 space-x-2">
+							<input
+								class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
+								type="text"
+								placeholder={`e.g.) "http://localhost:3000"`}
+								bind:value={adminConfig.WEBUI_URL}
+							/>
+						</div>
+
+						<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
+							{$i18n.t(
+								'Enter the public URL of your WebUI. This URL will be used to generate links in the notifications.'
+							)}
+						</div>
+					</div>
+
+					<div class=" w-full justify-between">
+						<div class="flex w-full justify-between">
+							<div class=" self-center text-xs font-medium">{$i18n.t('Webhook URL')}</div>
+						</div>
+
+						<div class="flex mt-2 space-x-2">
+							<input
+								class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
+								type="text"
+								placeholder={`https://example.com/webhook`}
+								bind:value={webhookUrl}
+							/>
+						</div>
+					</div>
+				</div>
 			</div>
 			</div>
-		</div>
+		{/if}
 	</div>
 	</div>
 
 
 	<div class="flex justify-end pt-3 text-sm font-medium">
 	<div class="flex justify-end pt-3 text-sm font-medium">

+ 39 - 17
src/lib/components/admin/Settings/Images.svelte

@@ -261,6 +261,9 @@
 										} else if (config.engine === 'openai' && config.openai.OPENAI_API_KEY === '') {
 										} else if (config.engine === 'openai' && config.openai.OPENAI_API_KEY === '') {
 											toast.error($i18n.t('OpenAI API Key is required.'));
 											toast.error($i18n.t('OpenAI API Key is required.'));
 											config.enabled = false;
 											config.enabled = false;
+										} else if (config.engine === 'gemini' && config.gemini.GEMINI_API_KEY === '') {
+											toast.error($i18n.t('Gemini API Key is required.'));
+											config.enabled = false;
 										}
 										}
 									}
 									}
 
 
@@ -284,7 +287,7 @@
 					<div class=" self-center text-xs font-medium">{$i18n.t('Image Generation Engine')}</div>
 					<div class=" self-center text-xs font-medium">{$i18n.t('Image Generation Engine')}</div>
 					<div class="flex items-center relative">
 					<div class="flex items-center relative">
 						<select
 						<select
-							class=" dark:bg-gray-900 w-fit pr-8 cursor-pointer rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
+							class=" dark:bg-gray-900 w-fit pr-8 cursor-pointer rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right"
 							bind:value={config.engine}
 							bind:value={config.engine}
 							placeholder={$i18n.t('Select Engine')}
 							placeholder={$i18n.t('Select Engine')}
 							on:change={async () => {
 							on:change={async () => {
@@ -294,11 +297,12 @@
 							<option value="openai">{$i18n.t('Default (Open AI)')}</option>
 							<option value="openai">{$i18n.t('Default (Open AI)')}</option>
 							<option value="comfyui">{$i18n.t('ComfyUI')}</option>
 							<option value="comfyui">{$i18n.t('ComfyUI')}</option>
 							<option value="automatic1111">{$i18n.t('Automatic1111')}</option>
 							<option value="automatic1111">{$i18n.t('Automatic1111')}</option>
+							<option value="gemini">{$i18n.t('Gemini')}</option>
 						</select>
 						</select>
 					</div>
 					</div>
 				</div>
 				</div>
 			</div>
 			</div>
-			<hr class=" dark:border-gray-850" />
+			<hr class=" border-gray-100 dark:border-gray-850" />
 
 
 			<div class="flex flex-col gap-2">
 			<div class="flex flex-col gap-2">
 				{#if (config?.engine ?? 'automatic1111') === 'automatic1111'}
 				{#if (config?.engine ?? 'automatic1111') === 'automatic1111'}
@@ -307,7 +311,7 @@
 						<div class="flex w-full">
 						<div class="flex w-full">
 							<div class="flex-1 mr-2">
 							<div class="flex-1 mr-2">
 								<input
 								<input
-									class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+									class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
 									placeholder={$i18n.t('Enter URL (e.g. http://127.0.0.1:7860/)')}
 									placeholder={$i18n.t('Enter URL (e.g. http://127.0.0.1:7860/)')}
 									bind:value={config.automatic1111.AUTOMATIC1111_BASE_URL}
 									bind:value={config.automatic1111.AUTOMATIC1111_BASE_URL}
 								/>
 								/>
@@ -386,7 +390,7 @@
 								<Tooltip content={$i18n.t('Enter Sampler (e.g. Euler a)')} placement="top-start">
 								<Tooltip content={$i18n.t('Enter Sampler (e.g. Euler a)')} placement="top-start">
 									<input
 									<input
 										list="sampler-list"
 										list="sampler-list"
-										class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+										class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
 										placeholder={$i18n.t('Enter Sampler (e.g. Euler a)')}
 										placeholder={$i18n.t('Enter Sampler (e.g. Euler a)')}
 										bind:value={config.automatic1111.AUTOMATIC1111_SAMPLER}
 										bind:value={config.automatic1111.AUTOMATIC1111_SAMPLER}
 									/>
 									/>
@@ -408,7 +412,7 @@
 								<Tooltip content={$i18n.t('Enter Scheduler (e.g. Karras)')} placement="top-start">
 								<Tooltip content={$i18n.t('Enter Scheduler (e.g. Karras)')} placement="top-start">
 									<input
 									<input
 										list="scheduler-list"
 										list="scheduler-list"
-										class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+										class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
 										placeholder={$i18n.t('Enter Scheduler (e.g. Karras)')}
 										placeholder={$i18n.t('Enter Scheduler (e.g. Karras)')}
 										bind:value={config.automatic1111.AUTOMATIC1111_SCHEDULER}
 										bind:value={config.automatic1111.AUTOMATIC1111_SCHEDULER}
 									/>
 									/>
@@ -429,7 +433,7 @@
 							<div class="flex-1 mr-2">
 							<div class="flex-1 mr-2">
 								<Tooltip content={$i18n.t('Enter CFG Scale (e.g. 7.0)')} placement="top-start">
 								<Tooltip content={$i18n.t('Enter CFG Scale (e.g. 7.0)')} placement="top-start">
 									<input
 									<input
-										class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+										class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
 										placeholder={$i18n.t('Enter CFG Scale (e.g. 7.0)')}
 										placeholder={$i18n.t('Enter CFG Scale (e.g. 7.0)')}
 										bind:value={config.automatic1111.AUTOMATIC1111_CFG_SCALE}
 										bind:value={config.automatic1111.AUTOMATIC1111_CFG_SCALE}
 									/>
 									/>
@@ -443,7 +447,7 @@
 						<div class="flex w-full">
 						<div class="flex w-full">
 							<div class="flex-1 mr-2">
 							<div class="flex-1 mr-2">
 								<input
 								<input
-									class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+									class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
 									placeholder={$i18n.t('Enter URL (e.g. http://127.0.0.1:7860/)')}
 									placeholder={$i18n.t('Enter URL (e.g. http://127.0.0.1:7860/)')}
 									bind:value={config.comfyui.COMFYUI_BASE_URL}
 									bind:value={config.comfyui.COMFYUI_BASE_URL}
 								/>
 								/>
@@ -497,7 +501,7 @@
 
 
 						{#if config.comfyui.COMFYUI_WORKFLOW}
 						{#if config.comfyui.COMFYUI_WORKFLOW}
 							<textarea
 							<textarea
-								class="w-full rounded-lg mb-1 py-2 px-4 text-xs bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none disabled:text-gray-600 resize-none"
+								class="w-full rounded-lg mb-1 py-2 px-4 text-xs bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden disabled:text-gray-600 resize-none"
 								rows="10"
 								rows="10"
 								bind:value={config.comfyui.COMFYUI_WORKFLOW}
 								bind:value={config.comfyui.COMFYUI_WORKFLOW}
 								required
 								required
@@ -525,7 +529,7 @@
 								/>
 								/>
 
 
 								<button
 								<button
-									class="w-full text-sm font-medium py-2 bg-transparent hover:bg-gray-100 border border-dashed dark:border-gray-800 dark:hover:bg-gray-850 text-center rounded-xl"
+									class="w-full text-sm font-medium py-2 bg-transparent hover:bg-gray-100 border border-dashed dark:border-gray-850 dark:hover:bg-gray-850 text-center rounded-xl"
 									type="button"
 									type="button"
 									on:click={() => {
 									on:click={() => {
 										document.getElementById('upload-comfyui-workflow-input')?.click();
 										document.getElementById('upload-comfyui-workflow-input')?.click();
@@ -548,7 +552,7 @@
 							<div class="text-xs flex flex-col gap-1.5">
 							<div class="text-xs flex flex-col gap-1.5">
 								{#each requiredWorkflowNodes as node}
 								{#each requiredWorkflowNodes as node}
 									<div class="flex w-full items-center border dark:border-gray-850 rounded-lg">
 									<div class="flex w-full items-center border dark:border-gray-850 rounded-lg">
-										<div class="flex-shrink-0">
+										<div class="shrink-0">
 											<div
 											<div
 												class=" capitalize line-clamp-1 font-medium px-3 py-1 w-20 text-center rounded-l-lg bg-green-500/10 text-green-700 dark:text-green-200"
 												class=" capitalize line-clamp-1 font-medium px-3 py-1 w-20 text-center rounded-l-lg bg-green-500/10 text-green-700 dark:text-green-200"
 											>
 											>
@@ -558,7 +562,7 @@
 										<div class="">
 										<div class="">
 											<Tooltip content="Input Key (e.g. text, unet_name, steps)">
 											<Tooltip content="Input Key (e.g. text, unet_name, steps)">
 												<input
 												<input
-													class="py-1 px-3 w-24 text-xs text-center bg-transparent outline-none border-r dark:border-gray-850"
+													class="py-1 px-3 w-24 text-xs text-center bg-transparent outline-hidden border-r dark:border-gray-850"
 													placeholder="Key"
 													placeholder="Key"
 													bind:value={node.key}
 													bind:value={node.key}
 													required
 													required
@@ -572,7 +576,7 @@
 												placement="top-start"
 												placement="top-start"
 											>
 											>
 												<input
 												<input
-													class="w-full py-1 px-4 rounded-r-lg text-xs bg-transparent outline-none"
+													class="w-full py-1 px-4 rounded-r-lg text-xs bg-transparent outline-hidden"
 													placeholder="Node Ids"
 													placeholder="Node Ids"
 													bind:value={node.node_ids}
 													bind:value={node.node_ids}
 												/>
 												/>
@@ -593,7 +597,7 @@
 
 
 						<div class="flex gap-2 mb-1">
 						<div class="flex gap-2 mb-1">
 							<input
 							<input
-								class="flex-1 w-full text-sm bg-transparent outline-none"
+								class="flex-1 w-full text-sm bg-transparent outline-hidden"
 								placeholder={$i18n.t('API Base URL')}
 								placeholder={$i18n.t('API Base URL')}
 								bind:value={config.openai.OPENAI_API_BASE_URL}
 								bind:value={config.openai.OPENAI_API_BASE_URL}
 								required
 								required
@@ -605,11 +609,29 @@
 							/>
 							/>
 						</div>
 						</div>
 					</div>
 					</div>
+				{:else if config?.engine === 'gemini'}
+					<div>
+						<div class=" mb-1.5 text-sm font-medium">{$i18n.t('Gemini API Config')}</div>
+
+						<div class="flex gap-2 mb-1">
+							<input
+								class="flex-1 w-full text-sm bg-transparent outline-none"
+								placeholder={$i18n.t('API Base URL')}
+								bind:value={config.gemini.GEMINI_API_BASE_URL}
+								required
+							/>
+
+							<SensitiveInput
+								placeholder={$i18n.t('API Key')}
+								bind:value={config.gemini.GEMINI_API_KEY}
+							/>
+						</div>
+					</div>
 				{/if}
 				{/if}
 			</div>
 			</div>
 
 
 			{#if config?.enabled}
 			{#if config?.enabled}
-				<hr class=" dark:border-gray-850" />
+				<hr class=" border-gray-100 dark:border-gray-850" />
 
 
 				<div>
 				<div>
 					<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Default Model')}</div>
 					<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Default Model')}</div>
@@ -620,7 +642,7 @@
 									<Tooltip content={$i18n.t('Enter Model ID')} placement="top-start">
 									<Tooltip content={$i18n.t('Enter Model ID')} placement="top-start">
 										<input
 										<input
 											list="model-list"
 											list="model-list"
-											class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+											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={imageGenerationConfig.MODEL}
 											bind:value={imageGenerationConfig.MODEL}
 											placeholder="Select a model"
 											placeholder="Select a model"
 											required
 											required
@@ -644,7 +666,7 @@
 						<div class="flex-1 mr-2">
 						<div class="flex-1 mr-2">
 							<Tooltip content={$i18n.t('Enter Image Size (e.g. 512x512)')} placement="top-start">
 							<Tooltip content={$i18n.t('Enter Image Size (e.g. 512x512)')} placement="top-start">
 								<input
 								<input
-									class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+									class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
 									placeholder={$i18n.t('Enter Image Size (e.g. 512x512)')}
 									placeholder={$i18n.t('Enter Image Size (e.g. 512x512)')}
 									bind:value={imageGenerationConfig.IMAGE_SIZE}
 									bind:value={imageGenerationConfig.IMAGE_SIZE}
 									required
 									required
@@ -660,7 +682,7 @@
 						<div class="flex-1 mr-2">
 						<div class="flex-1 mr-2">
 							<Tooltip content={$i18n.t('Enter Number of Steps (e.g. 50)')} placement="top-start">
 							<Tooltip content={$i18n.t('Enter Number of Steps (e.g. 50)')} placement="top-start">
 								<input
 								<input
-									class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+									class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
 									placeholder={$i18n.t('Enter Number of Steps (e.g. 50)')}
 									placeholder={$i18n.t('Enter Number of Steps (e.g. 50)')}
 									bind:value={imageGenerationConfig.IMAGE_STEPS}
 									bind:value={imageGenerationConfig.IMAGE_STEPS}
 									required
 									required

+ 228 - 207
src/lib/components/admin/Settings/Interface.svelte

@@ -23,6 +23,7 @@
 	let taskConfig = {
 	let taskConfig = {
 		TASK_MODEL: '',
 		TASK_MODEL: '',
 		TASK_MODEL_EXTERNAL: '',
 		TASK_MODEL_EXTERNAL: '',
+		ENABLE_TITLE_GENERATION: true,
 		TITLE_GENERATION_PROMPT_TEMPLATE: '',
 		TITLE_GENERATION_PROMPT_TEMPLATE: '',
 		IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE: '',
 		IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE: '',
 		ENABLE_AUTOCOMPLETE_GENERATION: true,
 		ENABLE_AUTOCOMPLETE_GENERATION: true,
@@ -50,7 +51,7 @@
 	onMount(async () => {
 	onMount(async () => {
 		taskConfig = await getTaskConfig(localStorage.token);
 		taskConfig = await getTaskConfig(localStorage.token);
 
 
-		promptSuggestions = $config?.default_prompt_suggestions;
+		promptSuggestions = $config?.default_prompt_suggestions ?? [];
 		banners = await getBanners(localStorage.token);
 		banners = await getBanners(localStorage.token);
 	});
 	});
 
 
@@ -68,9 +69,13 @@
 		}}
 		}}
 	>
 	>
 		<div class="  overflow-y-scroll scrollbar-hidden h-full pr-1.5">
 		<div class="  overflow-y-scroll scrollbar-hidden h-full pr-1.5">
-			<div>
-				<div class=" mb-2.5 text-sm font-medium flex items-center">
-					<div class=" mr-1">{$i18n.t('Set Task Model')}</div>
+			<div class="mb-3.5">
+				<div class=" mb-2.5 text-base font-medium">{$i18n.t('Tasks')}</div>
+
+				<hr class=" border-gray-100 dark:border-gray-850 my-2" />
+
+				<div class=" mb-1 font-medium flex items-center">
+					<div class=" text-xs mr-1">{$i18n.t('Set Task Model')}</div>
 					<Tooltip
 					<Tooltip
 						content={$i18n.t(
 						content={$i18n.t(
 							'A task model is used when performing tasks such as generating titles for chats and web search queries'
 							'A task model is used when performing tasks such as generating titles for chats and web search queries'
@@ -92,11 +97,12 @@
 						</svg>
 						</svg>
 					</Tooltip>
 					</Tooltip>
 				</div>
 				</div>
-				<div class="flex w-full gap-2">
+
+				<div class=" mb-2.5 flex w-full gap-2">
 					<div class="flex-1">
 					<div class="flex-1">
 						<div class=" text-xs mb-1">{$i18n.t('Local Models')}</div>
 						<div class=" text-xs mb-1">{$i18n.t('Local Models')}</div>
 						<select
 						<select
-							class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+							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={taskConfig.TASK_MODEL}
 							bind:value={taskConfig.TASK_MODEL}
 							placeholder={$i18n.t('Select a model')}
 							placeholder={$i18n.t('Select a model')}
 						>
 						>
@@ -112,7 +118,7 @@
 					<div class="flex-1">
 					<div class="flex-1">
 						<div class=" text-xs mb-1">{$i18n.t('External Models')}</div>
 						<div class=" text-xs mb-1">{$i18n.t('External Models')}</div>
 						<select
 						<select
-							class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+							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={taskConfig.TASK_MODEL_EXTERNAL}
 							bind:value={taskConfig.TASK_MODEL_EXTERNAL}
 							placeholder={$i18n.t('Select a model')}
 							placeholder={$i18n.t('Select a model')}
 						>
 						>
@@ -126,72 +132,33 @@
 					</div>
 					</div>
 				</div>
 				</div>
 
 
-				<div class="mt-3">
-					<div class=" mb-2.5 text-xs font-medium">{$i18n.t('Title Generation Prompt')}</div>
-
-					<Tooltip
-						content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
-						placement="top-start"
-					>
-						<Textarea
-							bind:value={taskConfig.TITLE_GENERATION_PROMPT_TEMPLATE}
-							placeholder={$i18n.t(
-								'Leave empty to use the default prompt, or enter a custom prompt'
-							)}
-						/>
-					</Tooltip>
-				</div>
-
-				<div class="mt-3">
-					<div class=" mb-2.5 text-xs font-medium">{$i18n.t('Image Prompt Generation Prompt')}</div>
-
-					<Tooltip
-						content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
-						placement="top-start"
-					>
-						<Textarea
-							bind:value={taskConfig.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE}
-							placeholder={$i18n.t(
-								'Leave empty to use the default prompt, or enter a custom prompt'
-							)}
-						/>
-					</Tooltip>
-				</div>
-
-				<hr class=" border-gray-50 dark:border-gray-850 my-3" />
-
-				<div class="my-3 flex w-full items-center justify-between">
+				<div class="mb-2.5 flex w-full items-center justify-between">
 					<div class=" self-center text-xs font-medium">
 					<div class=" self-center text-xs font-medium">
-						{$i18n.t('Autocomplete Generation')}
+						{$i18n.t('Title Generation')}
 					</div>
 					</div>
 
 
-					<Tooltip content={$i18n.t('Enable autocomplete generation for chat messages')}>
-						<Switch bind:state={taskConfig.ENABLE_AUTOCOMPLETE_GENERATION} />
-					</Tooltip>
+					<Switch bind:state={taskConfig.ENABLE_TITLE_GENERATION} />
 				</div>
 				</div>
 
 
-				{#if taskConfig.ENABLE_AUTOCOMPLETE_GENERATION}
-					<div class="mt-3">
-						<div class=" mb-2.5 text-xs font-medium">
-							{$i18n.t('Autocomplete Generation Input Max Length')}
-						</div>
+				{#if taskConfig.ENABLE_TITLE_GENERATION}
+					<div class="mb-2.5">
+						<div class=" mb-1 text-xs font-medium">{$i18n.t('Title Generation Prompt')}</div>
 
 
 						<Tooltip
 						<Tooltip
-							content={$i18n.t('Character limit for autocomplete generation input')}
+							content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
 							placement="top-start"
 							placement="top-start"
 						>
 						>
-							<input
-								class="w-full outline-none bg-transparent"
-								bind:value={taskConfig.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH}
-								placeholder={$i18n.t('-1 for no limit, or a positive integer for a specific limit')}
+							<Textarea
+								bind:value={taskConfig.TITLE_GENERATION_PROMPT_TEMPLATE}
+								placeholder={$i18n.t(
+									'Leave empty to use the default prompt, or enter a custom prompt'
+								)}
 							/>
 							/>
 						</Tooltip>
 						</Tooltip>
 					</div>
 					</div>
 				{/if}
 				{/if}
 
 
-				<hr class=" border-gray-50 dark:border-gray-850 my-3" />
-
-				<div class="my-3 flex w-full items-center justify-between">
+				<div class="mb-2.5 flex w-full items-center justify-between">
 					<div class=" self-center text-xs font-medium">
 					<div class=" self-center text-xs font-medium">
 						{$i18n.t('Tags Generation')}
 						{$i18n.t('Tags Generation')}
 					</div>
 					</div>
@@ -200,8 +167,8 @@
 				</div>
 				</div>
 
 
 				{#if taskConfig.ENABLE_TAGS_GENERATION}
 				{#if taskConfig.ENABLE_TAGS_GENERATION}
-					<div class="mt-3">
-						<div class=" mb-2.5 text-xs font-medium">{$i18n.t('Tags Generation Prompt')}</div>
+					<div class="mb-2.5">
+						<div class=" mb-1 text-xs font-medium">{$i18n.t('Tags Generation Prompt')}</div>
 
 
 						<Tooltip
 						<Tooltip
 							content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
 							content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
@@ -217,9 +184,7 @@
 					</div>
 					</div>
 				{/if}
 				{/if}
 
 
-				<hr class=" border-gray-50 dark:border-gray-850 my-3" />
-
-				<div class="my-3 flex w-full items-center justify-between">
+				<div class="mb-2.5 flex w-full items-center justify-between">
 					<div class=" self-center text-xs font-medium">
 					<div class=" self-center text-xs font-medium">
 						{$i18n.t('Retrieval Query Generation')}
 						{$i18n.t('Retrieval Query Generation')}
 					</div>
 					</div>
@@ -227,7 +192,7 @@
 					<Switch bind:state={taskConfig.ENABLE_RETRIEVAL_QUERY_GENERATION} />
 					<Switch bind:state={taskConfig.ENABLE_RETRIEVAL_QUERY_GENERATION} />
 				</div>
 				</div>
 
 
-				<div class="my-3 flex w-full items-center justify-between">
+				<div class="mb-2.5 flex w-full items-center justify-between">
 					<div class=" self-center text-xs font-medium">
 					<div class=" self-center text-xs font-medium">
 						{$i18n.t('Web Search Query Generation')}
 						{$i18n.t('Web Search Query Generation')}
 					</div>
 					</div>
@@ -235,8 +200,8 @@
 					<Switch bind:state={taskConfig.ENABLE_SEARCH_QUERY_GENERATION} />
 					<Switch bind:state={taskConfig.ENABLE_SEARCH_QUERY_GENERATION} />
 				</div>
 				</div>
 
 
-				<div class="">
-					<div class=" mb-2.5 text-xs font-medium">{$i18n.t('Query Generation Prompt')}</div>
+				<div class="mb-2.5">
+					<div class=" mb-1 text-xs font-medium">{$i18n.t('Query Generation Prompt')}</div>
 
 
 					<Tooltip
 					<Tooltip
 						content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
 						content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
@@ -250,131 +215,96 @@
 						/>
 						/>
 					</Tooltip>
 					</Tooltip>
 				</div>
 				</div>
-			</div>
 
 
-			<div class="mt-3">
-				<div class=" mb-2.5 text-xs font-medium">{$i18n.t('Tools Function Calling Prompt')}</div>
-
-				<Tooltip
-					content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
-					placement="top-start"
-				>
-					<Textarea
-						bind:value={taskConfig.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE}
-						placeholder={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
-					/>
-				</Tooltip>
-			</div>
+				<div class="mb-2.5 flex w-full items-center justify-between">
+					<div class=" self-center text-xs font-medium">
+						{$i18n.t('Autocomplete Generation')}
+					</div>
 
 
-			<hr class=" border-gray-50 dark:border-gray-850 my-3" />
+					<Tooltip content={$i18n.t('Enable autocomplete generation for chat messages')}>
+						<Switch bind:state={taskConfig.ENABLE_AUTOCOMPLETE_GENERATION} />
+					</Tooltip>
+				</div>
 
 
-			<div class=" space-y-3 {banners.length > 0 ? ' mb-3' : ''}">
-				<div class="flex w-full justify-between">
-					<div class=" self-center text-sm font-semibold">
-						{$i18n.t('Banners')}
-					</div>
+				{#if taskConfig.ENABLE_AUTOCOMPLETE_GENERATION}
+					<div class="mb-2.5">
+						<div class=" mb-1 text-xs font-medium">
+							{$i18n.t('Autocomplete Generation Input Max Length')}
+						</div>
 
 
-					<button
-						class="p-1 px-3 text-xs flex rounded transition"
-						type="button"
-						on:click={() => {
-							if (banners.length === 0 || banners.at(-1).content !== '') {
-								banners = [
-									...banners,
-									{
-										id: uuidv4(),
-										type: '',
-										title: '',
-										content: '',
-										dismissible: true,
-										timestamp: Math.floor(Date.now() / 1000)
-									}
-								];
-							}
-						}}
-					>
-						<svg
-							xmlns="http://www.w3.org/2000/svg"
-							viewBox="0 0 20 20"
-							fill="currentColor"
-							class="w-4 h-4"
+						<Tooltip
+							content={$i18n.t('Character limit for autocomplete generation input')}
+							placement="top-start"
 						>
 						>
-							<path
-								d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z"
+							<input
+								class="w-full outline-hidden bg-transparent"
+								bind:value={taskConfig.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH}
+								placeholder={$i18n.t('-1 for no limit, or a positive integer for a specific limit')}
 							/>
 							/>
-						</svg>
-					</button>
+						</Tooltip>
+					</div>
+				{/if}
+
+				<div class="mb-2.5">
+					<div class=" mb-1 text-xs font-medium">{$i18n.t('Image Prompt Generation Prompt')}</div>
+
+					<Tooltip
+						content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
+						placement="top-start"
+					>
+						<Textarea
+							bind:value={taskConfig.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE}
+							placeholder={$i18n.t(
+								'Leave empty to use the default prompt, or enter a custom prompt'
+							)}
+						/>
+					</Tooltip>
 				</div>
 				</div>
-				<div class="flex flex-col space-y-1">
-					{#each banners as banner, bannerIdx}
-						<div class=" flex justify-between">
-							<div class="flex flex-row flex-1 border rounded-xl dark:border-gray-800">
-								<select
-									class="w-fit capitalize rounded-xl py-2 px-4 text-xs bg-transparent outline-none"
-									bind:value={banner.type}
-									required
-								>
-									{#if banner.type == ''}
-										<option value="" selected disabled class="text-gray-900"
-											>{$i18n.t('Type')}</option
-										>
-									{/if}
-									<option value="info" class="text-gray-900">{$i18n.t('Info')}</option>
-									<option value="warning" class="text-gray-900">{$i18n.t('Warning')}</option>
-									<option value="error" class="text-gray-900">{$i18n.t('Error')}</option>
-									<option value="success" class="text-gray-900">{$i18n.t('Success')}</option>
-								</select>
-
-								<input
-									class="pr-5 py-1.5 text-xs w-full bg-transparent outline-none"
-									placeholder={$i18n.t('Content')}
-									bind:value={banner.content}
-								/>
 
 
-								<div class="relative top-1.5 -left-2">
-									<Tooltip content={$i18n.t('Dismissible')} className="flex h-fit items-center">
-										<Switch bind:state={banner.dismissible} />
-									</Tooltip>
-								</div>
-							</div>
+				<div class="mb-2.5">
+					<div class=" mb-1 text-xs font-medium">{$i18n.t('Tools Function Calling Prompt')}</div>
 
 
-							<button
-								class="px-2"
-								type="button"
-								on:click={() => {
-									banners.splice(bannerIdx, 1);
-									banners = banners;
-								}}
-							>
-								<svg
-									xmlns="http://www.w3.org/2000/svg"
-									viewBox="0 0 20 20"
-									fill="currentColor"
-									class="w-4 h-4"
-								>
-									<path
-										d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
-									/>
-								</svg>
-							</button>
-						</div>
-					{/each}
+					<Tooltip
+						content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
+						placement="top-start"
+					>
+						<Textarea
+							bind:value={taskConfig.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE}
+							placeholder={$i18n.t(
+								'Leave empty to use the default prompt, or enter a custom prompt'
+							)}
+						/>
+					</Tooltip>
 				</div>
 				</div>
 			</div>
 			</div>
 
 
-			{#if $user.role === 'admin'}
-				<div class=" space-y-3">
-					<div class="flex w-full justify-between mb-2">
+			<div class="mb-3.5">
+				<div class=" mb-2.5 text-base font-medium">{$i18n.t('UI')}</div>
+
+				<hr class=" border-gray-100 dark:border-gray-850 my-2" />
+
+				<div class="  {banners.length > 0 ? ' mb-3' : ''}">
+					<div class="mb-2.5 flex w-full justify-between">
 						<div class=" self-center text-sm font-semibold">
 						<div class=" self-center text-sm font-semibold">
-							{$i18n.t('Default Prompt Suggestions')}
+							{$i18n.t('Banners')}
 						</div>
 						</div>
 
 
 						<button
 						<button
-							class="p-1 px-3 text-xs flex rounded transition"
+							class="p-1 px-3 text-xs flex rounded-sm transition"
 							type="button"
 							type="button"
 							on:click={() => {
 							on:click={() => {
-								if (promptSuggestions.length === 0 || promptSuggestions.at(-1).content !== '') {
-									promptSuggestions = [...promptSuggestions, { content: '', title: ['', ''] }];
+								if (banners.length === 0 || banners.at(-1).content !== '') {
+									banners = [
+										...banners,
+										{
+											id: uuidv4(),
+											type: '',
+											title: '',
+											content: '',
+											dismissible: true,
+											timestamp: Math.floor(Date.now() / 1000)
+										}
+									];
 								}
 								}
 							}}
 							}}
 						>
 						>
@@ -390,40 +320,48 @@
 							</svg>
 							</svg>
 						</button>
 						</button>
 					</div>
 					</div>
-					<div class="grid lg:grid-cols-2 flex-col gap-1.5">
-						{#each promptSuggestions as prompt, promptIdx}
-							<div
-								class=" flex border border-gray-100 dark:border-none dark:bg-gray-850 rounded-xl py-1.5"
-							>
-								<div class="flex flex-col flex-1 pl-1">
-									<div class="flex border-b border-gray-100 dark:border-gray-800 w-full">
-										<input
-											class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r border-gray-100 dark:border-gray-800"
-											placeholder={$i18n.t('Title (e.g. Tell me a fun fact)')}
-											bind:value={prompt.title[0]}
-										/>
 
 
-										<input
-											class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r border-gray-100 dark:border-gray-800"
-											placeholder={$i18n.t('Subtitle (e.g. about the Roman Empire)')}
-											bind:value={prompt.title[1]}
-										/>
-									</div>
-
-									<textarea
-										class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r border-gray-100 dark:border-gray-800 resize-none"
-										placeholder={$i18n.t('Prompt (e.g. Tell me a fun fact about the Roman Empire)')}
-										rows="3"
-										bind:value={prompt.content}
+					<div class=" flex flex-col space-y-1">
+						{#each banners as banner, bannerIdx}
+							<div class=" flex justify-between">
+								<div
+									class="flex flex-row flex-1 border rounded-xl border-gray-100 dark:border-gray-850"
+								>
+									<select
+										class="w-fit capitalize rounded-xl py-2 px-4 text-xs bg-transparent outline-hidden"
+										bind:value={banner.type}
+										required
+									>
+										{#if banner.type == ''}
+											<option value="" selected disabled class="text-gray-900"
+												>{$i18n.t('Type')}</option
+											>
+										{/if}
+										<option value="info" class="text-gray-900">{$i18n.t('Info')}</option>
+										<option value="warning" class="text-gray-900">{$i18n.t('Warning')}</option>
+										<option value="error" class="text-gray-900">{$i18n.t('Error')}</option>
+										<option value="success" class="text-gray-900">{$i18n.t('Success')}</option>
+									</select>
+
+									<input
+										class="pr-5 py-1.5 text-xs w-full bg-transparent outline-hidden"
+										placeholder={$i18n.t('Content')}
+										bind:value={banner.content}
 									/>
 									/>
+
+									<div class="relative top-1.5 -left-2">
+										<Tooltip content={$i18n.t('Dismissible')} className="flex h-fit items-center">
+											<Switch bind:state={banner.dismissible} />
+										</Tooltip>
+									</div>
 								</div>
 								</div>
 
 
 								<button
 								<button
-									class="px-3"
+									class="px-2"
 									type="button"
 									type="button"
 									on:click={() => {
 									on:click={() => {
-										promptSuggestions.splice(promptIdx, 1);
-										promptSuggestions = promptSuggestions;
+										banners.splice(bannerIdx, 1);
+										banners = banners;
 									}}
 									}}
 								>
 								>
 									<svg
 									<svg
@@ -440,14 +378,97 @@
 							</div>
 							</div>
 						{/each}
 						{/each}
 					</div>
 					</div>
+				</div>
 
 
-					{#if promptSuggestions.length > 0}
-						<div class="text-xs text-left w-full mt-2">
-							{$i18n.t('Adjusting these settings will apply changes universally to all users.')}
+				{#if $user.role === 'admin'}
+					<div class=" space-y-3">
+						<div class="flex w-full justify-between mb-2">
+							<div class=" self-center text-sm font-semibold">
+								{$i18n.t('Default Prompt Suggestions')}
+							</div>
+
+							<button
+								class="p-1 px-3 text-xs flex rounded-sm transition"
+								type="button"
+								on:click={() => {
+									if (promptSuggestions.length === 0 || promptSuggestions.at(-1).content !== '') {
+										promptSuggestions = [...promptSuggestions, { content: '', title: ['', ''] }];
+									}
+								}}
+							>
+								<svg
+									xmlns="http://www.w3.org/2000/svg"
+									viewBox="0 0 20 20"
+									fill="currentColor"
+									class="w-4 h-4"
+								>
+									<path
+										d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z"
+									/>
+								</svg>
+							</button>
 						</div>
 						</div>
-					{/if}
-				</div>
-			{/if}
+						<div class="grid lg:grid-cols-2 flex-col gap-1.5">
+							{#each promptSuggestions as prompt, promptIdx}
+								<div
+									class=" flex border border-gray-100 dark:border-none dark:bg-gray-850 rounded-xl py-1.5"
+								>
+									<div class="flex flex-col flex-1 pl-1">
+										<div class="flex border-b border-gray-100 dark:border-gray-850 w-full">
+											<input
+												class="px-3 py-1.5 text-xs w-full bg-transparent outline-hidden border-r border-gray-100 dark:border-gray-850"
+												placeholder={$i18n.t('Title (e.g. Tell me a fun fact)')}
+												bind:value={prompt.title[0]}
+											/>
+
+											<input
+												class="px-3 py-1.5 text-xs w-full bg-transparent outline-hidden border-r border-gray-100 dark:border-gray-850"
+												placeholder={$i18n.t('Subtitle (e.g. about the Roman Empire)')}
+												bind:value={prompt.title[1]}
+											/>
+										</div>
+
+										<textarea
+											class="px-3 py-1.5 text-xs w-full bg-transparent outline-hidden border-r border-gray-100 dark:border-gray-850 resize-none"
+											placeholder={$i18n.t(
+												'Prompt (e.g. Tell me a fun fact about the Roman Empire)'
+											)}
+											rows="3"
+											bind:value={prompt.content}
+										/>
+									</div>
+
+									<button
+										class="px-3"
+										type="button"
+										on:click={() => {
+											promptSuggestions.splice(promptIdx, 1);
+											promptSuggestions = promptSuggestions;
+										}}
+									>
+										<svg
+											xmlns="http://www.w3.org/2000/svg"
+											viewBox="0 0 20 20"
+											fill="currentColor"
+											class="w-4 h-4"
+										>
+											<path
+												d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
+											/>
+										</svg>
+									</button>
+								</div>
+							{/each}
+						</div>
+
+						{#if promptSuggestions.length > 0}
+							<div class="text-xs text-left w-full mt-2">
+								{$i18n.t('Adjusting these settings will apply changes universally to all users.')}
+							</div>
+						{/if}
+					</div>
+				{/if}
+			</div>
 		</div>
 		</div>
 
 
 		<div class="flex justify-end text-sm font-medium">
 		<div class="flex justify-end text-sm font-medium">

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

@@ -199,7 +199,7 @@
 						<Search className="size-3.5" />
 						<Search className="size-3.5" />
 					</div>
 					</div>
 					<input
 					<input
-						class=" w-full text-sm py-1 rounded-r-xl outline-none bg-transparent"
+						class=" w-full text-sm py-1 rounded-r-xl outline-hidden bg-transparent"
 						bind:value={searchValue}
 						bind:value={searchValue}
 						placeholder={$i18n.t('Search Models')}
 						placeholder={$i18n.t('Search Models')}
 					/>
 					/>

+ 2 - 2
src/lib/components/admin/Settings/Models/ConfigureModelsModal.svelte

@@ -165,7 +165,7 @@
 									<select
 									<select
 										class="w-full py-1 text-sm rounded-lg bg-transparent {selectedModelId
 										class="w-full py-1 text-sm rounded-lg bg-transparent {selectedModelId
 											? ''
 											? ''
-											: 'text-gray-500'} placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none"
+											: 'text-gray-500'} placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
 										bind:value={selectedModelId}
 										bind:value={selectedModelId}
 									>
 									>
 										<option value="">{$i18n.t('Select a model')}</option>
 										<option value="">{$i18n.t('Select a model')}</option>
@@ -186,7 +186,7 @@
 												<div class=" text-sm flex-1 py-1 rounded-lg">
 												<div class=" text-sm flex-1 py-1 rounded-lg">
 													{$models.find((model) => model.id === modelId)?.name}
 													{$models.find((model) => model.id === modelId)?.name}
 												</div>
 												</div>
-												<div class="flex-shrink-0">
+												<div class="shrink-0">
 													<button
 													<button
 														type="button"
 														type="button"
 														on:click={() => {
 														on:click={() => {

+ 1 - 1
src/lib/components/admin/Settings/Models/Manage/ManageMultipleOllama.svelte

@@ -12,7 +12,7 @@
 {#if ollamaConfig}
 {#if ollamaConfig}
 	<div class="flex-1 mb-2.5 pr-1.5 rounded-lg bg-gray-50 dark:text-gray-300 dark:bg-gray-850">
 	<div class="flex-1 mb-2.5 pr-1.5 rounded-lg bg-gray-50 dark:text-gray-300 dark:bg-gray-850">
 		<select
 		<select
-			class="w-full py-2 px-4 text-sm outline-none bg-transparent"
+			class="w-full py-2 px-4 text-sm outline-hidden bg-transparent"
 			bind:value={selectedUrlIdx}
 			bind:value={selectedUrlIdx}
 			placeholder={$i18n.t('Select an Ollama instance')}
 			placeholder={$i18n.t('Select an Ollama instance')}
 		>
 		>

+ 7 - 7
src/lib/components/admin/Settings/Models/Manage/ManageOllama.svelte

@@ -598,7 +598,7 @@
 					<div class="flex w-full">
 					<div class="flex w-full">
 						<div class="flex-1 mr-2">
 						<div class="flex-1 mr-2">
 							<input
 							<input
-								class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+								class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
 								placeholder={$i18n.t('Enter model tag (e.g. {{modelTag}})', {
 								placeholder={$i18n.t('Enter model tag (e.g. {{modelTag}})', {
 									modelTag: 'mistral:7b'
 									modelTag: 'mistral:7b'
 								})}
 								})}
@@ -740,7 +740,7 @@
 							class="flex-1 mr-2 pr-1.5 rounded-lg bg-gray-50 dark:text-gray-300 dark:bg-gray-850"
 							class="flex-1 mr-2 pr-1.5 rounded-lg bg-gray-50 dark:text-gray-300 dark:bg-gray-850"
 						>
 						>
 							<select
 							<select
-								class="w-full py-2 px-4 text-sm outline-none bg-transparent"
+								class="w-full py-2 px-4 text-sm outline-hidden bg-transparent"
 								bind:value={deleteModelTag}
 								bind:value={deleteModelTag}
 								placeholder={$i18n.t('Select a model')}
 								placeholder={$i18n.t('Select a model')}
 							>
 							>
@@ -781,7 +781,7 @@
 					<div class="flex w-full">
 					<div class="flex w-full">
 						<div class="flex-1 mr-2 flex flex-col gap-2">
 						<div class="flex-1 mr-2 flex flex-col gap-2">
 							<input
 							<input
-								class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+								class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
 								placeholder={$i18n.t('Enter model tag (e.g. {{modelTag}})', {
 								placeholder={$i18n.t('Enter model tag (e.g. {{modelTag}})', {
 									modelTag: 'my-modelfile'
 									modelTag: 'my-modelfile'
 								})}
 								})}
@@ -791,7 +791,7 @@
 
 
 							<textarea
 							<textarea
 								bind:value={createModelObject}
 								bind:value={createModelObject}
-								class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-100 dark:bg-gray-850 outline-none resize-none scrollbar-hidden"
+								class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-100 dark:bg-gray-850 outline-hidden resize-none scrollbar-hidden"
 								rows="6"
 								rows="6"
 								placeholder={`e.g. {"model": "my-modelfile", "from": "ollama:7b"})`}
 								placeholder={`e.g. {"model": "my-modelfile", "from": "ollama:7b"})`}
 								disabled={createModelLoading}
 								disabled={createModelLoading}
@@ -870,7 +870,7 @@
 							<div class="  text-sm font-medium">{$i18n.t('Upload a GGUF model')}</div>
 							<div class="  text-sm font-medium">{$i18n.t('Upload a GGUF model')}</div>
 
 
 							<button
 							<button
-								class="p-1 px-3 text-xs flex rounded transition"
+								class="p-1 px-3 text-xs flex rounded-sm transition"
 								on:click={() => {
 								on:click={() => {
 									if (modelUploadMode === 'file') {
 									if (modelUploadMode === 'file') {
 										modelUploadMode = 'url';
 										modelUploadMode = 'url';
@@ -922,7 +922,7 @@
 								{:else}
 								{:else}
 									<div class="flex-1 {modelFileUrl !== '' ? 'mr-2' : ''}">
 									<div class="flex-1 {modelFileUrl !== '' ? 'mr-2' : ''}">
 										<input
 										<input
-											class="w-full rounded-lg text-left py-2 px-4 bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none {modelFileUrl !==
+											class="w-full rounded-lg text-left py-2 px-4 bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden {modelFileUrl !==
 											''
 											''
 												? 'mr-2'
 												? 'mr-2'
 												: ''}"
 												: ''}"
@@ -998,7 +998,7 @@
 									</div>
 									</div>
 									<textarea
 									<textarea
 										bind:value={modelFileContent}
 										bind:value={modelFileContent}
-										class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-100 dark:bg-gray-850 outline-none resize-none"
+										class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-100 dark:bg-gray-850 outline-hidden resize-none"
 										rows="6"
 										rows="6"
 									/>
 									/>
 								</div>
 								</div>

+ 8 - 8
src/lib/components/admin/Settings/Pipelines.svelte

@@ -234,7 +234,7 @@
 					<div class="flex gap-2">
 					<div class="flex gap-2">
 						<div class="flex-1">
 						<div class="flex-1">
 							<select
 							<select
-								class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+								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={selectedPipelinesUrlIdx}
 								bind:value={selectedPipelinesUrlIdx}
 								placeholder={$i18n.t('Select a pipeline url')}
 								placeholder={$i18n.t('Select a pipeline url')}
 								on:change={async () => {
 								on:change={async () => {
@@ -271,7 +271,7 @@
 							/>
 							/>
 
 
 							<button
 							<button
-								class="w-full text-sm font-medium py-2 bg-transparent hover:bg-gray-100 border border-dashed dark:border-gray-800 dark:hover:bg-gray-850 text-center rounded-xl"
+								class="w-full text-sm font-medium py-2 bg-transparent hover:bg-gray-100 border border-dashed dark:border-gray-850 dark:hover:bg-gray-850 text-center rounded-xl"
 								type="button"
 								type="button"
 								on:click={() => {
 								on:click={() => {
 									document.getElementById('pipelines-upload-input')?.click();
 									document.getElementById('pipelines-upload-input')?.click();
@@ -348,7 +348,7 @@
 					<div class="flex w-full">
 					<div class="flex w-full">
 						<div class="flex-1 mr-2">
 						<div class="flex-1 mr-2">
 							<input
 							<input
-								class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+								class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
 								placeholder={$i18n.t('Enter Github Raw URL')}
 								placeholder={$i18n.t('Enter Github Raw URL')}
 								bind:value={pipelineDownloadUrl}
 								bind:value={pipelineDownloadUrl}
 							/>
 							/>
@@ -418,7 +418,7 @@
 					</div>
 					</div>
 				</div>
 				</div>
 
 
-				<hr class=" dark:border-gray-800 my-3 w-full" />
+				<hr class="border-gray-100 dark:border-gray-850 my-3 w-full" />
 
 
 				{#if pipelines !== null}
 				{#if pipelines !== null}
 					{#if pipelines.length > 0}
 					{#if pipelines.length > 0}
@@ -432,7 +432,7 @@
 								<div class="flex gap-2">
 								<div class="flex gap-2">
 									<div class="flex-1">
 									<div class="flex-1">
 										<select
 										<select
-											class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+											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={selectedPipelineIdx}
 											bind:value={selectedPipelineIdx}
 											placeholder={$i18n.t('Select a pipeline')}
 											placeholder={$i18n.t('Select a pipeline')}
 											on:change={async () => {
 											on:change={async () => {
@@ -482,7 +482,7 @@
 													</div>
 													</div>
 
 
 													<button
 													<button
-														class="p-1 px-3 text-xs flex rounded transition"
+														class="p-1 px-3 text-xs flex rounded-sm transition"
 														type="button"
 														type="button"
 														on:click={() => {
 														on:click={() => {
 															valves[property] = (valves[property] ?? null) === null ? '' : null;
 															valves[property] = (valves[property] ?? null) === null ? '' : null;
@@ -502,7 +502,7 @@
 														<div class=" flex-1">
 														<div class=" flex-1">
 															{#if valves_spec.properties[property]?.enum ?? null}
 															{#if valves_spec.properties[property]?.enum ?? null}
 																<select
 																<select
-																	class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+																	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={valves[property]}
 																	bind:value={valves[property]}
 																>
 																>
 																	{#each valves_spec.properties[property].enum as option}
 																	{#each valves_spec.properties[property].enum as option}
@@ -523,7 +523,7 @@
 																</div>
 																</div>
 															{:else}
 															{:else}
 																<input
 																<input
-																	class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+																	class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
 																	type="text"
 																	type="text"
 																	placeholder={valves_spec.properties[property].title}
 																	placeholder={valves_spec.properties[property].title}
 																	bind:value={valves[property]}
 																	bind:value={valves[property]}

+ 58 - 14
src/lib/components/admin/Settings/WebSearch.svelte

@@ -6,6 +6,7 @@
 	import { onMount, getContext } from 'svelte';
 	import { onMount, getContext } from 'svelte';
 	import { toast } from 'svelte-sonner';
 	import { toast } from 'svelte-sonner';
 	import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
 	import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
+	import Tooltip from '$lib/components/common/Tooltip.svelte';
 
 
 	const i18n = getContext('i18n');
 	const i18n = getContext('i18n');
 
 
@@ -23,6 +24,7 @@
 		'serper',
 		'serper',
 		'serply',
 		'serply',
 		'searchapi',
 		'searchapi',
+		'serpapi',
 		'duckduckgo',
 		'duckduckgo',
 		'tavily',
 		'tavily',
 		'jina',
 		'jina',
@@ -102,7 +104,7 @@
 					<div class=" self-center text-xs font-medium">{$i18n.t('Web Search Engine')}</div>
 					<div class=" self-center text-xs font-medium">{$i18n.t('Web Search Engine')}</div>
 					<div class="flex items-center relative">
 					<div class="flex items-center relative">
 						<select
 						<select
-							class="dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
+							class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right"
 							bind:value={webConfig.search.engine}
 							bind:value={webConfig.search.engine}
 							placeholder={$i18n.t('Select a engine')}
 							placeholder={$i18n.t('Select a engine')}
 							required
 							required
@@ -115,6 +117,19 @@
 					</div>
 					</div>
 				</div>
 				</div>
 
 
+				<div class=" py-0.5 flex w-full justify-between">
+					<div class=" self-center text-xs font-medium">{$i18n.t('Full Context Mode')}</div>
+					<div class="flex items-center relative">
+						<Tooltip
+							content={webConfig.RAG_WEB_SEARCH_FULL_CONTEXT
+								? 'Inject the entire web results as context for comprehensive processing, this is recommended for complex queries.'
+								: 'Default to segmented retrieval for focused and relevant content extraction, this is recommended for most cases.'}
+						>
+							<Switch bind:state={webConfig.RAG_WEB_SEARCH_FULL_CONTEXT} />
+						</Tooltip>
+					</div>
+				</div>
+
 				{#if webConfig.search.engine !== ''}
 				{#if webConfig.search.engine !== ''}
 					<div class="mt-1.5">
 					<div class="mt-1.5">
 						{#if webConfig.search.engine === 'searxng'}
 						{#if webConfig.search.engine === 'searxng'}
@@ -126,7 +141,7 @@
 								<div class="flex w-full">
 								<div class="flex w-full">
 									<div class="flex-1">
 									<div class="flex-1">
 										<input
 										<input
-											class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+											class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
 											type="text"
 											type="text"
 											placeholder={$i18n.t('Enter Searxng Query URL')}
 											placeholder={$i18n.t('Enter Searxng Query URL')}
 											bind:value={webConfig.search.searxng_query_url}
 											bind:value={webConfig.search.searxng_query_url}
@@ -154,7 +169,7 @@
 								<div class="flex w-full">
 								<div class="flex w-full">
 									<div class="flex-1">
 									<div class="flex-1">
 										<input
 										<input
-											class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+											class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
 											type="text"
 											type="text"
 											placeholder={$i18n.t('Enter Google PSE Engine Id')}
 											placeholder={$i18n.t('Enter Google PSE Engine Id')}
 											bind:value={webConfig.search.google_pse_engine_id}
 											bind:value={webConfig.search.google_pse_engine_id}
@@ -259,7 +274,7 @@
 								<div class="flex w-full">
 								<div class="flex w-full">
 									<div class="flex-1">
 									<div class="flex-1">
 										<input
 										<input
-											class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+											class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
 											type="text"
 											type="text"
 											placeholder={$i18n.t('Enter SearchApi Engine')}
 											placeholder={$i18n.t('Enter SearchApi Engine')}
 											bind:value={webConfig.search.searchapi_engine}
 											bind:value={webConfig.search.searchapi_engine}
@@ -268,6 +283,34 @@
 									</div>
 									</div>
 								</div>
 								</div>
 							</div>
 							</div>
+						{:else if webConfig.search.engine === 'serpapi'}
+							<div>
+								<div class=" self-center text-xs font-medium mb-1">
+									{$i18n.t('SerpApi API Key')}
+								</div>
+
+								<SensitiveInput
+									placeholder={$i18n.t('Enter SerpApi API Key')}
+									bind:value={webConfig.search.serpapi_api_key}
+								/>
+							</div>
+							<div class="mt-1.5">
+								<div class=" self-center text-xs font-medium mb-1">
+									{$i18n.t('SerpApi Engine')}
+								</div>
+
+								<div class="flex w-full">
+									<div class="flex-1">
+										<input
+											class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
+											type="text"
+											placeholder={$i18n.t('Enter SerpApi Engine')}
+											bind:value={webConfig.search.serpapi_engine}
+											autocomplete="off"
+										/>
+									</div>
+								</div>
+							</div>
 						{:else if webConfig.search.engine === 'tavily'}
 						{:else if webConfig.search.engine === 'tavily'}
 							<div>
 							<div>
 								<div class=" self-center text-xs font-medium mb-1">
 								<div class=" self-center text-xs font-medium mb-1">
@@ -310,7 +353,7 @@
 								<div class="flex w-full">
 								<div class="flex w-full">
 									<div class="flex-1">
 									<div class="flex-1">
 										<input
 										<input
-											class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+											class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
 											type="text"
 											type="text"
 											placeholder={$i18n.t('Enter Bing Search V7 Endpoint')}
 											placeholder={$i18n.t('Enter Bing Search V7 Endpoint')}
 											bind:value={webConfig.search.bing_search_v7_endpoint}
 											bind:value={webConfig.search.bing_search_v7_endpoint}
@@ -342,7 +385,7 @@
 							</div>
 							</div>
 
 
 							<input
 							<input
-								class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+								class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
 								placeholder={$i18n.t('Search Result Count')}
 								placeholder={$i18n.t('Search Result Count')}
 								bind:value={webConfig.search.result_count}
 								bind:value={webConfig.search.result_count}
 								required
 								required
@@ -355,7 +398,7 @@
 							</div>
 							</div>
 
 
 							<input
 							<input
-								class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+								class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
 								placeholder={$i18n.t('Concurrent Requests')}
 								placeholder={$i18n.t('Concurrent Requests')}
 								bind:value={webConfig.search.concurrent_requests}
 								bind:value={webConfig.search.concurrent_requests}
 								required
 								required
@@ -369,7 +412,7 @@
 						</div>
 						</div>
 
 
 						<input
 						<input
-							class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+							class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
 							placeholder={$i18n.t(
 							placeholder={$i18n.t(
 								'Enter domains separated by commas (e.g., example.com,site.org)'
 								'Enter domains separated by commas (e.g., example.com,site.org)'
 							)}
 							)}
@@ -379,7 +422,7 @@
 				{/if}
 				{/if}
 			</div>
 			</div>
 
 
-			<hr class=" dark:border-gray-850 my-2" />
+			<hr class="border-gray-100 dark:border-gray-850 my-2" />
 
 
 			<div>
 			<div>
 				<div class=" mb-1 text-sm font-medium">
 				<div class=" mb-1 text-sm font-medium">
@@ -393,14 +436,15 @@
 						</div>
 						</div>
 
 
 						<button
 						<button
-							class="p-1 px-3 text-xs flex rounded transition"
+							class="p-1 px-3 text-xs flex rounded-sm transition"
 							on:click={() => {
 							on:click={() => {
-								webConfig.web_loader_ssl_verification = !webConfig.web_loader_ssl_verification;
+								webConfig.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION =
+									!webConfig.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION;
 								submitHandler();
 								submitHandler();
 							}}
 							}}
 							type="button"
 							type="button"
 						>
 						>
-							{#if webConfig.web_loader_ssl_verification === false}
+							{#if webConfig.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION === false}
 								<span class="ml-2 self-center">{$i18n.t('On')}</span>
 								<span class="ml-2 self-center">{$i18n.t('On')}</span>
 							{:else}
 							{:else}
 								<span class="ml-2 self-center">{$i18n.t('Off')}</span>
 								<span class="ml-2 self-center">{$i18n.t('Off')}</span>
@@ -418,7 +462,7 @@
 						<div class=" w-20 text-xs font-medium self-center">{$i18n.t('Language')}</div>
 						<div class=" w-20 text-xs font-medium self-center">{$i18n.t('Language')}</div>
 						<div class=" flex-1 self-center">
 						<div class=" flex-1 self-center">
 							<input
 							<input
-								class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+								class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
 								type="text"
 								type="text"
 								placeholder={$i18n.t('Enter language codes')}
 								placeholder={$i18n.t('Enter language codes')}
 								bind:value={youtubeLanguage}
 								bind:value={youtubeLanguage}
@@ -433,7 +477,7 @@
 						<div class=" w-20 text-xs font-medium self-center">{$i18n.t('Proxy URL')}</div>
 						<div class=" w-20 text-xs font-medium self-center">{$i18n.t('Proxy URL')}</div>
 						<div class=" flex-1 self-center">
 						<div class=" flex-1 self-center">
 							<input
 							<input
-								class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+								class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
 								type="text"
 								type="text"
 								placeholder={$i18n.t('Enter proxy URL (e.g. https://user:password@host:port)')}
 								placeholder={$i18n.t('Enter proxy URL (e.g. https://user:password@host:port)')}
 								bind:value={youtubeProxyUrl}
 								bind:value={youtubeProxyUrl}

+ 3 - 3
src/lib/components/admin/Users/Groups.svelte

@@ -140,7 +140,7 @@
 						</svg>
 						</svg>
 					</div>
 					</div>
 					<input
 					<input
-						class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-none bg-transparent"
+						class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-hidden bg-transparent"
 						bind:value={search}
 						bind:value={search}
 						placeholder={$i18n.t('Search')}
 						placeholder={$i18n.t('Search')}
 					/>
 					/>
@@ -195,7 +195,7 @@
 					<div class="w-full"></div>
 					<div class="w-full"></div>
 				</div>
 				</div>
 
 
-				<hr class="mt-1.5 border-gray-50 dark:border-gray-850" />
+				<hr class="mt-1.5 border-gray-100 dark:border-gray-850" />
 
 
 				{#each filteredGroups as group}
 				{#each filteredGroups as group}
 					<div class="my-2">
 					<div class="my-2">
@@ -205,7 +205,7 @@
 			</div>
 			</div>
 		{/if}
 		{/if}
 
 
-		<hr class="mb-2 border-gray-50 dark:border-gray-850" />
+		<hr class="mb-2 border-gray-100 dark:border-gray-850" />
 
 
 		<GroupModal
 		<GroupModal
 			bind:show={showDefaultPermissionsModal}
 			bind:show={showDefaultPermissionsModal}

+ 2 - 2
src/lib/components/admin/Users/Groups/AddGroupModal.svelte

@@ -78,7 +78,7 @@
 
 
 								<div class="flex-1">
 								<div class="flex-1">
 									<input
 									<input
-										class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none"
+										class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
 										type="text"
 										type="text"
 										bind:value={name}
 										bind:value={name}
 										placeholder={$i18n.t('Group Name')}
 										placeholder={$i18n.t('Group Name')}
@@ -94,7 +94,7 @@
 
 
 							<div class="flex-1">
 							<div class="flex-1">
 								<Textarea
 								<Textarea
-									className="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none resize-none"
+									className="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden resize-none"
 									rows={2}
 									rows={2}
 									bind:value={description}
 									bind:value={description}
 									placeholder={$i18n.t('Group Description')}
 									placeholder={$i18n.t('Group Description')}

+ 3 - 3
src/lib/components/admin/Users/Groups/Display.svelte

@@ -16,7 +16,7 @@
 
 
 		<div class="flex-1">
 		<div class="flex-1">
 			<input
 			<input
-				class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none"
+				class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
 				type="text"
 				type="text"
 				bind:value={name}
 				bind:value={name}
 				placeholder={$i18n.t('Group Name')}
 				placeholder={$i18n.t('Group Name')}
@@ -36,7 +36,7 @@
 				<div class="text-gray-500">#</div>
 				<div class="text-gray-500">#</div>
 
 
 				<input
 				<input
-					class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none"
+					class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
 					type="text"
 					type="text"
 					bind:value={color}
 					bind:value={color}
 					placeholder={$i18n.t('Hex Color')}
 					placeholder={$i18n.t('Hex Color')}
@@ -52,7 +52,7 @@
 
 
 	<div class="flex-1">
 	<div class="flex-1">
 		<Textarea
 		<Textarea
-			className="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none resize-none"
+			className="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden resize-none"
 			rows={4}
 			rows={4}
 			bind:value={description}
 			bind:value={description}
 			placeholder={$i18n.t('Group Description')}
 			placeholder={$i18n.t('Group Description')}

+ 6 - 6
src/lib/components/admin/Users/Groups/Permissions.svelte

@@ -76,7 +76,7 @@
 										<div class=" text-sm flex-1 rounded-lg">
 										<div class=" text-sm flex-1 rounded-lg">
 											{modelId}
 											{modelId}
 										</div>
 										</div>
-										<div class="flex-shrink-0">
+										<div class="shrink-0">
 											<button
 											<button
 												type="button"
 												type="button"
 												on:click={() => {
 												on:click={() => {
@@ -102,7 +102,7 @@
 					<select
 					<select
 						class="w-full py-1 text-sm rounded-lg bg-transparent {selectedModelId
 						class="w-full py-1 text-sm rounded-lg bg-transparent {selectedModelId
 							? ''
 							? ''
-							: 'text-gray-500'} placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-none"
+							: 'text-gray-500'} placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
 						bind:value={selectedModelId}
 						bind:value={selectedModelId}
 					>
 					>
 						<option value="">{$i18n.t('Select a model')}</option>
 						<option value="">{$i18n.t('Select a model')}</option>
@@ -137,7 +137,7 @@
 
 
 			<div class="flex-1 mr-2">
 			<div class="flex-1 mr-2">
 				<select
 				<select
-					class="w-full bg-transparent outline-none py-0.5 text-sm"
+					class="w-full bg-transparent outline-hidden py-0.5 text-sm"
 					bind:value={permissions.model.default_id}
 					bind:value={permissions.model.default_id}
 					placeholder="Select a model"
 					placeholder="Select a model"
 				>
 				>
@@ -150,7 +150,7 @@
 		</div>
 		</div>
 	</div>
 	</div>
 
 
-	<hr class=" border-gray-50 dark:border-gray-850 my-2" /> -->
+	<hr class=" border-gray-100 dark:border-gray-850 my-2" /> -->
 
 
 	<div>
 	<div>
 		<div class=" mb-2 text-sm font-medium">{$i18n.t('Workspace Permissions')}</div>
 		<div class=" mb-2 text-sm font-medium">{$i18n.t('Workspace Permissions')}</div>
@@ -192,7 +192,7 @@
 		</div>
 		</div>
 	</div>
 	</div>
 
 
-	<hr class=" border-gray-50 dark:border-gray-850 my-2" />
+	<hr class=" border-gray-100 dark:border-gray-850 my-2" />
 
 
 	<div>
 	<div>
 		<div class=" mb-2 text-sm font-medium">{$i18n.t('Chat Permissions')}</div>
 		<div class=" mb-2 text-sm font-medium">{$i18n.t('Chat Permissions')}</div>
@@ -238,7 +238,7 @@
 		</div>
 		</div>
 	</div>
 	</div>
 
 
-	<hr class=" border-gray-50 dark:border-gray-850 my-2" />
+	<hr class=" border-gray-100 dark:border-gray-850 my-2" />
 
 
 	<div>
 	<div>
 		<div class=" mb-2 text-sm font-medium">{$i18n.t('Features Permissions')}</div>
 		<div class=" mb-2 text-sm font-medium">{$i18n.t('Features Permissions')}</div>

+ 1 - 1
src/lib/components/admin/Users/Groups/Users.svelte

@@ -64,7 +64,7 @@
 				</svg>
 				</svg>
 			</div>
 			</div>
 			<input
 			<input
-				class=" w-full text-sm pr-4 rounded-r-xl outline-none bg-transparent"
+				class=" w-full text-sm pr-4 rounded-r-xl outline-hidden bg-transparent"
 				bind:value={query}
 				bind:value={query}
 				placeholder={$i18n.t('Search')}
 				placeholder={$i18n.t('Search')}
 			/>
 			/>

+ 7 - 4
src/lib/components/admin/Users/UserList.svelte

@@ -85,8 +85,9 @@
 				return true;
 				return true;
 			} else {
 			} else {
 				let name = user.name.toLowerCase();
 				let name = user.name.toLowerCase();
+				let email = user.email.toLowerCase();
 				const query = search.toLowerCase();
 				const query = search.toLowerCase();
-				return name.includes(query);
+				return name.includes(query) || email.includes(query);
 			}
 			}
 		})
 		})
 		.sort((a, b) => {
 		.sort((a, b) => {
@@ -149,7 +150,7 @@
 					</svg>
 					</svg>
 				</div>
 				</div>
 				<input
 				<input
-					class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-none bg-transparent"
+					class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-hidden bg-transparent"
 					bind:value={search}
 					bind:value={search}
 					placeholder={$i18n.t('Search')}
 					placeholder={$i18n.t('Search')}
 				/>
 				/>
@@ -171,9 +172,11 @@
 	</div>
 	</div>
 </div>
 </div>
 
 
-<div class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full rounded pt-0.5">
+<div
+	class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full rounded-sm pt-0.5"
+>
 	<table
 	<table
-		class="w-full text-sm text-left text-gray-500 dark:text-gray-400 table-auto max-w-full rounded"
+		class="w-full text-sm text-left text-gray-500 dark:text-gray-400 table-auto max-w-full rounded-sm"
 	>
 	>
 		<thead
 		<thead
 			class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-850 dark:text-gray-400 -translate-y-0.5"
 			class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-850 dark:text-gray-400 -translate-y-0.5"

+ 6 - 6
src/lib/components/admin/Users/UserList/AddUserModal.svelte

@@ -181,7 +181,7 @@
 
 
 								<div class="flex-1">
 								<div class="flex-1">
 									<select
 									<select
-										class="w-full capitalize rounded-lg text-sm bg-transparent dark:disabled:text-gray-500 outline-none"
+										class="w-full capitalize rounded-lg text-sm bg-transparent dark:disabled:text-gray-500 outline-hidden"
 										bind:value={_user.role}
 										bind:value={_user.role}
 										placeholder={$i18n.t('Enter Your Role')}
 										placeholder={$i18n.t('Enter Your Role')}
 										required
 										required
@@ -198,7 +198,7 @@
 
 
 								<div class="flex-1">
 								<div class="flex-1">
 									<input
 									<input
-										class="w-full text-sm bg-transparent disabled:text-gray-500 dark:disabled:text-gray-500 outline-none"
+										class="w-full text-sm bg-transparent disabled:text-gray-500 dark:disabled:text-gray-500 outline-hidden"
 										type="text"
 										type="text"
 										bind:value={_user.name}
 										bind:value={_user.name}
 										placeholder={$i18n.t('Enter Your Full Name')}
 										placeholder={$i18n.t('Enter Your Full Name')}
@@ -208,14 +208,14 @@
 								</div>
 								</div>
 							</div>
 							</div>
 
 
-							<hr class=" border-gray-50 dark:border-gray-850 my-2.5 w-full" />
+							<hr class=" border-gray-100 dark:border-gray-850 my-2.5 w-full" />
 
 
 							<div class="flex flex-col w-full">
 							<div class="flex flex-col w-full">
 								<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Email')}</div>
 								<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Email')}</div>
 
 
 								<div class="flex-1">
 								<div class="flex-1">
 									<input
 									<input
-										class="w-full text-sm bg-transparent disabled:text-gray-500 dark:disabled:text-gray-500 outline-none"
+										class="w-full text-sm bg-transparent disabled:text-gray-500 dark:disabled:text-gray-500 outline-hidden"
 										type="email"
 										type="email"
 										bind:value={_user.email}
 										bind:value={_user.email}
 										placeholder={$i18n.t('Enter Your Email')}
 										placeholder={$i18n.t('Enter Your Email')}
@@ -229,7 +229,7 @@
 
 
 								<div class="flex-1">
 								<div class="flex-1">
 									<input
 									<input
-										class="w-full text-sm bg-transparent disabled:text-gray-500 dark:disabled:text-gray-500 outline-none"
+										class="w-full text-sm bg-transparent disabled:text-gray-500 dark:disabled:text-gray-500 outline-hidden"
 										type="password"
 										type="password"
 										bind:value={_user.password}
 										bind:value={_user.password}
 										placeholder={$i18n.t('Enter Your Password')}
 										placeholder={$i18n.t('Enter Your Password')}
@@ -249,7 +249,7 @@
 									/>
 									/>
 
 
 									<button
 									<button
-										class="w-full text-sm font-medium py-3 bg-transparent hover:bg-gray-100 border border-dashed dark:border-gray-800 dark:hover:bg-gray-850 text-center rounded-xl"
+										class="w-full text-sm font-medium py-3 bg-transparent hover:bg-gray-100 border border-dashed dark:border-gray-850 dark:hover:bg-gray-850 text-center rounded-xl"
 										type="button"
 										type="button"
 										on:click={() => {
 										on:click={() => {
 											document.getElementById('upload-user-csv-input')?.click();
 											document.getElementById('upload-user-csv-input')?.click();

+ 5 - 5
src/lib/components/admin/Users/UserList/EditUserModal.svelte

@@ -65,7 +65,7 @@
 				</svg>
 				</svg>
 			</button>
 			</button>
 		</div>
 		</div>
-		<hr class=" dark:border-gray-800" />
+		<hr class="border-gray-100 dark:border-gray-850" />
 
 
 		<div class="flex flex-col md:flex-row w-full p-5 md:space-x-4 dark:text-gray-200">
 		<div class="flex flex-col md:flex-row w-full p-5 md:space-x-4 dark:text-gray-200">
 			<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
 			<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
@@ -94,7 +94,7 @@
 						</div>
 						</div>
 					</div>
 					</div>
 
 
-					<hr class=" dark:border-gray-800 my-3 w-full" />
+					<hr class="border-gray-100 dark:border-gray-850 my-3 w-full" />
 
 
 					<div class=" flex flex-col space-y-1.5">
 					<div class=" flex flex-col space-y-1.5">
 						<div class="flex flex-col w-full">
 						<div class="flex flex-col w-full">
@@ -102,7 +102,7 @@
 
 
 							<div class="flex-1">
 							<div class="flex-1">
 								<input
 								<input
-									class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 disabled:text-gray-500 dark:disabled:text-gray-500 outline-none"
+									class="w-full rounded-sm py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 disabled:text-gray-500 dark:disabled:text-gray-500 outline-hidden"
 									type="email"
 									type="email"
 									bind:value={_user.email}
 									bind:value={_user.email}
 									autocomplete="off"
 									autocomplete="off"
@@ -117,7 +117,7 @@
 
 
 							<div class="flex-1">
 							<div class="flex-1">
 								<input
 								<input
-									class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
+									class="w-full rounded-sm py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-hidden"
 									type="text"
 									type="text"
 									bind:value={_user.name}
 									bind:value={_user.name}
 									autocomplete="off"
 									autocomplete="off"
@@ -131,7 +131,7 @@
 
 
 							<div class="flex-1">
 							<div class="flex-1">
 								<input
 								<input
-									class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
+									class="w-full rounded-sm py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-hidden"
 									type="password"
 									type="password"
 									bind:value={_user.password}
 									bind:value={_user.password}
 									autocomplete="new-password"
 									autocomplete="new-password"

+ 1 - 1
src/lib/components/admin/Users/UserList/UserChatsModal.svelte

@@ -82,7 +82,7 @@
 						<div class="relative overflow-x-auto">
 						<div class="relative overflow-x-auto">
 							<table class="w-full text-sm text-left text-gray-600 dark:text-gray-400 table-auto">
 							<table class="w-full text-sm text-left text-gray-600 dark:text-gray-400 table-auto">
 								<thead
 								<thead
-									class="text-xs text-gray-700 uppercase bg-transparent dark:text-gray-200 border-b-2 dark:border-gray-800"
+									class="text-xs text-gray-700 uppercase bg-transparent dark:text-gray-200 border-b-2 dark:border-gray-850"
 								>
 								>
 									<tr>
 									<tr>
 										<th
 										<th

+ 1 - 1
src/lib/components/channel/Channel.svelte

@@ -281,7 +281,7 @@
 			<PaneResizer
 			<PaneResizer
 				class="relative flex w-[3px] items-center justify-center bg-background group bg-gray-50 dark:bg-gray-850"
 				class="relative flex w-[3px] items-center justify-center bg-background group bg-gray-50 dark:bg-gray-850"
 			>
 			>
-				<div class="z-10 flex h-7 w-5 items-center justify-center rounded-sm">
+				<div class="z-10 flex h-7 w-5 items-center justify-center rounded-xs">
 					<EllipsisVertical className="size-4 invisible group-hover:visible" />
 					<EllipsisVertical className="size-4 invisible group-hover:visible" />
 				</div>
 				</div>
 			</PaneResizer>
 			</PaneResizer>

+ 5 - 3
src/lib/components/channel/MessageInput.svelte

@@ -103,7 +103,9 @@
 				return;
 				return;
 			}
 			}
 
 
-			if (['image/gif', 'image/webp', 'image/jpeg', 'image/png'].includes(file['type'])) {
+			if (
+				['image/gif', 'image/webp', 'image/jpeg', 'image/png', 'image/avif'].includes(file['type'])
+			) {
 				let reader = new FileReader();
 				let reader = new FileReader();
 
 
 				reader.onload = async (event) => {
 				reader.onload = async (event) => {
@@ -455,7 +457,7 @@
 
 
 						<div class="px-2.5">
 						<div class="px-2.5">
 							<div
 							<div
-								class="scrollbar-hidden font-primary text-left bg-transparent dark:text-gray-100 outline-none w-full pt-3 px-1 rounded-xl resize-none h-fit max-h-80 overflow-auto"
+								class="scrollbar-hidden font-primary text-left bg-transparent dark:text-gray-100 outline-hidden w-full pt-3 px-1 rounded-xl resize-none h-fit max-h-80 overflow-auto"
 							>
 							>
 								<RichTextInput
 								<RichTextInput
 									bind:value={content}
 									bind:value={content}
@@ -513,7 +515,7 @@
 									}}
 									}}
 								>
 								>
 									<button
 									<button
-										class="bg-transparent hover:bg-white/80 text-gray-800 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5 outline-none focus:outline-none"
+										class="bg-transparent hover:bg-white/80 text-gray-800 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5 outline-hidden focus:outline-hidden"
 										type="button"
 										type="button"
 										aria-label="More"
 										aria-label="More"
 									>
 									>

+ 1 - 1
src/lib/components/channel/MessageInput/InputMenu.svelte

@@ -44,7 +44,7 @@
 
 
 	<div slot="content">
 	<div slot="content">
 		<DropdownMenu.Content
 		<DropdownMenu.Content
-			class="w-full max-w-[200px] rounded-xl px-1 py-1  border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow"
+			class="w-full max-w-[200px] rounded-xl px-1 py-1  border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-sm"
 			sideOffset={15}
 			sideOffset={15}
 			alignOffset={-8}
 			alignOffset={-8}
 			side="top"
 			side="top"

+ 4 - 4
src/lib/components/channel/Messages/Message.svelte

@@ -72,7 +72,7 @@
 				class=" absolute {showButtons ? '' : 'invisible group-hover:visible'} right-1 -top-2 z-10"
 				class=" absolute {showButtons ? '' : 'invisible group-hover:visible'} right-1 -top-2 z-10"
 			>
 			>
 				<div
 				<div
-					class="flex gap-1 rounded-lg bg-white dark:bg-gray-850 shadow-md p-0.5 border border-gray-100 dark:border-gray-800"
+					class="flex gap-1 rounded-lg bg-white dark:bg-gray-850 shadow-md p-0.5 border border-gray-100 dark:border-gray-850"
 				>
 				>
 					<ReactionPicker
 					<ReactionPicker
 						onClose={() => (showButtons = false)}
 						onClose={() => (showButtons = false)}
@@ -138,7 +138,7 @@
 			dir={$settings.chatDirection}
 			dir={$settings.chatDirection}
 		>
 		>
 			<div
 			<div
-				class={`flex-shrink-0 ${($settings?.chatDirection ?? 'LTR') === 'LTR' ? 'mr-3' : 'ml-3'} w-9`}
+				class={`shrink-0 ${($settings?.chatDirection ?? 'LTR') === 'LTR' ? 'mr-3' : 'ml-3'} w-9`}
 			>
 			>
 				{#if showUserProfile}
 				{#if showUserProfile}
 					<ProfilePreview user={message.user}>
 					<ProfilePreview user={message.user}>
@@ -153,7 +153,7 @@
 
 
 					{#if message.created_at}
 					{#if message.created_at}
 						<div
 						<div
-							class="mt-1.5 flex flex-shrink-0 items-center text-xs self-center invisible group-hover:visible text-gray-500 font-medium first-letter:capitalize"
+							class="mt-1.5 flex shrink-0 items-center text-xs self-center invisible group-hover:visible text-gray-500 font-medium first-letter:capitalize"
 						>
 						>
 							<Tooltip content={dayjs(message.created_at / 1000000).format('LLLL')}>
 							<Tooltip content={dayjs(message.created_at / 1000000).format('LLLL')}>
 								{dayjs(message.created_at / 1000000).format('HH:mm')}
 								{dayjs(message.created_at / 1000000).format('HH:mm')}
@@ -206,7 +206,7 @@
 				{#if edit}
 				{#if edit}
 					<div class="py-2">
 					<div class="py-2">
 						<Textarea
 						<Textarea
-							className=" bg-transparent outline-none w-full resize-none"
+							className=" bg-transparent outline-hidden w-full resize-none"
 							bind:value={editedContent}
 							bind:value={editedContent}
 							onKeydown={(e) => {
 							onKeydown={(e) => {
 								if (e.key === 'Escape') {
 								if (e.key === 'Escape') {

+ 1 - 1
src/lib/components/channel/Messages/Message/ProfilePreview.svelte

@@ -29,7 +29,7 @@
 
 
 	<slot name="content">
 	<slot name="content">
 		<DropdownMenu.Content
 		<DropdownMenu.Content
-			class="max-w-full w-[240px] rounded-lg z-[9999] bg-white dark:bg-black dark:text-white shadow-lg"
+			class="max-w-full w-[240px] rounded-lg z-9999 bg-white dark:bg-black dark:text-white shadow-lg"
 			sideOffset={8}
 			sideOffset={8}
 			{side}
 			{side}
 			{align}
 			{align}

+ 2 - 2
src/lib/components/channel/Messages/Message/ReactionPicker.svelte

@@ -107,7 +107,7 @@
 		<slot />
 		<slot />
 	</DropdownMenu.Trigger>
 	</DropdownMenu.Trigger>
 	<DropdownMenu.Content
 	<DropdownMenu.Content
-		class="max-w-full w-80 bg-gray-50 dark:bg-gray-850 rounded-lg z-[9999] shadow-lg dark:text-white"
+		class="max-w-full w-80 bg-gray-50 dark:bg-gray-850 rounded-lg z-9999 shadow-lg dark:text-white"
 		sideOffset={8}
 		sideOffset={8}
 		{side}
 		{side}
 		{align}
 		{align}
@@ -116,7 +116,7 @@
 		<div class="mb-1 px-3 pt-2 pb-2">
 		<div class="mb-1 px-3 pt-2 pb-2">
 			<input
 			<input
 				type="text"
 				type="text"
-				class="w-full text-sm bg-transparent outline-none"
+				class="w-full text-sm bg-transparent outline-hidden"
 				placeholder="Search all emojis"
 				placeholder="Search all emojis"
 				bind:value={search}
 				bind:value={search}
 			/>
 			/>

+ 1 - 1
src/lib/components/channel/Navbar.svelte

@@ -18,7 +18,7 @@
 
 
 <nav class="sticky top-0 z-30 w-full px-1.5 py-1.5 -mb-8 flex items-center drag-region">
 <nav class="sticky top-0 z-30 w-full px-1.5 py-1.5 -mb-8 flex items-center drag-region">
 	<div
 	<div
-		class=" bg-gradient-to-b via-50% from-white via-white to-transparent dark:from-gray-900 dark:via-gray-900 dark:to-transparent pointer-events-none absolute inset-0 -bottom-7 z-[-1] blur"
+		class=" bg-linear-to-b via-50% from-white via-white to-transparent dark:from-gray-900 dark:via-gray-900 dark:to-transparent pointer-events-none absolute inset-0 -bottom-7 z-[-1]"
 	></div>
 	></div>
 
 
 	<div class=" flex max-w-full w-full mx-auto px-1 pt-0.5 bg-transparent">
 	<div class=" flex max-w-full w-full mx-auto px-1 pt-0.5 bg-transparent">

+ 2 - 2
src/lib/components/chat/Chat.svelte

@@ -1241,7 +1241,7 @@
 			// Response not done
 			// Response not done
 			return;
 			return;
 		}
 		}
-		if (messages.length != 0 && messages.at(-1).error) {
+		if (messages.length != 0 && messages.at(-1).error && !messages.at(-1).content) {
 			// Error in response
 			// Error in response
 			toast.error($i18n.t(`Oops! There was an error in the previous response.`));
 			toast.error($i18n.t(`Oops! There was an error in the previous response.`));
 			return;
 			return;
@@ -1896,7 +1896,7 @@
 			/>
 			/>
 
 
 			<div
 			<div
-				class="absolute top-0 left-0 w-full h-full bg-gradient-to-t from-white to-white/85 dark:from-gray-900 dark:to-[#171717]/90 z-0"
+				class="absolute top-0 left-0 w-full h-full bg-linear-to-t from-white to-white/85 dark:from-gray-900 dark:to-gray-900/90 z-0"
 			/>
 			/>
 		{/if}
 		{/if}
 
 

+ 2 - 2
src/lib/components/chat/ChatControls.svelte

@@ -195,7 +195,7 @@
 
 
 		{#if $showControls}
 		{#if $showControls}
 			<PaneResizer class="relative flex w-2 items-center justify-center bg-background group">
 			<PaneResizer class="relative flex w-2 items-center justify-center bg-background group">
-				<div class="z-10 flex h-7 w-5 items-center justify-center rounded-sm">
+				<div class="z-10 flex h-7 w-5 items-center justify-center rounded-xs">
 					<EllipsisVertical className="size-4 invisible group-hover:visible" />
 					<EllipsisVertical className="size-4 invisible group-hover:visible" />
 				</div>
 				</div>
 			</PaneResizer>
 			</PaneResizer>
@@ -230,7 +230,7 @@
 					<div
 					<div
 						class="w-full {($showOverview || $showArtifacts) && !$showCallOverlay
 						class="w-full {($showOverview || $showArtifacts) && !$showCallOverlay
 							? ' '
 							? ' '
-							: 'px-4 py-4 bg-white dark:shadow-lg dark:bg-gray-850  border border-gray-50 dark:border-gray-850'}  rounded-xl z-40 pointer-events-auto overflow-y-auto scrollbar-hidden"
+							: 'px-4 py-4 bg-white dark:shadow-lg dark:bg-gray-850  border border-gray-100 dark:border-gray-850'}  rounded-xl z-40 pointer-events-auto overflow-y-auto scrollbar-hidden"
 					>
 					>
 						{#if $showCallOverlay}
 						{#if $showCallOverlay}
 							<div class="w-full h-full flex justify-center">
 							<div class="w-full h-full flex justify-center">

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