浏览代码

Merge branch 'dev' into patch-1

Elkana Bardugo 2 月之前
父节点
当前提交
93590d224b
共有 100 个文件被更改,包括 1936 次插入1290 次删除
  1. 40 5
      backend/open_webui/config.py
  2. 1 0
      backend/open_webui/env.py
  3. 27 15
      backend/open_webui/main.py
  4. 10 14
      backend/open_webui/retrieval/web/duckduckgo.py
  5. 10 3
      backend/open_webui/retrieval/web/tavily.py
  6. 91 2
      backend/open_webui/retrieval/web/utils.py
  7. 22 6
      backend/open_webui/routers/auths.py
  8. 3 1
      backend/open_webui/routers/channels.py
  9. 1 1
      backend/open_webui/routers/ollama.py
  10. 59 51
      backend/open_webui/routers/pipelines.py
  11. 17 4
      backend/open_webui/routers/retrieval.py
  12. 0 0
      backend/open_webui/static/loader.js
  13. 0 2
      backend/open_webui/static/swagger-ui/swagger-ui.css
  14. 54 1
      backend/open_webui/utils/auth.py
  15. 1 7
      backend/open_webui/utils/chat.py
  16. 35 28
      backend/open_webui/utils/middleware.py
  17. 21 7
      backend/open_webui/utils/oauth.py
  18. 3 3
      backend/open_webui/utils/webhook.py
  19. 527 312
      package-lock.json
  20. 2 2
      package.json
  21. 1 2
      postcss.config.js
  22. 14 2
      src/app.css
  23. 1 0
      src/app.html
  24. 1 1
      src/lib/apis/chats/index.ts
  25. 6 6
      src/lib/components/AddConnectionModal.svelte
  26. 1 1
      src/lib/components/ChangelogModal.svelte
  27. 2 2
      src/lib/components/NotificationToast.svelte
  28. 2 2
      src/lib/components/OnBoarding.svelte
  29. 3 3
      src/lib/components/admin/Evaluations/Feedbacks.svelte
  30. 3 3
      src/lib/components/admin/Evaluations/Leaderboard.svelte
  31. 6 6
      src/lib/components/admin/Functions.svelte
  32. 5 5
      src/lib/components/admin/Functions/FunctionEditor.svelte
  33. 3 3
      src/lib/components/admin/Functions/FunctionMenu.svelte
  34. 24 24
      src/lib/components/admin/Settings/Audio.svelte
  35. 4 4
      src/lib/components/admin/Settings/CodeInterpreter.svelte
  36. 4 4
      src/lib/components/admin/Settings/Connections.svelte
  37. 1 1
      src/lib/components/admin/Settings/Connections/ManageOllamaModal.svelte
  38. 1 1
      src/lib/components/admin/Settings/Connections/OllamaConnection.svelte
  39. 2 2
      src/lib/components/admin/Settings/Connections/OpenAIConnection.svelte
  40. 1 1
      src/lib/components/admin/Settings/Database.svelte
  41. 23 23
      src/lib/components/admin/Settings/Documents.svelte
  42. 5 5
      src/lib/components/admin/Settings/Evaluations/ArenaModelModal.svelte
  43. 1 1
      src/lib/components/admin/Settings/Evaluations/Model.svelte
  44. 471 316
      src/lib/components/admin/Settings/General.svelte
  45. 17 17
      src/lib/components/admin/Settings/Images.svelte
  46. 217 209
      src/lib/components/admin/Settings/Interface.svelte
  47. 1 1
      src/lib/components/admin/Settings/Models.svelte
  48. 2 2
      src/lib/components/admin/Settings/Models/ConfigureModelsModal.svelte
  49. 1 1
      src/lib/components/admin/Settings/Models/Manage/ManageMultipleOllama.svelte
  50. 7 7
      src/lib/components/admin/Settings/Models/Manage/ManageOllama.svelte
  51. 8 8
      src/lib/components/admin/Settings/Pipelines.svelte
  52. 13 13
      src/lib/components/admin/Settings/WebSearch.svelte
  53. 3 3
      src/lib/components/admin/Users/Groups.svelte
  54. 2 2
      src/lib/components/admin/Users/Groups/AddGroupModal.svelte
  55. 3 3
      src/lib/components/admin/Users/Groups/Display.svelte
  56. 6 6
      src/lib/components/admin/Users/Groups/Permissions.svelte
  57. 1 1
      src/lib/components/admin/Users/Groups/Users.svelte
  58. 3 3
      src/lib/components/admin/Users/UserList.svelte
  59. 6 6
      src/lib/components/admin/Users/UserList/AddUserModal.svelte
  60. 5 5
      src/lib/components/admin/Users/UserList/EditUserModal.svelte
  61. 1 1
      src/lib/components/admin/Users/UserList/UserChatsModal.svelte
  62. 1 1
      src/lib/components/channel/Channel.svelte
  63. 2 2
      src/lib/components/channel/MessageInput.svelte
  64. 1 1
      src/lib/components/channel/MessageInput/InputMenu.svelte
  65. 4 4
      src/lib/components/channel/Messages/Message.svelte
  66. 1 1
      src/lib/components/channel/Messages/Message/ProfilePreview.svelte
  67. 2 2
      src/lib/components/channel/Messages/Message/ReactionPicker.svelte
  68. 1 1
      src/lib/components/channel/Navbar.svelte
  69. 2 2
      src/lib/components/chat/Chat.svelte
  70. 2 2
      src/lib/components/chat/ChatControls.svelte
  71. 5 5
      src/lib/components/chat/ContentRenderer/FloatingButtons.svelte
  72. 1 1
      src/lib/components/chat/Controls/Controls.svelte
  73. 2 2
      src/lib/components/chat/Controls/Valves.svelte
  74. 10 10
      src/lib/components/chat/MessageInput.svelte
  75. 1 1
      src/lib/components/chat/MessageInput/CallOverlay/VideoInputMenu.svelte
  76. 1 1
      src/lib/components/chat/MessageInput/Commands.svelte
  77. 6 6
      src/lib/components/chat/MessageInput/Commands/Knowledge.svelte
  78. 1 1
      src/lib/components/chat/MessageInput/Commands/Models.svelte
  79. 1 1
      src/lib/components/chat/MessageInput/Commands/Prompts.svelte
  80. 2 2
      src/lib/components/chat/MessageInput/FilesOverlay.svelte
  81. 3 3
      src/lib/components/chat/MessageInput/InputMenu.svelte
  82. 1 1
      src/lib/components/chat/MessageInput/VoiceRecording.svelte
  83. 6 6
      src/lib/components/chat/Messages/Citations.svelte
  84. 5 3
      src/lib/components/chat/Messages/CitationsModal.svelte
  85. 2 2
      src/lib/components/chat/Messages/CodeBlock.svelte
  86. 1 1
      src/lib/components/chat/Messages/CodeExecutionModal.svelte
  87. 3 9
      src/lib/components/chat/Messages/Markdown/MarkdownInlineTokens.svelte
  88. 4 4
      src/lib/components/chat/Messages/Markdown/MarkdownTokens.svelte
  89. 16 1
      src/lib/components/chat/Messages/Markdown/Source.svelte
  90. 3 3
      src/lib/components/chat/Messages/MultiResponseMessages.svelte
  91. 5 5
      src/lib/components/chat/Messages/RateComment.svelte
  92. 4 4
      src/lib/components/chat/Messages/ResponseMessage.svelte
  93. 7 7
      src/lib/components/chat/Messages/Skeleton.svelte
  94. 3 3
      src/lib/components/chat/Messages/UserMessage.svelte
  95. 12 12
      src/lib/components/chat/ModelSelector/Selector.svelte
  96. 1 1
      src/lib/components/chat/Navbar.svelte
  97. 1 1
      src/lib/components/chat/Placeholder.svelte
  98. 2 2
      src/lib/components/chat/Settings/About.svelte
  99. 3 3
      src/lib/components/chat/Settings/Account.svelte
  100. 3 3
      src/lib/components/chat/Settings/Account/UpdatePassword.svelte

+ 40 - 5
backend/open_webui/config.py

@@ -2,6 +2,8 @@ import json
 import logging
 import os
 import shutil
+import base64
+
 from datetime import datetime
 from pathlib import Path
 from typing import Generic, Optional, TypeVar
@@ -586,6 +588,20 @@ load_oauth_providers()
 
 STATIC_DIR = Path(os.getenv("STATIC_DIR", OPEN_WEBUI_DIR / "static")).resolve()
 
+
+def override_static(path: str, content: str):
+    # Ensure path is safe
+    if "/" in path or ".." in path:
+        log.error(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
+
+
 frontend_favicon = FRONTEND_BUILD_DIR / "static" / "favicon.png"
 
 if frontend_favicon.exists():
@@ -593,8 +609,6 @@ if frontend_favicon.exists():
         shutil.copyfile(frontend_favicon, STATIC_DIR / "favicon.png")
     except Exception as 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"
 
@@ -603,12 +617,18 @@ if frontend_splash.exists():
         shutil.copyfile(frontend_splash, STATIC_DIR / "splash.png")
     except Exception as 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", "")
@@ -650,6 +670,16 @@ if CUSTOM_NAME:
         pass
 
 
+####################################
+# LICENSE_KEY
+####################################
+
+LICENSE_KEY = PersistentConfig(
+    "LICENSE_KEY",
+    "license.key",
+    os.environ.get("LICENSE_KEY", ""),
+)
+
 ####################################
 # STORAGE PROVIDER
 ####################################
@@ -1853,6 +1883,11 @@ RAG_WEB_SEARCH_CONCURRENT_REQUESTS = PersistentConfig(
     int(os.getenv("RAG_WEB_SEARCH_CONCURRENT_REQUESTS", "10")),
 )
 
+RAG_WEB_SEARCH_TRUST_ENV = PersistentConfig(
+    "RAG_WEB_SEARCH_TRUST_ENV",
+    "rag.web.search.trust_env",
+    os.getenv("RAG_WEB_SEARCH_TRUST_ENV", False),
+)
 
 ####################################
 # Images

+ 1 - 0
backend/open_webui/env.py

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

+ 27 - 15
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.config import (
+    LICENSE_KEY,
     # Ollama
     ENABLE_OLLAMA_API,
     OLLAMA_BASE_URLS,
@@ -175,6 +176,7 @@ from open_webui.config import (
     RAG_WEB_SEARCH_ENGINE,
     RAG_WEB_SEARCH_RESULT_COUNT,
     RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
+    RAG_WEB_SEARCH_TRUST_ENV,
     RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
     JINA_API_KEY,
     SEARCHAPI_API_KEY,
@@ -313,15 +315,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.auth import (
+    get_license_data,
     decode_token,
     get_admin_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.tasks import stop_task, list_tasks  # Import from tasks.py
 
+
 if SAFE_MODE:
     print("SAFE MODE ENABLED")
     Functions.deactivate_all_functions()
@@ -348,12 +352,12 @@ class SPAStaticFiles(StaticFiles):
 
 print(
     rf"""
-  ___                    __        __   _     _   _ ___
- / _ \ _ __   ___ _ __   \ \      / /__| |__ | | | |_ _|
-| | | | '_ \ / _ \ '_ \   \ \ /\ / / _ \ '_ \| | | || |
-| |_| | |_) |  __/ | | |   \ V  V /  __/ |_) | |_| || |
- \___/| .__/ \___|_| |_|    \_/\_/ \___|_.__/ \___/|___|
-      |_|
+ ██████╗ ██████╗ ███████╗███╗   ██╗    ██╗    ██╗███████╗██████╗ ██╗   ██╗██╗
+██╔═══██╗██╔══██╗██╔════╝████╗  ██║    ██║    ██║██╔════╝██╔══██╗██║   ██║██║
+██║   ██║██████╔╝█████╗  ██╔██╗ ██║    ██║ █╗ ██║█████╗  ██████╔╝██║   ██║██║
+██║   ██║██╔═══╝ ██╔══╝  ██║╚██╗██║    ██║███╗██║██╔══╝  ██╔══██╗██║   ██║██║
+╚██████╔╝██║     ███████╗██║ ╚████║    ╚███╔███╔╝███████╗██████╔╝╚██████╔╝██║
+ ╚═════╝ ╚═╝     ╚══════╝╚═╝  ╚═══╝     ╚══╝╚══╝ ╚══════╝╚═════╝  ╚═════╝ ╚═╝
 
 
 v{VERSION} - building the best open-source AI user interface.
@@ -368,6 +372,9 @@ async def lifespan(app: FastAPI):
     if RESET_CONFIG_ON_START:
         reset_config()
 
+    if app.state.config.LICENSE_KEY:
+        get_license_data(app, app.state.config.LICENSE_KEY)
+
     asyncio.create_task(periodic_usage_pool_cleanup())
     yield
 
@@ -379,8 +386,12 @@ app = FastAPI(
     lifespan=lifespan,
 )
 
+oauth_manager = OAuthManager(app)
+
 app.state.config = AppConfig()
 
+app.state.WEBUI_NAME = WEBUI_NAME
+app.state.config.LICENSE_KEY = LICENSE_KEY
 
 ########################################
 #
@@ -482,10 +493,10 @@ app.state.config.LDAP_CIPHERS = LDAP_CIPHERS
 app.state.AUTH_TRUSTED_EMAIL_HEADER = WEBUI_AUTH_TRUSTED_EMAIL_HEADER
 app.state.AUTH_TRUSTED_NAME_HEADER = WEBUI_AUTH_TRUSTED_NAME_HEADER
 
+app.state.USER_COUNT = None
 app.state.TOOLS = {}
 app.state.FUNCTIONS = {}
 
-
 ########################################
 #
 # RETRIEVAL
@@ -558,6 +569,7 @@ 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_CONCURRENT_REQUESTS = RAG_WEB_SEARCH_CONCURRENT_REQUESTS
+app.state.config.RAG_WEB_SEARCH_TRUST_ENV = RAG_WEB_SEARCH_TRUST_ENV
 
 app.state.EMBEDDING_FUNCTION = None
 app.state.ef = None
@@ -1069,7 +1081,7 @@ async def get_app_config(request: Request):
     return {
         **({"onboarding": True} if onboarding else {}),
         "status": True,
-        "name": WEBUI_NAME,
+        "name": app.state.WEBUI_NAME,
         "version": VERSION,
         "default_locale": str(DEFAULT_LOCALE),
         "oauth": {
@@ -1204,7 +1216,7 @@ if len(OAUTH_PROVIDERS) > 0:
 
 @app.get("/oauth/{provider}/login")
 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:
@@ -1215,14 +1227,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
 @app.get("/oauth/{provider}/callback")
 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")
 async def get_manifest_json():
     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.",
         "start_url": "/",
         "display": "standalone",
@@ -1249,8 +1261,8 @@ async def get_manifest_json():
 async def get_opensearch_xml():
     xml_content = rf"""
     <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>
     <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}"}"/>

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

@@ -32,19 +32,15 @@ def search_duckduckgo(
             # Convert the search results into a list
             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:
-        results = get_filtered_results(results, filter_list)
+        search_results = get_filtered_results(search_results, filter_list)
+
     # 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
+    ]

+ 10 - 3
backend/open_webui/retrieval/web/tavily.py

@@ -1,4 +1,5 @@
 import logging
+from typing import Optional
 
 import requests
 from open_webui.retrieval.web.main import SearchResult
@@ -8,7 +9,13 @@ log = logging.getLogger(__name__)
 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.
 
     Args:
@@ -20,8 +27,8 @@ def search_tavily(api_key: str, query: str, count: int) -> list[SearchResult]:
     """
     url = "https://api.tavily.com/search"
     data = {"query": query, "api_key": api_key}
-
-    response = requests.post(url, json=data)
+    include_domain = filter_list
+    response = requests.post(url, include_domain, json=data)
     response.raise_for_status()
 
     json_response = response.json()

+ 91 - 2
backend/open_webui/retrieval/web/utils.py

@@ -1,7 +1,10 @@
 import socket
+import aiohttp
+import asyncio
 import urllib.parse
 import validators
-from typing import Union, Sequence, Iterator
+from typing import Any, AsyncIterator, Dict, Iterator, List, Sequence, Union
+
 
 from langchain_community.document_loaders import (
     WebBaseLoader,
@@ -68,6 +71,70 @@ def resolve_hostname(hostname):
 class SafeWebBaseLoader(WebBaseLoader):
     """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]:
         """Lazy load text from the url(s) in web_path with error handling."""
         for path in self.web_paths:
@@ -91,18 +158,40 @@ class SafeWebBaseLoader(WebBaseLoader):
                 # Log the error and continue with the next URL
                 log.error(f"Error loading {path}: {e}")
 
+    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()]
+
 
 def get_web_loader(
     urls: Union[str, Sequence[str]],
     verify_ssl: bool = True,
     requests_per_second: int = 2,
+    trust_env: bool = False,
 ):
     # Check if the URLs are valid
     safe_urls = safe_validate_urls([urls] if isinstance(urls, str) else urls)
 
     return SafeWebBaseLoader(
-        safe_urls,
+        web_path=safe_urls,
         verify_ssl=verify_ssl,
         requests_per_second=requests_per_second,
         continue_on_failure=True,
+        trust_env=trust_env,
     )

+ 22 - 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)
             if not user:
                 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 = (
                         "admin"
-                        if Users.get_num_users() == 0
+                        if user_count == 0
                         else request.app.state.config.DEFAULT_USER_ROLE
                     )
 
@@ -413,6 +423,8 @@ async def signin(request: Request, response: Response, form_data: SigninForm):
 
 @router.post("/signup", response_model=SessionUserResponse)
 async def signup(request: Request, response: Response, form_data: SignupForm):
+    user_count = Users.get_num_users()
+
     if WEBUI_AUTH:
         if (
             not request.app.state.config.ENABLE_SIGNUP
@@ -422,11 +434,16 @@ async def signup(request: Request, response: Response, form_data: SignupForm):
                 status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED
             )
     else:
-        if Users.get_num_users() != 0:
+        if user_count != 0:
             raise HTTPException(
                 status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED
             )
 
+    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()):
         raise HTTPException(
             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:
         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
             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:
                 post_webhook(
+                    request.app.state.WEBUI_NAME,
                     request.app.state.config.WEBHOOK_URL,
                     WEBHOOK_MESSAGES.USER_SIGNUP(user.name),
                     {

+ 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)
 
     for user in users:
@@ -206,6 +206,7 @@ async def send_notification(webui_url, channel, message, active_user_ids):
 
                 if webhook_url:
                     post_webhook(
+                        name,
                         webhook_url,
                         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(
                 send_notification,
+                request.app.state.WEBUI_NAME,
                 request.app.state.config.WEBUI_URL,
                 channel,
                 message,

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

@@ -944,7 +944,7 @@ class ChatMessage(BaseModel):
 class GenerateChatCompletionForm(BaseModel):
     model: str
     messages: list[ChatMessage]
-    format: Optional[dict] = None
+    format: Optional[Union[dict, str]] = None
     options: Optional[dict] = None
     template: Optional[str] = None
     stream: Optional[bool] = True

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

@@ -9,6 +9,7 @@ from fastapi import (
     status,
     APIRouter,
 )
+import aiohttp
 import os
 import logging
 import shutil
@@ -56,96 +57,103 @@ def get_sorted_filters(model_id, models):
     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}
     model_id = payload["model"]
-
     sorted_filters = get_sorted_filters(model_id, models)
     model = models[model_id]
 
     if "pipeline" in 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]
             key = request.app.state.config.OPENAI_API_KEYS[urlIdx]
 
-            if key == "":
+            if not key:
                 continue
 
             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:
-                    raise Exception(r.status_code, res["detail"])
+                    raise Exception(response.status, res["detail"])
+            except Exception as e:
+                print(f"Connection error: {e}")
 
     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}
     model_id = payload["model"]
-
     sorted_filters = get_sorted_filters(model_id, models)
     model = models[model_id]
 
     if "pipeline" in model:
         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]
             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:
-                    res = r.json()
+                    res = (
+                        await response.json()
+                        if "application/json" in response.content_type
+                        else {}
+                    )
                     if "detail" in res:
-                        return Exception(r.status_code, res)
+                        raise Exception(response.status, res)
                 except Exception:
                     pass
-
-            else:
-                pass
+            except Exception as e:
+                print(f"Connection error: {e}")
 
     return payload
 

+ 17 - 4
backend/open_webui/routers/retrieval.py

@@ -21,6 +21,7 @@ from fastapi import (
     APIRouter,
 )
 from fastapi.middleware.cors import CORSMiddleware
+from fastapi.concurrency import run_in_threadpool
 from pydantic import BaseModel
 import tiktoken
 
@@ -450,6 +451,7 @@ class WebSearchConfig(BaseModel):
     exa_api_key: Optional[str] = None
     result_count: Optional[int] = None
     concurrent_requests: Optional[int] = None
+    trust_env: Optional[bool] = None
     domain_filter_list: Optional[List[str]] = []
 
 
@@ -569,6 +571,9 @@ async def update_rag_config(
         request.app.state.config.RAG_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 = (
             form_data.web.search.domain_filter_list
         )
@@ -621,6 +626,7 @@ async def update_rag_config(
                 "exa_api_key": request.app.state.config.EXA_API_KEY,
                 "result_count": request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
                 "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,
             },
         },
@@ -1308,7 +1314,7 @@ def search_web(request: Request, engine: str, query: str) -> list[SearchResult]:
 
 
 @router.post("/process/web/search")
-def process_web_search(
+async def process_web_search(
     request: Request, form_data: SearchForm, user=Depends(get_verified_user)
 ):
     try:
@@ -1340,16 +1346,23 @@ def process_web_search(
             urls,
             verify_ssl=request.app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
             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()
+        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:
         log.exception(e)

+ 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 {
 	font-size: 16px;
 }
-
-/*# sourceMappingURL=swagger-ui.css.map*/

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

@@ -1,6 +1,11 @@
 import logging
 import uuid
 import jwt
+import base64
+import hmac
+import hashlib
+import requests
+
 
 from datetime import UTC, datetime, timedelta
 from typing import Optional, Union, List, Dict
@@ -8,7 +13,8 @@ from typing import Optional, Union, List, Dict
 from open_webui.models.users import Users
 
 from open_webui.constants import ERROR_MESSAGES
-from open_webui.env import WEBUI_SECRET_KEY
+from open_webui.config import override_static
+from open_webui.env import WEBUI_SECRET_KEY, TRUSTED_SIGNATURE_KEY
 
 from fastapi import Depends, HTTPException, Request, Response, status
 from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
@@ -24,6 +30,53 @@ ALGORITHM = "HS256"
 # 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 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)
 pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
 

+ 1 - 7
backend/open_webui/utils/chat.py

@@ -186,12 +186,6 @@ async def generate_chat_completion(
     if model_id not in models:
         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]
 
     if getattr(request.state, "direct", False):
@@ -308,7 +302,7 @@ async def chat_completed(request: Request, form_data: dict, user: Any):
     model = models[model_id]
 
     try:
-        data = process_pipeline_outlet_filter(request, data, user, models)
+        data = await process_pipeline_outlet_filter(request, data, user, models)
     except Exception as e:
         return Exception(f"Error: {e}")
 

+ 35 - 28
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.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
 
@@ -334,21 +337,15 @@ async def chat_web_search_handler(
 
     try:
 
-        # 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,
-                ),
-            )
+        results = await process_web_search(
+            request,
+            SearchForm(
+                **{
+                    "query": searchQuery,
+                }
+            ),
+            user,
+        )
 
         if results:
             await event_emitter(
@@ -682,6 +679,25 @@ async def process_chat_payload(request, form_data, metadata, user, model):
 
     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)
     if features:
         if "web_search" in features and features["web_search"]:
@@ -704,17 +720,6 @@ async def process_chat_payload(request, form_data, metadata, user, model):
                 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)
     files = form_data.pop("files", None)
     # Remove files duplicates
@@ -778,7 +783,7 @@ async def process_chat_payload(request, form_data, metadata, user, model):
 
             if "document" in source:
                 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()
         prompt = get_last_user_message(form_data["messages"])
@@ -1003,6 +1008,7 @@ async def process_chat_response(
                         webhook_url = Users.get_user_webhook_url_by_id(user.id)
                         if webhook_url:
                             post_webhook(
+                                request.app.state.WEBUI_NAME,
                                 webhook_url,
                                 f"{title} - {request.app.state.config.WEBUI_URL}/c/{metadata['chat_id']}\n\n{content}",
                                 {
@@ -1868,6 +1874,7 @@ async def process_chat_response(
                     webhook_url = Users.get_user_webhook_url_by_id(user.id)
                     if webhook_url:
                         post_webhook(
+                            request.app.state.WEBUI_NAME,
                             webhook_url,
                             f"{title} - {request.app.state.config.WEBUI_URL}/c/{metadata['chat_id']}\n\n{content}",
                             {

+ 21 - 7
backend/open_webui/utils/oauth.py

@@ -36,7 +36,11 @@ from open_webui.config import (
     AppConfig,
 )
 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.auth import get_password_hash, create_token
 from open_webui.utils.webhook import post_webhook
@@ -66,8 +70,9 @@ auth_manager_config.JWT_EXPIRES_IN = JWT_EXPIRES_IN
 
 
 class OAuthManager:
-    def __init__(self):
+    def __init__(self, app):
         self.oauth = OAuth()
+        self.app = app
         for _, provider_config in OAUTH_PROVIDERS.items():
             provider_config["register"](self.oauth)
 
@@ -200,7 +205,7 @@ class OAuthManager:
                     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:
             raise HTTPException(404)
         # If the provider has a custom redirect URL, use that, otherwise automatically generate one
@@ -212,7 +217,7 @@ class OAuthManager:
             raise HTTPException(404)
         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:
             raise HTTPException(404)
         client = self.get_client(provider)
@@ -266,6 +271,17 @@ class OAuthManager:
                 Users.update_user_role_by_id(user.id, determined_role)
 
         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 auth_manager_config.ENABLE_OAUTH_SIGNUP:
                 # Check if an existing user with the same email already exists
@@ -334,6 +350,7 @@ class OAuthManager:
 
                 if auth_manager_config.WEBHOOK_URL:
                     post_webhook(
+                        WEBUI_NAME,
                         auth_manager_config.WEBHOOK_URL,
                         WEBHOOK_MESSAGES.USER_SIGNUP(user.name),
                         {
@@ -380,6 +397,3 @@ class OAuthManager:
         # Redirect back to the frontend with the JWT token
         redirect_url = f"{request.base_url}auth#token={jwt_token}"
         return RedirectResponse(url=redirect_url, headers=response.headers)
-
-
-oauth_manager = OAuthManager()

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

@@ -2,14 +2,14 @@ import json
 import logging
 
 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
 
 log = logging.getLogger(__name__)
 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:
         log.debug(f"post_webhook: {url}, {message}, {event_data}")
         payload = {}
@@ -39,7 +39,7 @@ def post_webhook(url: str, message: str, event_data: dict) -> bool:
                 "sections": [
                     {
                         "activityTitle": message,
-                        "activitySubtitle": f"{WEBUI_NAME} ({VERSION}) - {action}",
+                        "activitySubtitle": f"{name} ({VERSION}) - {action}",
                         "activityImage": WEBUI_FAVICON_URL,
                         "facts": facts,
                         "markdown": True,

文件差异内容过多而无法显示
+ 527 - 312
package-lock.json


+ 2 - 2
package.json

@@ -26,10 +26,10 @@
 		"@sveltejs/kit": "^2.5.20",
 		"@sveltejs/vite-plugin-svelte": "^3.1.1",
 		"@tailwindcss/container-queries": "^0.1.1",
+		"@tailwindcss/postcss": "^4.0.0",
 		"@tailwindcss/typography": "^0.5.13",
 		"@typescript-eslint/eslint-plugin": "^6.17.0",
 		"@typescript-eslint/parser": "^6.17.0",
-		"autoprefixer": "^10.4.16",
 		"cypress": "^13.15.0",
 		"eslint": "^8.56.0",
 		"eslint-config-prettier": "^9.1.0",
@@ -43,7 +43,7 @@
 		"svelte": "^4.2.18",
 		"svelte-check": "^3.8.5",
 		"svelte-confetti": "^1.3.2",
-		"tailwindcss": "^3.3.3",
+		"tailwindcss": "^4.0.0",
 		"tslib": "^2.4.1",
 		"typescript": "^5.5.4",
 		"vite": "^5.4.14",

+ 1 - 2
postcss.config.js

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

+ 14 - 2
src/app.css

@@ -1,3 +1,5 @@
+@reference "./tailwind.css";
+
 @font-face {
 	font-family: 'Inter';
 	src: url('/assets/fonts/Inter-Variable.ttf');
@@ -217,8 +219,18 @@ input[type='number'] {
 	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 {

+ 1 - 0
src/app.html

@@ -21,6 +21,7 @@
 			title="Open WebUI"
 			href="/opensearch.xml"
 		/>
+		<script src="/static/loader.js" defer></script>
 
 		<script>
 			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;
 		})
 		.catch((err) => {
-			error = err;
+			error = err.detail;
 
 			console.log(err);
 			return null;

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

@@ -169,7 +169,7 @@
 
 								<div class="flex-1">
 									<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"
 										bind:value={url}
 										placeholder={$i18n.t('API Base URL')}
@@ -202,7 +202,7 @@
 								</button>
 							</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')}>
 									<Switch bind:state={enable} />
 								</Tooltip>
@@ -215,7 +215,7 @@
 
 								<div class="flex-1">
 									<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}
 										placeholder={$i18n.t('API Key')}
 										required={!ollama}
@@ -233,7 +233,7 @@
 										)}
 									>
 										<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"
 											bind:value={prefixId}
 											placeholder={$i18n.t('Prefix ID')}
@@ -258,7 +258,7 @@
 											<div class=" text-sm flex-1 py-1 rounded-lg">
 												{modelId}
 											</div>
-											<div class="flex-shrink-0">
+											<div class="shrink-0">
 												<button
 													type="button"
 													on:click={() => {
@@ -292,7 +292,7 @@
 							<input
 								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}
 								placeholder={$i18n.t('Add a model ID')}
 							/>

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

@@ -68,7 +68,7 @@
 								v{version} - {changelog[version].date}
 							</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}
 								<div class="">

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

@@ -31,13 +31,13 @@
 </script>
 
 <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={() => {
 		onClick();
 		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" />
 	</div>
 

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

@@ -30,10 +30,10 @@
 		<SlideShow duration={5000} />
 
 		<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 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="flex flex-col justify-end w-full items-center pb-10 text-center">

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

@@ -131,14 +131,14 @@
 	</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}
 		<div class="text-center text-xs text-gray-500 dark:text-gray-400 py-1">
 			{$i18n.t('No feedbacks found')}
 		</div>
 	{:else}
 		<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
 				class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-850 dark:text-gray-400 -translate-y-0.5"
@@ -169,7 +169,7 @@
 						<td class=" py-0.5 text-right font-semibold">
 							<div class="flex justify-center">
 								<Tooltip content={feedback?.user?.name}>
-									<div class="flex-shrink-0">
+									<div class="shrink-0">
 										<img
 											src={feedback?.user?.profile_image_url ?? '/user.png'}
 											alt={feedback?.user?.name}

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

@@ -288,7 +288,7 @@
 					<MagnifyingGlass className="size-3" />
 				</div>
 				<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}
 					placeholder={$i18n.t('Search')}
 					on:focus={() => {
@@ -300,7 +300,7 @@
 	</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}
 		<div class=" absolute top-0 bottom-0 left-0 right-0 flex">
 			<div class="m-auto">
@@ -349,7 +349,7 @@
 						</td>
 						<td class="px-3 py-1.5 flex flex-col justify-center">
 							<div class="flex items-center gap-2">
-								<div class="flex-shrink-0">
+								<div class="shrink-0">
 									<img
 										src={model?.info?.meta?.profile_image_url ?? '/favicon.png'}
 										alt={model.name}

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

@@ -180,12 +180,12 @@
 
 		window.addEventListener('keydown', onKeyDown);
 		window.addEventListener('keyup', onKeyUp);
-		window.addEventListener('blur', onBlur);
+		window.addEventListener('blur-sm', onBlur);
 
 		return () => {
 			window.removeEventListener('keydown', onKeyDown);
 			window.removeEventListener('keyup', onKeyUp);
-			window.removeEventListener('blur', onBlur);
+			window.removeEventListener('blur-sm', onBlur);
 		};
 	});
 </script>
@@ -211,7 +211,7 @@
 				<Search className="size-3.5" />
 			</div>
 			<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}
 				placeholder={$i18n.t('Search Functions')}
 			/>
@@ -241,14 +241,14 @@
 					<div class=" flex-1 self-center pl-1">
 						<div class=" font-semibold flex items-center gap-1.5">
 							<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}
 							</div>
 
 							{#if func?.meta?.manifest?.version}
 								<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 ?? ''}
 								</div>
@@ -260,7 +260,7 @@
 						</div>
 
 						<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">
 								{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="w-full mb-2 flex flex-col gap-0.5">
 					<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')}>
 								<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"
@@ -317,7 +317,7 @@ class Pipe:
 						<div class="flex-1">
 							<Tooltip content={$i18n.t('e.g. My Filter')} placement="top-start">
 								<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"
 									placeholder={$i18n.t('Function Name')}
 									bind:value={name}
@@ -333,13 +333,13 @@ class Pipe:
 
 					<div class=" flex gap-2 px-1 items-center">
 						{#if edit}
-							<div class="text-sm text-gray-500 flex-shrink-0">
+							<div class="text-sm text-gray-500 shrink-0">
 								{id}
 							</div>
 						{:else}
 							<Tooltip className="w-full" content={$i18n.t('e.g. my_filter')} placement="top-start">
 								<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"
 									placeholder={$i18n.t('Function ID')}
 									bind:value={id}
@@ -355,7 +355,7 @@ class Pipe:
 							placement="top-start"
 						>
 							<input
-								class="w-full text-sm bg-transparent outline-none"
+								class="w-full text-sm bg-transparent outline-hidden"
 								type="text"
 								placeholder={$i18n.t('Function Description')}
 								bind:value={meta.description}

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

@@ -42,7 +42,7 @@
 
 	<div slot="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}
 			side="bottom"
 			align="start"
@@ -63,7 +63,7 @@
 					</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}
 
 			<DropdownMenu.Item
@@ -122,7 +122,7 @@
 				<div class="flex items-center">{$i18n.t('Export')}</div>
 			</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
 				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"

+ 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="flex items-center relative">
 						<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}
 							placeholder="Select an engine"
 						>
@@ -188,7 +188,7 @@
 					<div>
 						<div class="mt-1 flex gap-2 mb-1">
 							<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')}
 								bind:value={STT_OPENAI_API_BASE_URL}
 								required
@@ -198,7 +198,7 @@
 						</div>
 					</div>
 
-					<hr class=" dark:border-gray-850 my-2" />
+					<hr class="border-gray-100 dark:border-gray-850 my-2" />
 
 					<div>
 						<div class=" mb-1.5 text-sm font-medium">{$i18n.t('STT Model')}</div>
@@ -206,7 +206,7 @@
 							<div class="flex-1">
 								<input
 									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}
 									placeholder="Select a model"
 								/>
@@ -224,14 +224,14 @@
 						</div>
 					</div>
 
-					<hr class=" dark:border-gray-850 my-2" />
+					<hr class="border-gray-100 dark:border-gray-850 my-2" />
 
 					<div>
 						<div class=" mb-1.5 text-sm font-medium">{$i18n.t('STT Model')}</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-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}
 									placeholder="Select a model (optional)"
 								/>
@@ -255,7 +255,7 @@
 						<div class="flex w-full">
 							<div class="flex-1 mr-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"
+									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')}
 									bind:value={STT_WHISPER_MODEL}
 								/>
@@ -333,7 +333,7 @@
 				{/if}
 			</div>
 
-			<hr class=" dark:border-gray-800" />
+			<hr class="border-gray-100 dark:border-gray-850" />
 
 			<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="flex items-center relative">
 						<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}
 							placeholder="Select a mode"
 							on:change={async (e) => {
@@ -372,7 +372,7 @@
 					<div>
 						<div class="mt-1 flex gap-2 mb-1">
 							<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')}
 								bind:value={TTS_OPENAI_API_BASE_URL}
 								required
@@ -385,7 +385,7 @@
 					<div>
 						<div class="mt-1 flex gap-2 mb-1">
 							<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')}
 								bind:value={TTS_API_KEY}
 								required
@@ -396,13 +396,13 @@
 					<div>
 						<div class="mt-1 flex gap-2 mb-1">
 							<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')}
 								bind:value={TTS_API_KEY}
 								required
 							/>
 							<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')}
 								bind:value={TTS_AZURE_SPEECH_REGION}
 								required
@@ -411,7 +411,7 @@
 					</div>
 				{/if}
 
-				<hr class=" dark:border-gray-850 my-2" />
+				<hr class="border-gray-100 dark:border-gray-850 my-2" />
 
 				{#if TTS_ENGINE === ''}
 					<div>
@@ -419,7 +419,7 @@
 						<div class="flex w-full">
 							<div class="flex-1">
 								<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}
 								>
 									<option value="" selected={TTS_VOICE !== ''}>{$i18n.t('Default')}</option>
@@ -442,7 +442,7 @@
 							<div class="flex-1">
 								<input
 									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}
 									placeholder="CMU ARCTIC speaker embedding name"
 								/>
@@ -484,7 +484,7 @@
 								<div class="flex-1">
 									<input
 										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}
 										placeholder="Select a voice"
 									/>
@@ -503,7 +503,7 @@
 								<div class="flex-1">
 									<input
 										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}
 										placeholder="Select a model"
 									/>
@@ -525,7 +525,7 @@
 								<div class="flex-1">
 									<input
 										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}
 										placeholder="Select a voice"
 									/>
@@ -544,7 +544,7 @@
 								<div class="flex-1">
 									<input
 										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}
 										placeholder="Select a model"
 									/>
@@ -566,7 +566,7 @@
 								<div class="flex-1">
 									<input
 										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}
 										placeholder="Select a voice"
 									/>
@@ -593,7 +593,7 @@
 								<div class="flex-1">
 									<input
 										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}
 										placeholder="Select a output format"
 									/>
@@ -603,13 +603,13 @@
 					</div>
 				{/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="self-center text-xs font-medium">{$i18n.t('Response splitting')}</div>
 					<div class="flex items-center relative">
 						<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"
 							bind:value={TTS_SPLIT_ON}
 						>

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

@@ -58,7 +58,7 @@
 					<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"
+							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
@@ -80,7 +80,7 @@
 						<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"
+									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}
@@ -97,7 +97,7 @@
 
 						<div>
 							<select
-								class="dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-left"
+								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')}
 							>
@@ -132,7 +132,7 @@
 				{/if}
 			</div>
 
-			<hr class=" dark:border-gray-850 my-2" />
+			<hr class="border-gray-100 dark:border-gray-850 my-2" />
 
 			<div>
 				<div class="py-0.5 w-full">

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

@@ -234,7 +234,7 @@
 					</div>
 
 					{#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="flex justify-between items-center">
@@ -283,7 +283,7 @@
 				</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="flex justify-between items-center text-sm mb-2">
@@ -300,7 +300,7 @@
 				</div>
 
 				{#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="flex justify-between items-center">
@@ -357,7 +357,7 @@
 				{/if}
 			</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="flex justify-between items-center text-sm">

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

@@ -16,7 +16,7 @@
 			<div
 				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')}
 				</div>
 			</div>

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

@@ -56,7 +56,7 @@
 		{/if}
 
 		<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)')}
 			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-1 relative">
 				<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')}
 					bind:value={url}
 					autocomplete="off"
@@ -85,7 +85,7 @@
 			</div>
 
 			<SensitiveInput
-				inputClassName=" outline-none bg-transparent w-full"
+				inputClassName=" outline-hidden bg-transparent w-full"
 				placeholder={$i18n.t('API Key')}
 				bind:value={key}
 			/>

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

@@ -119,7 +119,7 @@
 				</div>
 			</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}
 				<div class="  flex w-full justify-between">

+ 23 - 23
src/lib/components/admin/Settings/Documents.svelte

@@ -296,7 +296,7 @@
 				<div class=" self-center text-xs font-medium">{$i18n.t('Embedding Model 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"
+						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}
 						placeholder="Select an embedding model engine"
 						on:change={(e) => {
@@ -319,7 +319,7 @@
 			{#if embeddingEngine === 'openai'}
 				<div class="my-0.5 flex gap-2 pr-2">
 					<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')}
 						bind:value={OpenAIUrl}
 						required
@@ -330,7 +330,7 @@
 			{:else if embeddingEngine === 'ollama'}
 				<div class="my-0.5 flex gap-2 pr-2">
 					<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')}
 						bind:value={OllamaUrl}
 						required
@@ -375,7 +375,7 @@
 				<div class=" self-center text-xs font-medium">{$i18n.t('Hybrid Search')}</div>
 
 				<button
-					class="p-1 px-3 text-xs flex rounded transition"
+					class="p-1 px-3 text-xs flex rounded-sm transition"
 					on:click={() => {
 						toggleHybridSearch();
 					}}
@@ -390,7 +390,7 @@
 			</div>
 		</div>
 
-		<hr class="dark:border-gray-850" />
+		<hr class="border-gray-100 dark:border-gray-850" />
 
 		<div class="space-y-2" />
 		<div>
@@ -400,7 +400,7 @@
 				<div class="flex w-full">
 					<div class="flex-1 mr-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"
+							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}
 							placeholder={$i18n.t('Set embedding model')}
 							required
@@ -411,7 +411,7 @@
 				<div class="flex w-full">
 					<div class="flex-1 mr-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"
+							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}})', {
 								model: embeddingModel.slice(-40)
 							})}
@@ -490,7 +490,7 @@
 					<div class="flex w-full">
 						<div class="flex-1 mr-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"
+								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}})', {
 									model: 'BAAI/bge-reranker-v2-m3'
 								})}
@@ -555,7 +555,7 @@
 			{/if}
 		</div>
 
-		<hr class=" dark:border-gray-850" />
+		<hr class=" border-gray-100 dark:border-gray-850" />
 
 		<div class="">
 			<div class="text-sm font-medium mb-1">{$i18n.t('Content Extraction')}</div>
@@ -564,7 +564,7 @@
 				<div class="self-center text-xs font-medium">{$i18n.t('Engine')}</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"
+						class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 text-xs bg-transparent outline-hidden text-right"
 						bind:value={contentExtractionEngine}
 						on:change={(e) => {
 							showTikaServerUrl = e.target.value === 'tika';
@@ -580,7 +580,7 @@
 				<div class="flex w-full mt-1">
 					<div class="flex-1 mr-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"
+							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')}
 							bind:value={tikaServerUrl}
 						/>
@@ -589,7 +589,7 @@
 			{/if}
 		</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>
 
@@ -602,7 +602,7 @@
 			</div>
 		</div>
 
-		<hr class=" dark:border-gray-850" />
+		<hr class=" border-gray-100 dark:border-gray-850" />
 
 		<div class=" ">
 			<div class=" text-sm font-medium mb-1">{$i18n.t('Query Params')}</div>
@@ -613,7 +613,7 @@
 
 					<div class="w-full">
 						<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"
 							placeholder={$i18n.t('Enter Top K')}
 							bind:value={querySettings.k}
@@ -631,7 +631,7 @@
 
 						<div class="w-full">
 							<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"
 								step="0.01"
 								placeholder={$i18n.t('Enter Score')}
@@ -667,7 +667,7 @@
 			</div>
 		</div>
 
-		<hr class=" dark:border-gray-850" />
+		<hr class=" border-gray-100 dark:border-gray-850" />
 
 		<div class=" ">
 			<div class="mb-1 text-sm font-medium">{$i18n.t('Chunk Params')}</div>
@@ -676,7 +676,7 @@
 				<div class="self-center text-xs font-medium">{$i18n.t('Text Splitter')}</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"
+						class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 text-xs bg-transparent outline-hidden text-right"
 						bind:value={textSplitter}
 					>
 						<option value="">{$i18n.t('Default')} ({$i18n.t('Character')})</option>
@@ -692,7 +692,7 @@
 					</div>
 					<div class="self-center">
 						<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"
 							placeholder={$i18n.t('Enter Chunk Size')}
 							bind:value={chunkSize}
@@ -709,7 +709,7 @@
 
 					<div class="self-center">
 						<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"
 							placeholder={$i18n.t('Enter Chunk Overlap')}
 							bind:value={chunkOverlap}
@@ -731,7 +731,7 @@
 			</div>
 		</div>
 
-		<hr class=" dark:border-gray-850" />
+		<hr class=" border-gray-100 dark:border-gray-850" />
 
 		<div class="">
 			<div class="text-sm font-medium mb-1">{$i18n.t('Files')}</div>
@@ -750,7 +750,7 @@
 							placement="top-start"
 						>
 							<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"
 								placeholder={$i18n.t('Leave empty for unlimited')}
 								bind:value={fileMaxSize}
@@ -773,7 +773,7 @@
 							placement="top-start"
 						>
 							<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"
 								placeholder={$i18n.t('Leave empty for unlimited')}
 								bind:value={fileMaxCount}
@@ -786,7 +786,7 @@
 			</div>
 		</div>
 
-		<hr class=" dark:border-gray-850" />
+		<hr class=" border-gray-100 dark:border-gray-850" />
 
 		<div>
 			<button

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

@@ -245,7 +245,7 @@
 
 								<div class="flex-1">
 									<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"
 										bind:value={name}
 										placeholder={$i18n.t('Model Name')}
@@ -260,7 +260,7 @@
 
 								<div class="flex-1">
 									<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"
 										bind:value={id}
 										placeholder={$i18n.t('Model ID')}
@@ -277,7 +277,7 @@
 
 							<div class="flex-1">
 								<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"
 									bind:value={description}
 									placeholder={$i18n.t('Enter description')}
@@ -324,7 +324,7 @@
 											<div class=" text-sm flex-1 py-1 rounded-lg">
 												{$models.find((model) => model.id === modelId)?.name}
 											</div>
-											<div class="flex-shrink-0">
+											<div class="shrink-0">
 												<button
 													type="button"
 													on:click={() => {
@@ -350,7 +350,7 @@
 							<select
 								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}
 							>
 								<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="flex items-center gap-1">
-						<div class="flex-shrink-0 line-clamp-1">
+						<div class="shrink-0 line-clamp-1">
 							{model.name}
 						</div>
 					</div>

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

@@ -1,5 +1,5 @@
 <script lang="ts">
-	import { getBackendConfig, getWebhookUrl, updateWebhookUrl } from '$lib/apis';
+	import { getBackendConfig, getVersionUpdates, getWebhookUrl, updateWebhookUrl } from '$lib/apis';
 	import {
 		getAdminConfig,
 		getLdapConfig,
@@ -11,7 +11,9 @@
 	import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
 	import Switch from '$lib/components/common/Switch.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 { toast } from 'svelte-sonner';
 
@@ -19,6 +21,12 @@
 
 	export let saveHandler: Function;
 
+	let updateAvailable = null;
+	let version = {
+		current: '',
+		latest: ''
+	};
+
 	let adminConfig = null;
 	let webhookUrl = '';
 
@@ -39,6 +47,21 @@
 		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 () => {
 		if (!ENABLE_LDAP) return;
 		const res = await updateLdapServer(localStorage.token, LDAP_SERVER).catch((error) => {
@@ -63,6 +86,8 @@
 	};
 
 	onMount(async () => {
+		checkForVersionUpdates();
+
 		await Promise.all([
 			(async () => {
 				adminConfig = await getAdminConfig(localStorage.token);
@@ -87,381 +112,511 @@
 		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}
-			<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>
 
-						<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
-									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"
-									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>
 							</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>
 
-				<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>
-
-				<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>
 
-				<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>
 
-					<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>
 
-				{#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 class="w-full"></div>
+
+							<Switch bind:state={adminConfig.ENABLE_API_KEY_ENDPOINT_RESTRICTIONS} />
 						</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>
+
 								<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>
-								<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>
+						{/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 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 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 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('TLS')}</div>
+								<div class="  font-medium">{$i18n.t('LDAP')}</div>
 
 								<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>
-							{#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>
-										<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 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>
-										<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
-												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 class="w-full"></div>
 								</div>
 							{/if}
 						</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>
+		{/if}
 	</div>
 
 	<div class="flex justify-end pt-3 text-sm font-medium">

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

@@ -284,7 +284,7 @@
 					<div class=" self-center text-xs font-medium">{$i18n.t('Image Generation Engine')}</div>
 					<div class="flex items-center relative">
 						<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}
 							placeholder={$i18n.t('Select Engine')}
 							on:change={async () => {
@@ -298,7 +298,7 @@
 					</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">
 				{#if (config?.engine ?? 'automatic1111') === 'automatic1111'}
@@ -307,7 +307,7 @@
 						<div class="flex w-full">
 							<div class="flex-1 mr-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"
+									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/)')}
 									bind:value={config.automatic1111.AUTOMATIC1111_BASE_URL}
 								/>
@@ -386,7 +386,7 @@
 								<Tooltip content={$i18n.t('Enter Sampler (e.g. Euler a)')} placement="top-start">
 									<input
 										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)')}
 										bind:value={config.automatic1111.AUTOMATIC1111_SAMPLER}
 									/>
@@ -408,7 +408,7 @@
 								<Tooltip content={$i18n.t('Enter Scheduler (e.g. Karras)')} placement="top-start">
 									<input
 										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)')}
 										bind:value={config.automatic1111.AUTOMATIC1111_SCHEDULER}
 									/>
@@ -429,7 +429,7 @@
 							<div class="flex-1 mr-2">
 								<Tooltip content={$i18n.t('Enter CFG Scale (e.g. 7.0)')} placement="top-start">
 									<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)')}
 										bind:value={config.automatic1111.AUTOMATIC1111_CFG_SCALE}
 									/>
@@ -443,7 +443,7 @@
 						<div class="flex w-full">
 							<div class="flex-1 mr-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"
+									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/)')}
 									bind:value={config.comfyui.COMFYUI_BASE_URL}
 								/>
@@ -497,7 +497,7 @@
 
 						{#if config.comfyui.COMFYUI_WORKFLOW}
 							<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"
 								bind:value={config.comfyui.COMFYUI_WORKFLOW}
 								required
@@ -525,7 +525,7 @@
 								/>
 
 								<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"
 									on:click={() => {
 										document.getElementById('upload-comfyui-workflow-input')?.click();
@@ -548,7 +548,7 @@
 							<div class="text-xs flex flex-col gap-1.5">
 								{#each requiredWorkflowNodes as node}
 									<div class="flex w-full items-center border dark:border-gray-850 rounded-lg">
-										<div class="flex-shrink-0">
+										<div class="shrink-0">
 											<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"
 											>
@@ -558,7 +558,7 @@
 										<div class="">
 											<Tooltip content="Input Key (e.g. text, unet_name, steps)">
 												<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"
 													bind:value={node.key}
 													required
@@ -572,7 +572,7 @@
 												placement="top-start"
 											>
 												<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"
 													bind:value={node.node_ids}
 												/>
@@ -593,7 +593,7 @@
 
 						<div class="flex gap-2 mb-1">
 							<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')}
 								bind:value={config.openai.OPENAI_API_BASE_URL}
 								required
@@ -609,7 +609,7 @@
 			</div>
 
 			{#if config?.enabled}
-				<hr class=" dark:border-gray-850" />
+				<hr class=" border-gray-100 dark:border-gray-850" />
 
 				<div>
 					<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Default Model')}</div>
@@ -620,7 +620,7 @@
 									<Tooltip content={$i18n.t('Enter Model ID')} placement="top-start">
 										<input
 											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}
 											placeholder="Select a model"
 											required
@@ -644,7 +644,7 @@
 						<div class="flex-1 mr-2">
 							<Tooltip content={$i18n.t('Enter Image Size (e.g. 512x512)')} placement="top-start">
 								<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)')}
 									bind:value={imageGenerationConfig.IMAGE_SIZE}
 									required
@@ -660,7 +660,7 @@
 						<div class="flex-1 mr-2">
 							<Tooltip content={$i18n.t('Enter Number of Steps (e.g. 50)')} placement="top-start">
 								<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)')}
 									bind:value={imageGenerationConfig.IMAGE_STEPS}
 									required

+ 217 - 209
src/lib/components/admin/Settings/Interface.svelte

@@ -69,9 +69,13 @@
 		}}
 	>
 		<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
 						content={$i18n.t(
 							'A task model is used when performing tasks such as generating titles for chats and web search queries'
@@ -93,11 +97,12 @@
 						</svg>
 					</Tooltip>
 				</div>
-				<div class="flex w-full gap-2">
+
+				<div class=" mb-2.5 flex w-full gap-2">
 					<div class="flex-1">
 						<div class=" text-xs mb-1">{$i18n.t('Local Models')}</div>
 						<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}
 							placeholder={$i18n.t('Select a model')}
 						>
@@ -113,7 +118,7 @@
 					<div class="flex-1">
 						<div class=" text-xs mb-1">{$i18n.t('External Models')}</div>
 						<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}
 							placeholder={$i18n.t('Select a model')}
 						>
@@ -127,9 +132,7 @@
 					</div>
 				</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">
 						{$i18n.t('Title Generation')}
 					</div>
@@ -138,8 +141,8 @@
 				</div>
 
 				{#if taskConfig.ENABLE_TITLE_GENERATION}
-					<div class="mt-3">
-						<div class=" mb-2.5 text-xs font-medium">{$i18n.t('Title Generation Prompt')}</div>
+					<div class="mb-2.5">
+						<div class=" mb-1 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')}
@@ -155,56 +158,7 @@
 					</div>
 				{/if}
 
-				<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=" self-center text-xs font-medium">
-						{$i18n.t('Autocomplete Generation')}
-					</div>
-
-					<Tooltip content={$i18n.t('Enable autocomplete generation for chat messages')}>
-						<Switch bind:state={taskConfig.ENABLE_AUTOCOMPLETE_GENERATION} />
-					</Tooltip>
-				</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>
-
-						<Tooltip
-							content={$i18n.t('Character limit for autocomplete generation input')}
-							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')}
-							/>
-						</Tooltip>
-					</div>
-				{/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">
 						{$i18n.t('Tags Generation')}
 					</div>
@@ -213,8 +167,8 @@
 				</div>
 
 				{#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
 							content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
@@ -230,9 +184,7 @@
 					</div>
 				{/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">
 						{$i18n.t('Retrieval Query Generation')}
 					</div>
@@ -240,7 +192,7 @@
 					<Switch bind:state={taskConfig.ENABLE_RETRIEVAL_QUERY_GENERATION} />
 				</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">
 						{$i18n.t('Web Search Query Generation')}
 					</div>
@@ -248,8 +200,8 @@
 					<Switch bind:state={taskConfig.ENABLE_SEARCH_QUERY_GENERATION} />
 				</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
 						content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
@@ -263,131 +215,96 @@
 						/>
 					</Tooltip>
 				</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 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>
 
-			{#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">
-							{$i18n.t('Default Prompt Suggestions')}
+							{$i18n.t('Banners')}
 						</div>
 
 						<button
-							class="p-1 px-3 text-xs flex rounded transition"
+							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: ['', ''] }];
+								if (banners.length === 0 || banners.at(-1).content !== '') {
+									banners = [
+										...banners,
+										{
+											id: uuidv4(),
+											type: '',
+											title: '',
+											content: '',
+											dismissible: true,
+											timestamp: Math.floor(Date.now() / 1000)
+										}
+									];
 								}
 							}}
 						>
@@ -403,40 +320,48 @@
 							</svg>
 						</button>
 					</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>
 
 								<button
-									class="px-3"
+									class="px-2"
 									type="button"
 									on:click={() => {
-										promptSuggestions.splice(promptIdx, 1);
-										promptSuggestions = promptSuggestions;
+										banners.splice(bannerIdx, 1);
+										banners = banners;
 									}}
 								>
 									<svg
@@ -453,14 +378,97 @@
 							</div>
 						{/each}
 					</div>
+				</div>
+
+				{#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 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>
 
-					{#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.')}
+									<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}
-				</div>
-			{/if}
+
+						{#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 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" />
 					</div>
 					<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}
 						placeholder={$i18n.t('Search Models')}
 					/>

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

@@ -165,7 +165,7 @@
 									<select
 										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}
 									>
 										<option value="">{$i18n.t('Select a model')}</option>
@@ -186,7 +186,7 @@
 												<div class=" text-sm flex-1 py-1 rounded-lg">
 													{$models.find((model) => model.id === modelId)?.name}
 												</div>
-												<div class="flex-shrink-0">
+												<div class="shrink-0">
 													<button
 														type="button"
 														on:click={() => {

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

@@ -12,7 +12,7 @@
 {#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">
 		<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}
 			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-1 mr-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"
+								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}})', {
 									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"
 						>
 							<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}
 								placeholder={$i18n.t('Select a model')}
 							>
@@ -781,7 +781,7 @@
 					<div class="flex w-full">
 						<div class="flex-1 mr-2 flex flex-col gap-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"
+								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}})', {
 									modelTag: 'my-modelfile'
 								})}
@@ -791,7 +791,7 @@
 
 							<textarea
 								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"
 								placeholder={`e.g. {"model": "my-modelfile", "from": "ollama:7b"})`}
 								disabled={createModelLoading}
@@ -870,7 +870,7 @@
 							<div class="  text-sm font-medium">{$i18n.t('Upload a GGUF model')}</div>
 
 							<button
-								class="p-1 px-3 text-xs flex rounded transition"
+								class="p-1 px-3 text-xs flex rounded-sm transition"
 								on:click={() => {
 									if (modelUploadMode === 'file') {
 										modelUploadMode = 'url';
@@ -922,7 +922,7 @@
 								{:else}
 									<div class="flex-1 {modelFileUrl !== '' ? 'mr-2' : ''}">
 										<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'
 												: ''}"
@@ -998,7 +998,7 @@
 									</div>
 									<textarea
 										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"
 									/>
 								</div>

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

@@ -234,7 +234,7 @@
 					<div class="flex gap-2">
 						<div class="flex-1">
 							<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}
 								placeholder={$i18n.t('Select a pipeline url')}
 								on:change={async () => {
@@ -271,7 +271,7 @@
 							/>
 
 							<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"
 								on:click={() => {
 									document.getElementById('pipelines-upload-input')?.click();
@@ -348,7 +348,7 @@
 					<div class="flex w-full">
 						<div class="flex-1 mr-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"
+								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')}
 								bind:value={pipelineDownloadUrl}
 							/>
@@ -418,7 +418,7 @@
 					</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.length > 0}
@@ -432,7 +432,7 @@
 								<div class="flex gap-2">
 									<div class="flex-1">
 										<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}
 											placeholder={$i18n.t('Select a pipeline')}
 											on:change={async () => {
@@ -482,7 +482,7 @@
 													</div>
 
 													<button
-														class="p-1 px-3 text-xs flex rounded transition"
+														class="p-1 px-3 text-xs flex rounded-sm transition"
 														type="button"
 														on:click={() => {
 															valves[property] = (valves[property] ?? null) === null ? '' : null;
@@ -502,7 +502,7 @@
 														<div class=" flex-1">
 															{#if valves_spec.properties[property]?.enum ?? null}
 																<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]}
 																>
 																	{#each valves_spec.properties[property].enum as option}
@@ -523,7 +523,7 @@
 																</div>
 															{:else}
 																<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"
 																	placeholder={valves_spec.properties[property].title}
 																	bind:value={valves[property]}

+ 13 - 13
src/lib/components/admin/Settings/WebSearch.svelte

@@ -103,7 +103,7 @@
 					<div class=" self-center text-xs font-medium">{$i18n.t('Web Search 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"
+							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}
 							placeholder={$i18n.t('Select a engine')}
 							required
@@ -127,7 +127,7 @@
 								<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-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"
 											placeholder={$i18n.t('Enter Searxng Query URL')}
 											bind:value={webConfig.search.searxng_query_url}
@@ -155,7 +155,7 @@
 								<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-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"
 											placeholder={$i18n.t('Enter Google PSE Engine Id')}
 											bind:value={webConfig.search.google_pse_engine_id}
@@ -260,7 +260,7 @@
 								<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-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"
 											placeholder={$i18n.t('Enter SearchApi Engine')}
 											bind:value={webConfig.search.searchapi_engine}
@@ -288,7 +288,7 @@
 								<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-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"
 											placeholder={$i18n.t('Enter SerpApi Engine')}
 											bind:value={webConfig.search.serpapi_engine}
@@ -339,7 +339,7 @@
 								<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-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"
 											placeholder={$i18n.t('Enter Bing Search V7 Endpoint')}
 											bind:value={webConfig.search.bing_search_v7_endpoint}
@@ -371,7 +371,7 @@
 							</div>
 
 							<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')}
 								bind:value={webConfig.search.result_count}
 								required
@@ -384,7 +384,7 @@
 							</div>
 
 							<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')}
 								bind:value={webConfig.search.concurrent_requests}
 								required
@@ -398,7 +398,7 @@
 						</div>
 
 						<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 domains separated by commas (e.g., example.com,site.org)'
 							)}
@@ -408,7 +408,7 @@
 				{/if}
 			</div>
 
-			<hr class=" dark:border-gray-850 my-2" />
+			<hr class="border-gray-100 dark:border-gray-850 my-2" />
 
 			<div>
 				<div class=" mb-1 text-sm font-medium">
@@ -422,7 +422,7 @@
 						</div>
 
 						<button
-							class="p-1 px-3 text-xs flex rounded transition"
+							class="p-1 px-3 text-xs flex rounded-sm transition"
 							on:click={() => {
 								webConfig.web_loader_ssl_verification = !webConfig.web_loader_ssl_verification;
 								submitHandler();
@@ -447,7 +447,7 @@
 						<div class=" w-20 text-xs font-medium self-center">{$i18n.t('Language')}</div>
 						<div class=" flex-1 self-center">
 							<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"
 								placeholder={$i18n.t('Enter language codes')}
 								bind:value={youtubeLanguage}
@@ -462,7 +462,7 @@
 						<div class=" w-20 text-xs font-medium self-center">{$i18n.t('Proxy URL')}</div>
 						<div class=" flex-1 self-center">
 							<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"
 								placeholder={$i18n.t('Enter proxy URL (e.g. https://user:password@host:port)')}
 								bind:value={youtubeProxyUrl}

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

@@ -140,7 +140,7 @@
 						</svg>
 					</div>
 					<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}
 						placeholder={$i18n.t('Search')}
 					/>
@@ -195,7 +195,7 @@
 					<div class="w-full"></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}
 					<div class="my-2">
@@ -205,7 +205,7 @@
 			</div>
 		{/if}
 
-		<hr class="mb-2 border-gray-50 dark:border-gray-850" />
+		<hr class="mb-2 border-gray-100 dark:border-gray-850" />
 
 		<GroupModal
 			bind:show={showDefaultPermissionsModal}

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

@@ -78,7 +78,7 @@
 
 								<div class="flex-1">
 									<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"
 										bind:value={name}
 										placeholder={$i18n.t('Group Name')}
@@ -94,7 +94,7 @@
 
 							<div class="flex-1">
 								<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}
 									bind:value={description}
 									placeholder={$i18n.t('Group Description')}

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

@@ -16,7 +16,7 @@
 
 		<div class="flex-1">
 			<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"
 				bind:value={name}
 				placeholder={$i18n.t('Group Name')}
@@ -36,7 +36,7 @@
 				<div class="text-gray-500">#</div>
 
 				<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"
 					bind:value={color}
 					placeholder={$i18n.t('Hex Color')}
@@ -52,7 +52,7 @@
 
 	<div class="flex-1">
 		<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}
 			bind:value={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">
 											{modelId}
 										</div>
-										<div class="flex-shrink-0">
+										<div class="shrink-0">
 											<button
 												type="button"
 												on:click={() => {
@@ -102,7 +102,7 @@
 					<select
 						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}
 					>
 						<option value="">{$i18n.t('Select a model')}</option>
@@ -137,7 +137,7 @@
 
 			<div class="flex-1 mr-2">
 				<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}
 					placeholder="Select a model"
 				>
@@ -150,7 +150,7 @@
 		</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 class=" mb-2 text-sm font-medium">{$i18n.t('Workspace Permissions')}</div>
@@ -192,7 +192,7 @@
 		</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 class=" mb-2 text-sm font-medium">{$i18n.t('Chat Permissions')}</div>
@@ -238,7 +238,7 @@
 		</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 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>
 			</div>
 			<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}
 				placeholder={$i18n.t('Search')}
 			/>

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

@@ -149,7 +149,7 @@
 					</svg>
 				</div>
 				<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}
 					placeholder={$i18n.t('Search')}
 				/>
@@ -171,9 +171,9 @@
 	</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
-		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
 			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">
 									<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}
 										placeholder={$i18n.t('Enter Your Role')}
 										required
@@ -198,7 +198,7 @@
 
 								<div class="flex-1">
 									<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"
 										bind:value={_user.name}
 										placeholder={$i18n.t('Enter Your Full Name')}
@@ -208,14 +208,14 @@
 								</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=" mb-1 text-xs text-gray-500">{$i18n.t('Email')}</div>
 
 								<div class="flex-1">
 									<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"
 										bind:value={_user.email}
 										placeholder={$i18n.t('Enter Your Email')}
@@ -229,7 +229,7 @@
 
 								<div class="flex-1">
 									<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"
 										bind:value={_user.password}
 										placeholder={$i18n.t('Enter Your Password')}
@@ -249,7 +249,7 @@
 									/>
 
 									<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"
 										on:click={() => {
 											document.getElementById('upload-user-csv-input')?.click();

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

@@ -65,7 +65,7 @@
 				</svg>
 			</button>
 		</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 w-full sm:flex-row sm:justify-center sm:space-x-6">
@@ -94,7 +94,7 @@
 						</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 w-full">
@@ -102,7 +102,7 @@
 
 							<div class="flex-1">
 								<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"
 									bind:value={_user.email}
 									autocomplete="off"
@@ -117,7 +117,7 @@
 
 							<div class="flex-1">
 								<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"
 									bind:value={_user.name}
 									autocomplete="off"
@@ -131,7 +131,7 @@
 
 							<div class="flex-1">
 								<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"
 									bind:value={_user.password}
 									autocomplete="new-password"

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

@@ -82,7 +82,7 @@
 						<div class="relative overflow-x-auto">
 							<table class="w-full text-sm text-left text-gray-600 dark:text-gray-400 table-auto">
 								<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>
 										<th

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

@@ -281,7 +281,7 @@
 			<PaneResizer
 				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" />
 				</div>
 			</PaneResizer>

+ 2 - 2
src/lib/components/channel/MessageInput.svelte

@@ -455,7 +455,7 @@
 
 						<div class="px-2.5">
 							<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
 									bind:value={content}
@@ -513,7 +513,7 @@
 									}}
 								>
 									<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"
 										aria-label="More"
 									>

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

@@ -44,7 +44,7 @@
 
 	<div slot="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}
 			alignOffset={-8}
 			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"
 			>
 				<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
 						onClose={() => (showButtons = false)}
@@ -138,7 +138,7 @@
 			dir={$settings.chatDirection}
 		>
 			<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}
 					<ProfilePreview user={message.user}>
@@ -153,7 +153,7 @@
 
 					{#if message.created_at}
 						<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')}>
 								{dayjs(message.created_at / 1000000).format('HH:mm')}
@@ -206,7 +206,7 @@
 				{#if edit}
 					<div class="py-2">
 						<Textarea
-							className=" bg-transparent outline-none w-full resize-none"
+							className=" bg-transparent outline-hidden w-full resize-none"
 							bind:value={editedContent}
 							onKeydown={(e) => {
 								if (e.key === 'Escape') {

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

@@ -29,7 +29,7 @@
 
 	<slot name="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}
 			{side}
 			{align}

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

@@ -107,7 +107,7 @@
 		<slot />
 	</DropdownMenu.Trigger>
 	<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}
 		{side}
 		{align}
@@ -116,7 +116,7 @@
 		<div class="mb-1 px-3 pt-2 pb-2">
 			<input
 				type="text"
-				class="w-full text-sm bg-transparent outline-none"
+				class="w-full text-sm bg-transparent outline-hidden"
 				placeholder="Search all emojis"
 				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">
 	<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 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
 			return;
 		}
-		if (messages.length != 0 && messages.at(-1).error) {
+		if (messages.length != 0 && messages.at(-1).error && !messages.at(-1).content) {
 			// Error in response
 			toast.error($i18n.t(`Oops! There was an error in the previous response.`));
 			return;
@@ -1896,7 +1896,7 @@
 			/>
 
 			<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}
 

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

@@ -195,7 +195,7 @@
 
 		{#if $showControls}
 			<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" />
 				</div>
 			</PaneResizer>
@@ -230,7 +230,7 @@
 					<div
 						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}
 							<div class="w-full h-full flex justify-center">

+ 5 - 5
src/lib/components/chat/ContentRenderer/FloatingButtons.svelte

@@ -221,7 +221,7 @@
 
 <div
 	id={`floating-buttons-${id}`}
-	class="absolute rounded-lg mt-1 text-xs z-[9999]"
+	class="absolute rounded-lg mt-1 text-xs z-9999"
 	style="display: none"
 >
 	{#if responseContent === null}
@@ -230,7 +230,7 @@
 				class="flex flex-row gap-0.5 shrink-0 p-1 bg-white dark:bg-gray-850 dark:text-gray-100 text-medium rounded-lg shadow-xl"
 			>
 				<button
-					class="px-1 hover:bg-gray-50 dark:hover:bg-gray-800 rounded flex items-center gap-1 min-w-fit"
+					class="px-1 hover:bg-gray-50 dark:hover:bg-gray-800 rounded-sm flex items-center gap-1 min-w-fit"
 					on:click={async () => {
 						selectedText = window.getSelection().toString();
 						floatingInput = true;
@@ -249,7 +249,7 @@
 					<div class="shrink-0">Ask</div>
 				</button>
 				<button
-					class="px-1 hover:bg-gray-50 dark:hover:bg-gray-800 rounded flex items-center gap-1 min-w-fit"
+					class="px-1 hover:bg-gray-50 dark:hover:bg-gray-800 rounded-sm flex items-center gap-1 min-w-fit"
 					on:click={() => {
 						selectedText = window.getSelection().toString();
 						explainHandler();
@@ -262,12 +262,12 @@
 			</div>
 		{:else}
 			<div
-				class="py-1 flex dark:text-gray-100 bg-gray-50 dark:bg-gray-800 border dark:border-gray-800 w-72 rounded-full shadow-xl"
+				class="py-1 flex dark:text-gray-100 bg-gray-50 dark:bg-gray-800 border dark:border-gray-850 w-72 rounded-full shadow-xl"
 			>
 				<input
 					type="text"
 					id="floating-message-input"
-					class="ml-5 bg-transparent outline-none w-full flex-1 text-sm"
+					class="ml-5 bg-transparent outline-hidden w-full flex-1 text-sm"
 					placeholder={$i18n.t('Ask a question')}
 					bind:value={floatingInputValue}
 					on:keydown={(e) => {

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

@@ -74,7 +74,7 @@
 				<div class="" slot="content">
 					<textarea
 						bind:value={params.system}
-						class="w-full text-xs py-1.5 bg-transparent outline-none resize-none"
+						class="w-full text-xs py-1.5 bg-transparent outline-hidden resize-none"
 						rows="4"
 						placeholder={$i18n.t('Enter system prompt')}
 					/>

+ 2 - 2
src/lib/components/chat/Controls/Valves.svelte

@@ -148,7 +148,7 @@
 				<div class="flex gap-2">
 					<div class="flex-1">
 						<select
-							class="  w-full rounded text-xs py-2 px-1 bg-transparent outline-none"
+							class="  w-full rounded-sm text-xs py-2 px-1 bg-transparent outline-hidden"
 							bind:value={tab}
 							placeholder="Select"
 						>
@@ -161,7 +161,7 @@
 
 					<div class="flex-1">
 						<select
-							class="w-full rounded py-2 px-1 text-xs bg-transparent outline-none"
+							class="w-full rounded-sm py-2 px-1 text-xs bg-transparent outline-hidden"
 							bind:value={selectedId}
 							on:change={async () => {
 								await tick();

+ 10 - 10
src/lib/components/chat/MessageInput.svelte

@@ -394,7 +394,7 @@
 				<div class="w-full relative">
 					{#if atSelectedModel !== undefined || selectedToolIds.length > 0 || webSearchEnabled || ($settings?.webSearch ?? false) === 'always' || imageGenerationEnabled || codeInterpreterEnabled}
 						<div
-							class="px-3 pb-0.5 pt-1.5 text-left w-full flex flex-col absolute bottom-0 left-0 right-0 bg-gradient-to-t from-white dark:from-gray-900 z-10"
+							class="px-3 pb-0.5 pt-1.5 text-left w-full flex flex-col absolute bottom-0 left-0 right-0 bg-linear-to-t from-white dark:from-gray-900 z-10"
 						>
 							{#if selectedToolIds.length > 0}
 								<div class="flex items-center justify-between w-full">
@@ -413,7 +413,7 @@
 											}) as tool, toolIdx (toolIdx)}
 												<Tooltip
 													content={tool?.meta?.description ?? ''}
-													className=" {toolIdx !== 0 ? 'pl-0.5' : ''} flex-shrink-0"
+													className=" {toolIdx !== 0 ? 'pl-0.5' : ''} shrink-0"
 													placement="top"
 												>
 													{tool.name}
@@ -682,7 +682,7 @@
 								<div class="px-2.5">
 									{#if $settings?.richTextInput ?? true}
 										<div
-											class="scrollbar-hidden text-left bg-transparent dark:text-gray-100 outline-none w-full pt-3 px-1 resize-none h-fit max-h-80 overflow-auto"
+											class="scrollbar-hidden text-left bg-transparent dark:text-gray-100 outline-hidden w-full pt-3 px-1 resize-none h-fit max-h-80 overflow-auto"
 										>
 											<RichTextInput
 												bind:this={chatInputElement}
@@ -886,7 +886,7 @@
 										<textarea
 											id="chat-input"
 											bind:this={chatInputElement}
-											class="scrollbar-hidden bg-transparent dark:text-gray-100 outline-none w-full pt-3 px-1 resize-none"
+											class="scrollbar-hidden bg-transparent dark:text-gray-100 outline-hidden w-full pt-3 px-1 resize-none"
 											placeholder={placeholder ? placeholder : $i18n.t('Send a Message')}
 											bind:value={prompt}
 											on:keypress={(e) => {
@@ -1114,7 +1114,7 @@
 											}}
 										>
 											<button
-												class="bg-transparent hover:bg-gray-100 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-gray-100 text-gray-800 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5 outline-hidden focus:outline-hidden"
 												type="button"
 												aria-label="More"
 											>
@@ -1138,10 +1138,10 @@
 														<button
 															on:click|preventDefault={() => (webSearchEnabled = !webSearchEnabled)}
 															type="button"
-															class="px-1.5 @sm:px-2.5 py-1.5 flex gap-1.5 items-center text-sm rounded-full font-medium transition-colors duration-300 focus:outline-none max-w-full overflow-hidden {webSearchEnabled ||
+															class="px-1.5 @sm:px-2.5 py-1.5 flex gap-1.5 items-center text-sm rounded-full font-medium transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden {webSearchEnabled ||
 															($settings?.webSearch ?? false) === 'always'
 																? 'bg-blue-100 dark:bg-blue-500/20 text-blue-500 dark:text-blue-400'
-																: 'bg-transparent text-gray-600 dark:text-gray-400 border-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800'}"
+																: 'bg-transparent text-gray-600 dark:text-gray-300 border-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800'}"
 														>
 															<GlobeAlt className="size-5" strokeWidth="1.75" />
 															<span
@@ -1158,7 +1158,7 @@
 															on:click|preventDefault={() =>
 																(imageGenerationEnabled = !imageGenerationEnabled)}
 															type="button"
-															class="px-1.5 @sm:px-2.5 py-1.5 flex gap-1.5 items-center text-sm rounded-full font-medium transition-colors duration-300 focus:outline-none max-w-full overflow-hidden {imageGenerationEnabled
+															class="px-1.5 @sm:px-2.5 py-1.5 flex gap-1.5 items-center text-sm rounded-full font-medium transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden {imageGenerationEnabled
 																? 'bg-gray-100 dark:bg-gray-500/20 text-gray-600 dark:text-gray-400'
 																: 'bg-transparent text-gray-600 dark:text-gray-300 border-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800 '}"
 														>
@@ -1177,7 +1177,7 @@
 															on:click|preventDefault={() =>
 																(codeInterpreterEnabled = !codeInterpreterEnabled)}
 															type="button"
-															class="px-1.5 @sm:px-2.5 py-1.5 flex gap-1.5 items-center text-sm rounded-full font-medium transition-colors duration-300 focus:outline-none max-w-full overflow-hidden {codeInterpreterEnabled
+															class="px-1.5 @sm:px-2.5 py-1.5 flex gap-1.5 items-center text-sm rounded-full font-medium transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden {codeInterpreterEnabled
 																? 'bg-gray-100 dark:bg-gray-500/20 text-gray-600 dark:text-gray-400'
 																: 'bg-transparent text-gray-600 dark:text-gray-300 border-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800 '}"
 														>
@@ -1193,7 +1193,7 @@
 										</div>
 									</div>
 
-									<div class="self-end flex space-x-1 mr-1 flex-shrink-0">
+									<div class="self-end flex space-x-1 mr-1 shrink-0">
 										{#if !history?.currentId || history.messages[history.currentId]?.done == true}
 											<Tooltip content={$i18n.t('Record voice')}>
 												<button

+ 1 - 1
src/lib/components/chat/MessageInput/CallOverlay/VideoInputMenu.svelte

@@ -26,7 +26,7 @@
 
 	<div slot="content">
 		<DropdownMenu.Content
-			class="w-full max-w-[180px] rounded-lg px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-[9999] bg-white dark:bg-gray-900 dark:text-white shadow-sm"
+			class="w-full max-w-[180px] rounded-lg px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-9999 bg-white dark:bg-gray-900 dark:text-white shadow-xs"
 			sideOffset={6}
 			side="top"
 			align="start"

+ 1 - 1
src/lib/components/chat/MessageInput/Commands.svelte

@@ -112,7 +112,7 @@
 			id="commands-container"
 			class="px-2 mb-2 text-left w-full absolute bottom-0 left-0 right-0 z-10"
 		>
-			<div class="flex w-full rounded-xl border border-gray-50 dark:border-gray-850">
+			<div class="flex w-full rounded-xl border border-gray-100 dark:border-gray-850">
 				<div
 					class="max-h-60 flex flex-col w-full rounded-xl bg-white dark:bg-gray-900 dark:text-gray-100"
 				>

+ 6 - 6
src/lib/components/chat/MessageInput/Commands/Knowledge.svelte

@@ -161,7 +161,7 @@
 		id="commands-container"
 		class="px-2 mb-2 text-left w-full absolute bottom-0 left-0 right-0 z-10"
 	>
-		<div class="flex w-full rounded-xl border border-gray-50 dark:border-gray-850">
+		<div class="flex w-full rounded-xl border border-gray-100 dark:border-gray-850">
 			<div
 				class="max-h-60 flex flex-col w-full rounded-xl bg-white dark:bg-gray-900 dark:text-gray-100"
 			>
@@ -185,25 +185,25 @@
 								<div class=" font-medium text-black dark:text-gray-100 flex items-center gap-1">
 									{#if item.legacy}
 										<div
-											class="bg-gray-500/20 text-gray-700 dark:text-gray-200 rounded uppercase text-xs font-bold px-1 flex-shrink-0"
+											class="bg-gray-500/20 text-gray-700 dark:text-gray-200 rounded-sm uppercase text-xs font-bold px-1 shrink-0"
 										>
 											Legacy
 										</div>
 									{:else if item?.meta?.document}
 										<div
-											class="bg-gray-500/20 text-gray-700 dark:text-gray-200 rounded uppercase text-xs font-bold px-1 flex-shrink-0"
+											class="bg-gray-500/20 text-gray-700 dark:text-gray-200 rounded-sm uppercase text-xs font-bold px-1 shrink-0"
 										>
 											Document
 										</div>
 									{:else if item?.type === 'file'}
 										<div
-											class="bg-gray-500/20 text-gray-700 dark:text-gray-200 rounded uppercase text-xs font-bold px-1 flex-shrink-0"
+											class="bg-gray-500/20 text-gray-700 dark:text-gray-200 rounded-sm uppercase text-xs font-bold px-1 shrink-0"
 										>
 											File
 										</div>
 									{:else}
 										<div
-											class="bg-green-500/20 text-green-700 dark:text-green-200 rounded uppercase text-xs font-bold px-1 flex-shrink-0"
+											class="bg-green-500/20 text-green-700 dark:text-green-200 rounded-sm uppercase text-xs font-bold px-1 shrink-0"
 										>
 											Collection
 										</div>
@@ -238,7 +238,7 @@
 													class=" font-medium text-black dark:text-gray-100 flex items-center gap-1"
 												>
 													<div
-														class="bg-gray-500/20 text-gray-700 dark:text-gray-200 rounded uppercase text-xs font-bold px-1 flex-shrink-0"
+														class="bg-gray-500/20 text-gray-700 dark:text-gray-200 rounded-sm uppercase text-xs font-bold px-1 shrink-0"
 													>
 														File
 													</div>

+ 1 - 1
src/lib/components/chat/MessageInput/Commands/Models.svelte

@@ -70,7 +70,7 @@
 		id="commands-container"
 		class="px-2 mb-2 text-left w-full absolute bottom-0 left-0 right-0 z-10"
 	>
-		<div class="flex w-full rounded-xl border border-gray-50 dark:border-gray-850">
+		<div class="flex w-full rounded-xl border border-gray-100 dark:border-gray-850">
 			<div
 				class="max-h-60 flex flex-col w-full rounded-xl bg-white dark:bg-gray-900 dark:text-gray-100"
 			>

+ 1 - 1
src/lib/components/chat/MessageInput/Commands/Prompts.svelte

@@ -139,7 +139,7 @@
 		id="commands-container"
 		class="px-2 mb-2 text-left w-full absolute bottom-0 left-0 right-0 z-10"
 	>
-		<div class="flex w-full rounded-xl border border-gray-50 dark:border-gray-850">
+		<div class="flex w-full rounded-xl border border-gray-100 dark:border-gray-850">
 			<div
 				class="max-h-60 flex flex-col w-full rounded-xl bg-white dark:bg-gray-900 dark:text-gray-100"
 			>

+ 2 - 2
src/lib/components/chat/MessageInput/FilesOverlay.svelte

@@ -19,12 +19,12 @@
 		bind:this={overlayElement}
 		class="fixed {$showSidebar
 			? 'left-0 md:left-[260px] md:w-[calc(100%-260px)]'
-			: 'left-0'}  fixed top-0 right-0 bottom-0 w-full h-full flex z-[9999] touch-none pointer-events-none"
+			: 'left-0'}  fixed top-0 right-0 bottom-0 w-full h-full flex z-9999 touch-none pointer-events-none"
 		id="dropzone"
 		role="region"
 		aria-label="Drag and Drop Container"
 	>
-		<div class="absolute w-full h-full backdrop-blur bg-gray-800/40 flex justify-center">
+		<div class="absolute w-full h-full backdrop-blur-sm bg-gray-800/40 flex justify-center">
 			<div class="m-auto pt-64 flex flex-col justify-center">
 				<div class="max-w-md">
 					<AddFilesPlaceholder />

+ 3 - 3
src/lib/components/chat/MessageInput/InputMenu.svelte

@@ -92,7 +92,7 @@
 
 	<div slot="content">
 		<DropdownMenu.Content
-			class="w-full max-w-[220px] 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-[220px] 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}
 			alignOffset={-8}
 			side="top"
@@ -114,7 +114,7 @@
 									placement="top-start"
 									className="flex flex-1 gap-2 items-center"
 								>
-									<div class="flex-shrink-0">
+									<div class="shrink-0">
 										<WrenchSolid />
 									</div>
 
@@ -122,7 +122,7 @@
 								</Tooltip>
 							</div>
 
-							<div class=" flex-shrink-0">
+							<div class=" shrink-0">
 								<Switch
 									state={tools[toolId].enabled}
 									on:change={async (e) => {

+ 1 - 1
src/lib/components/chat/MessageInput/VoiceRecording.svelte

@@ -362,7 +362,7 @@
 			{#each visualizerData.slice().reverse() as rms}
 				<div class="flex items-center h-full">
 					<div
-						class="w-[2px] flex-shrink-0
+						class="w-[2px] shrink-0
                     
                     {loading
 							? ' bg-gray-500 dark:bg-gray-400   '

+ 6 - 6
src/lib/components/chat/Messages/Citations.svelte

@@ -101,7 +101,7 @@
 				{#each citations as citation, idx}
 					<button
 						id={`source-${idx}`}
-						class="no-toggle outline-none flex dark:text-gray-300 p-1 bg-white dark:bg-gray-900 rounded-xl max-w-96"
+						class="no-toggle outline-hidden flex dark:text-gray-300 p-1 bg-white dark:bg-gray-900 rounded-xl max-w-96"
 						on:click={() => {
 							showCitationModal = true;
 							selectedCitation = citation;
@@ -133,14 +133,14 @@
 					<div
 						class="flex-1 flex items-center gap-1 overflow-auto scrollbar-none w-full max-w-full"
 					>
-						<span class="whitespace-nowrap hidden sm:inline flex-shrink-0"
+						<span class="whitespace-nowrap hidden sm:inline shrink-0"
 							>{$i18n.t('References from')}</span
 						>
 						<div class="flex items-center overflow-auto scrollbar-none w-full max-w-full flex-1">
 							<div class="flex text-xs font-medium items-center">
 								{#each citations.slice(0, 2) as citation, idx}
 									<button
-										class="no-toggle outline-none flex dark:text-gray-300 p-1 bg-gray-50 hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 transition rounded-xl max-w-96"
+										class="no-toggle outline-hidden flex dark:text-gray-300 p-1 bg-gray-50 hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 transition rounded-xl max-w-96"
 										on:click={() => {
 											showCitationModal = true;
 											selectedCitation = citation;
@@ -161,13 +161,13 @@
 								{/each}
 							</div>
 						</div>
-						<div class="flex items-center gap-1 whitespace-nowrap flex-shrink-0">
+						<div class="flex items-center gap-1 whitespace-nowrap shrink-0">
 							<span class="hidden sm:inline">{$i18n.t('and')}</span>
 							{citations.length - 2}
 							<span>{$i18n.t('more')}</span>
 						</div>
 					</div>
-					<div class="flex-shrink-0">
+					<div class="shrink-0">
 						{#if isCollapsibleOpen}
 							<ChevronUp strokeWidth="3.5" className="size-3.5" />
 						{:else}
@@ -180,7 +180,7 @@
 						{#each citations as citation, idx}
 							<button
 								id={`source-${idx}`}
-								class="no-toggle outline-none flex dark:text-gray-300 p-1 bg-gray-50 hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 transition rounded-xl max-w-96"
+								class="no-toggle outline-hidden flex dark:text-gray-300 p-1 bg-gray-50 hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 transition rounded-xl max-w-96"
 								on:click={() => {
 									showCitationModal = true;
 									selectedCitation = citation;

+ 5 - 3
src/lib/components/chat/Messages/CitationsModal.svelte

@@ -90,7 +90,7 @@
 							>
 								<div class="text-sm dark:text-gray-400 flex items-center gap-2 w-fit">
 									<a
-										class="hover:text-gray-500 hover:dark:text-gray-100 underline flex-grow"
+										class="hover:text-gray-500 dark:hover:text-gray-100 underline grow"
 										href={document?.metadata?.file_id
 											? `${WEBUI_API_BASE_URL}/files/${document?.metadata?.file_id}/content${document?.metadata?.page !== undefined ? `#page=${document.metadata.page + 1}` : ''}`
 											: document.source?.url?.includes('http')
@@ -122,7 +122,9 @@
 										<div class="text-sm my-1 dark:text-gray-400 flex items-center gap-2 w-fit">
 											{#if showPercentage}
 												{@const percentage = calculatePercentage(document.distance)}
-												<span class={`px-1 rounded font-medium ${getRelevanceColor(percentage)}`}>
+												<span
+													class={`px-1 rounded-sm font-medium ${getRelevanceColor(percentage)}`}
+												>
 													{percentage.toFixed(2)}%
 												</span>
 												<span class="text-gray-500 dark:text-gray-500">
@@ -166,7 +168,7 @@
 					</div>
 
 					{#if documentIdx !== mergedDocuments.length - 1}
-						<hr class=" dark:border-gray-850 my-3" />
+						<hr class="border-gray-100 dark:border-gray-850 my-3" />
 					{/if}
 				{/each}
 			</div>

+ 2 - 2
src/lib/components/chat/Messages/CodeBlock.svelte

@@ -302,7 +302,7 @@
 		{#if lang === 'mermaid'}
 			{#if mermaidHtml}
 				<SvgPanZoom
-					className=" border border-gray-50 dark:border-gray-850 rounded-lg max-h-fit overflow-hidden"
+					className=" border border-gray-100 dark:border-gray-850 rounded-lg max-h-fit overflow-hidden"
 					svg={mermaidHtml}
 					content={_token.text}
 				/>
@@ -377,7 +377,7 @@
 
 			{#if executing || stdout || stderr || result}
 				<div
-					class="bg-gray-50 dark:bg-[#202123] dark:text-white !rounded-b-lg py-4 px-4 flex flex-col gap-2"
+					class="bg-gray-50 dark:bg-[#202123] dark:text-white rounded-b-lg! py-4 px-4 flex flex-col gap-2"
 				>
 					{#if executing}
 						<div class=" ">

+ 1 - 1
src/lib/components/chat/Messages/CodeExecutionModal.svelte

@@ -99,7 +99,7 @@
 				{/if}
 				{#if codeExecution?.result?.files && codeExecution?.result?.files.length > 0}
 					<div class="flex flex-col w-full">
-						<hr class=" dark:border-gray-850 my-2" />
+						<hr class="border-gray-100 dark:border-gray-850 my-2" />
 						<div class=" text-sm font-medium dark:text-gray-300">
 							{$i18n.t('Files')}
 						</div>

+ 3 - 9
src/lib/components/chat/Messages/Markdown/MarkdownInlineTokens.svelte

@@ -44,13 +44,9 @@
 	{:else if token.type === 'image'}
 		<Image src={token.href} alt={token.text} />
 	{:else if token.type === 'strong'}
-		<strong>
-			<svelte:self id={`${id}-strong`} tokens={token.tokens} {onSourceClick} />
-		</strong>
+		<strong><svelte:self id={`${id}-strong`} tokens={token.tokens} {onSourceClick} /></strong>
 	{:else if token.type === 'em'}
-		<em>
-			<svelte:self id={`${id}-em`} tokens={token.tokens} {onSourceClick} />
-		</em>
+		<em><svelte:self id={`${id}-em`} tokens={token.tokens} {onSourceClick} /></em>
 	{:else if token.type === 'codespan'}
 		<!-- svelte-ignore a11y-click-events-have-key-events -->
 		<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
@@ -64,9 +60,7 @@
 	{:else if token.type === 'br'}
 		<br />
 	{:else if token.type === 'del'}
-		<del>
-			<svelte:self id={`${id}-del`} tokens={token.tokens} {onSourceClick} />
-		</del>
+		<del><svelte:self id={`${id}-del`} tokens={token.tokens} {onSourceClick} /></del>
 	{:else if token.type === 'inlineKatex'}
 		{#if token.text}
 			<KatexRenderer content={token.text} displayMode={false} />

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

@@ -74,7 +74,7 @@
 <!-- {JSON.stringify(tokens)} -->
 {#each tokens as token, tokenIdx (tokenIdx)}
 	{#if token.type === 'hr'}
-		<hr class=" border-gray-50 dark:border-gray-850" />
+		<hr class=" border-gray-100 dark:border-gray-850" />
 	{:else if token.type === 'heading'}
 		<svelte:element this={headerComponent(token.depth)}>
 			<MarkdownInlineTokens id={`${id}-${tokenIdx}-h`} tokens={token.tokens} {onSourceClick} />
@@ -115,11 +115,11 @@
 							{#each token.header as header, headerIdx}
 								<th
 									scope="col"
-									class="!px-3 !py-1.5 cursor-pointer border border-gray-50 dark:border-gray-850"
+									class="px-3! py-1.5! cursor-pointer border border-gray-100 dark:border-gray-850"
 									style={token.align[headerIdx] ? '' : `text-align: ${token.align[headerIdx]}`}
 								>
 									<div class="flex flex-col gap-1.5 text-start">
-										<div class="flex-shrink-0 break-normal">
+										<div class="shrink-0 break-normal">
 											<MarkdownInlineTokens
 												id={`${id}-${tokenIdx}-header-${headerIdx}`}
 												tokens={header.tokens}
@@ -136,7 +136,7 @@
 							<tr class="bg-white dark:bg-gray-900 dark:border-gray-850 text-xs">
 								{#each row ?? [] as cell, cellIdx}
 									<td
-										class="!px-3 !py-1.5 text-gray-900 dark:text-white w-max border border-gray-50 dark:border-gray-850"
+										class="px-3! py-1.5! text-gray-900 dark:text-white w-max border border-gray-100 dark:border-gray-850"
 										style={token.align[cellIdx] ? '' : `text-align: ${token.align[cellIdx]}`}
 									>
 										<div class="flex flex-col break-normal">

+ 16 - 1
src/lib/components/chat/Messages/Markdown/Source.svelte

@@ -17,6 +17,21 @@
 		return attrs;
 	}
 
+	// Helper function to return only the domain from a URL
+	function getDomain(url: string): string {
+		const domain = url.replace('http://', '').replace('https://', '').split(/[/?#]/)[0];
+		return domain;
+	}
+
+	// Helper function to check if text is a URL and return the domain
+	function formattedTitle(title: string): string {
+		if (title.startsWith('http')) {
+			return getDomain(title);
+		}
+
+		return title;
+	}
+
 	$: attributes = extractAttributes(token.text);
 </script>
 
@@ -27,6 +42,6 @@
 	}}
 >
 	<span class="line-clamp-1">
-		{attributes.title}
+		{formattedTitle(attributes.title)}
 	</span>
 </button>

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

@@ -194,10 +194,10 @@
 					<div
 						class=" snap-center w-full max-w-full m-1 border {history.messages[messageId]
 							?.modelIdx == modelIdx
-							? `border-gray-100 dark:border-gray-800 border-[1.5px] ${
+							? `border-gray-100 dark:border-gray-850 border-[1.5px] ${
 									$mobile ? 'min-w-full' : 'min-w-80'
 								}`
-							: `border-gray-50 dark:border-gray-850 border-dashed ${
+							: `border-gray-100 dark:border-gray-850 border-dashed ${
 									$mobile ? 'min-w-full' : 'min-w-80'
 								}`} transition-all p-5 rounded-2xl"
 						on:click={async () => {
@@ -286,7 +286,7 @@
 					</div>
 
 					{#if isLastMessage}
-						<div class=" flex-shrink-0 text-gray-600 dark:text-gray-500 mt-1">
+						<div class=" shrink-0 text-gray-600 dark:text-gray-500 mt-1">
 							<Tooltip content={$i18n.t('Merge Responses')} placement="bottom">
 								<button
 									type="button"

+ 5 - 5
src/lib/components/chat/Messages/RateComment.svelte

@@ -110,7 +110,7 @@
 {/if}
 
 <div
-	class=" my-2.5 rounded-xl px-4 py-3 border border-gray-50 dark:border-gray-850"
+	class=" my-2.5 rounded-xl px-4 py-3 border border-gray-100 dark:border-gray-850"
 	id="message-feedback-{message.id}"
 >
 	<div class="flex justify-between items-center">
@@ -142,10 +142,10 @@
 				<!-- 1-10 scale -->
 				{#each Array.from({ length: 10 }).map((_, i) => i + 1) as rating}
 					<button
-						class="size-7 text-sm border border-gray-50 dark:border-gray-850 hover:bg-gray-50 dark:hover:bg-gray-850 {detailedRating ===
+						class="size-7 text-sm border border-gray-100 dark:border-gray-850 hover:bg-gray-50 dark:hover:bg-gray-850 {detailedRating ===
 						rating
 							? 'bg-gray-100 dark:bg-gray-800'
-							: ''} transition rounded-full disabled:cursor-not-allowed disabled:text-gray-500 disabled:bg-white disabled:dark:bg-gray-900"
+							: ''} transition rounded-full disabled:cursor-not-allowed disabled:text-gray-500 disabled:bg-white dark:disabled:bg-gray-900"
 						on:click={() => {
 							detailedRating = rating;
 						}}
@@ -175,7 +175,7 @@
 			<div class="flex flex-wrap gap-1.5 text-sm mt-1.5">
 				{#each reasons as reason}
 					<button
-						class="px-3 py-0.5 border border-gray-50 dark:border-gray-850 hover:bg-gray-50 dark:hover:bg-gray-850 {selectedReason ===
+						class="px-3 py-0.5 border border-gray-100 dark:border-gray-850 hover:bg-gray-50 dark:hover:bg-gray-850 {selectedReason ===
 						reason
 							? 'bg-gray-100 dark:bg-gray-800'
 							: ''} transition rounded-xl"
@@ -223,7 +223,7 @@
 	<div class="mt-2">
 		<textarea
 			bind:value={comment}
-			class="w-full text-sm px-1 py-2 bg-transparent outline-none resize-none rounded-xl"
+			class="w-full text-sm px-1 py-2 bg-transparent outline-hidden resize-none rounded-xl"
 			placeholder={$i18n.t('Feel free to add specific details')}
 			rows="3"
 		/>

+ 4 - 4
src/lib/components/chat/Messages/ResponseMessage.svelte

@@ -15,7 +15,7 @@
 	import { getChatById } from '$lib/apis/chats';
 	import { generateTags } from '$lib/apis';
 
-	import { config, models, settings, TTSWorker, user } from '$lib/stores';
+	import { config, models, settings, temporaryChatEnabled, TTSWorker, user } from '$lib/stores';
 	import { synthesizeOpenAISpeech } from '$lib/apis/audio';
 	import { imageGenerations } from '$lib/apis/images';
 	import {
@@ -522,7 +522,7 @@
 		id="message-{message.id}"
 		dir={$settings.chatDirection}
 	>
-		<div class={`flex-shrink-0 ${($settings?.chatDirection ?? 'LTR') === 'LTR' ? 'mr-3' : 'ml-3'}`}>
+		<div class={`shrink-0 ${($settings?.chatDirection ?? 'LTR') === 'LTR' ? 'mr-3' : 'ml-3'}`}>
 			<ProfileImage
 				src={model?.info?.meta?.profile_image_url ??
 					($i18n.language === 'dg-DG' ? `/doge.png` : `${WEBUI_BASE_URL}/static/favicon.png`)}
@@ -645,7 +645,7 @@
 								<textarea
 									id="message-edit-{message.id}"
 									bind:this={editTextAreaElement}
-									class=" bg-transparent outline-none w-full resize-none"
+									class=" bg-transparent outline-hidden w-full resize-none"
 									bind:value={editedContent}
 									on:input={(e) => {
 										e.target.style.height = '';
@@ -1089,7 +1089,7 @@
 								{/if}
 
 								{#if !readOnly}
-									{#if $config?.features.enable_message_rating ?? true}
+									{#if !$temporaryChatEnabled && ($config?.features.enable_message_rating ?? true)}
 										<Tooltip content={$i18n.t('Good Response')} placement="bottom">
 											<button
 												class="{isLastMessage

+ 7 - 7
src/lib/components/chat/Messages/Skeleton.svelte

@@ -5,31 +5,31 @@
 <div class="w-full mt-2 mb-2">
 	<div class="animate-pulse flex w-full">
 		<div class="{size === 'md' ? 'space-y-2' : 'space-y-1.5'} w-full">
-			<div class="{size === 'md' ? 'h-2' : 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded mr-14" />
+			<div class="{size === 'md' ? 'h-2' : 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded-sm mr-14" />
 
 			<div class="grid grid-cols-3 gap-4">
 				<div
-					class="{size === 'md' ? 'h-2' : 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded col-span-2"
+					class="{size === 'md' ? 'h-2' : 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded-sm col-span-2"
 				/>
 				<div
-					class="{size === 'md' ? 'h-2' : 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded col-span-1"
+					class="{size === 'md' ? 'h-2' : 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded-sm col-span-1"
 				/>
 			</div>
 			<div class="grid grid-cols-4 gap-4">
 				<div
-					class="{size === 'md' ? 'h-2' : 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded col-span-1"
+					class="{size === 'md' ? 'h-2' : 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded-sm col-span-1"
 				/>
 				<div
-					class="{size === 'md' ? 'h-2' : 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded col-span-2"
+					class="{size === 'md' ? 'h-2' : 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded-sm col-span-2"
 				/>
 				<div
 					class="{size === 'md'
 						? 'h-2'
-						: 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded col-span-1 mr-4"
+						: 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded-sm col-span-1 mr-4"
 				/>
 			</div>
 
-			<div class="{size === 'md' ? 'h-2' : 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded" />
+			<div class="{size === 'md' ? 'h-2' : 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded-sm" />
 		</div>
 	</div>
 </div>

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

@@ -87,7 +87,7 @@
 
 <div class=" flex w-full user-message" dir={$settings.chatDirection} id="message-{message.id}">
 	{#if !($settings?.chatBubble ?? true)}
-		<div class={`flex-shrink-0 ${($settings?.chatDirection ?? 'LTR') === 'LTR' ? 'mr-3' : 'ml-3'}`}>
+		<div class={`shrink-0 ${($settings?.chatDirection ?? 'LTR') === 'LTR' ? 'mr-3' : 'ml-3'}`}>
 			<ProfileImage
 				src={message.user
 					? ($models.find((m) => m.id === message.user)?.info?.meta?.profile_image_url ??
@@ -152,7 +152,7 @@
 							<textarea
 								id="message-edit-{message.id}"
 								bind:this={messageEditTextAreaElement}
-								class=" bg-transparent outline-none w-full resize-none"
+								class=" bg-transparent outline-hidden w-full resize-none"
 								bind:value={editedContent}
 								on:input={(e) => {
 									e.target.style.height = '';
@@ -338,7 +338,7 @@
 							{#if !isFirstMessage && !readOnly}
 								<Tooltip content={$i18n.t('Delete')} placement="bottom">
 									<button
-										class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition"
+										class="invisible group-hover:visible p-1 rounded-sm dark:hover:text-white hover:text-black transition"
 										on:click={() => {
 											deleteMessageHandler();
 										}}

+ 12 - 12
src/lib/components/chat/ModelSelector/Selector.svelte

@@ -248,7 +248,7 @@
 		id="model-selector-{id}-button"
 	>
 		<div
-			class="flex w-full text-left px-0.5 outline-none bg-transparent truncate {triggerClassName} justify-between font-medium placeholder-gray-400 focus:outline-none"
+			class="flex w-full text-left px-0.5 outline-hidden bg-transparent truncate {triggerClassName} justify-between font-medium placeholder-gray-400 focus:outline-hidden"
 		>
 			{#if selectedModel}
 				{selectedModel.label}
@@ -262,7 +262,7 @@
 	<DropdownMenu.Content
 		class=" z-40 {$mobile
 			? `w-full`
-			: `${className}`} max-w-[calc(100vw-1rem)] justify-start rounded-xl  bg-white dark:bg-gray-850 dark:text-white shadow-lg  outline-none"
+			: `${className}`} max-w-[calc(100vw-1rem)] justify-start rounded-xl  bg-white dark:bg-gray-850 dark:text-white shadow-lg  outline-hidden"
 		transition={flyAndScale}
 		side={$mobile ? 'bottom' : 'bottom-start'}
 		sideOffset={3}
@@ -275,7 +275,7 @@
 					<input
 						id="model-search-input"
 						bind:value={searchValue}
-						class="w-full text-sm bg-transparent outline-none"
+						class="w-full text-sm bg-transparent outline-hidden"
 						placeholder={searchPlaceholder}
 						autocomplete="off"
 						on:keydown={(e) => {
@@ -298,14 +298,14 @@
 					/>
 				</div>
 
-				<hr class="border-gray-50 dark:border-gray-800" />
+				<hr class="border-gray-100 dark:border-gray-850" />
 			{/if}
 
 			<div class="px-3 my-2 max-h-64 overflow-y-auto scrollbar-hidden group">
 				{#each filteredItems as item, index}
 					<button
 						aria-label="model-item"
-						class="flex w-full text-left font-medium line-clamp-1 select-none items-center rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-none transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg cursor-pointer data-[highlighted]:bg-muted {index ===
+						class="flex w-full text-left font-medium line-clamp-1 select-none items-center rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-hidden transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg cursor-pointer data-highlighted:bg-muted {index ===
 						selectedModelIdx
 							? 'bg-gray-100 dark:bg-gray-800 group-hover:bg-transparent'
 							: ''}"
@@ -322,7 +322,7 @@
 								<div class="flex gap-0.5 self-start h-full mb-1.5 -translate-x-1">
 									{#each item.model?.info?.meta.tags as tag}
 										<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"
 										>
 											{tag.name}
 										</div>
@@ -445,7 +445,7 @@
 										{#each item.model?.info?.meta.tags as tag}
 											<Tooltip content={tag.name}>
 												<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"
 												>
 													{tag.name}
 												</div>
@@ -478,7 +478,7 @@
 						placement="top-start"
 					>
 						<button
-							class="flex w-full font-medium line-clamp-1 select-none items-center rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-none transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg cursor-pointer data-[highlighted]:bg-muted"
+							class="flex w-full font-medium line-clamp-1 select-none items-center rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-hidden transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg cursor-pointer data-highlighted:bg-muted"
 							on:click={() => {
 								pullModelHandler();
 							}}
@@ -492,7 +492,7 @@
 
 				{#each Object.keys($MODEL_DOWNLOAD_POOL) as model}
 					<div
-						class="flex w-full justify-between font-medium select-none rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-none transition-all duration-75 rounded-lg cursor-pointer data-[highlighted]:bg-muted"
+						class="flex w-full justify-between font-medium select-none rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-hidden transition-all duration-75 rounded-lg cursor-pointer data-highlighted:bg-muted"
 					>
 						<div class="flex">
 							<div class="-ml-2 mr-2.5 translate-y-0.5">
@@ -527,7 +527,7 @@
 										Downloading "{model}"
 									</div>
 
-									<div class="flex-shrink-0">
+									<div class="shrink-0">
 										{'pullProgress' in $MODEL_DOWNLOAD_POOL[model]
 											? `(${$MODEL_DOWNLOAD_POOL[model].pullProgress}%)`
 											: ''}
@@ -575,11 +575,11 @@
 			</div>
 
 			{#if showTemporaryChatControl}
-				<hr class="border-gray-50 dark:border-gray-800" />
+				<hr class="border-gray-100 dark:border-gray-850" />
 
 				<div class="flex items-center mx-2 my-2">
 					<button
-						class="flex justify-between w-full font-medium line-clamp-1 select-none items-center rounded-button py-2 px-3 text-sm text-gray-700 dark:text-gray-100 outline-none transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg cursor-pointer data-[highlighted]:bg-muted"
+						class="flex justify-between w-full font-medium line-clamp-1 select-none items-center rounded-button py-2 px-3 text-sm text-gray-700 dark:text-gray-100 outline-hidden transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg cursor-pointer data-highlighted:bg-muted"
 						on:click={async () => {
 							temporaryChatEnabled.set(!$temporaryChatEnabled);
 							await goto('/');

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

@@ -45,7 +45,7 @@
 
 <nav class="sticky top-0 z-30 w-full px-1.5 py-1.5 -mb-8 flex items-center drag-region">
 	<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 class=" flex max-w-full w-full mx-auto px-1 pt-0.5 bg-transparent">

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

@@ -106,7 +106,7 @@
 	>
 		<div class="w-full flex flex-col justify-center items-center">
 			<div class="flex flex-row justify-center gap-3 @sm:gap-3.5 w-fit px-5">
-				<div class="flex flex-shrink-0 justify-center">
+				<div class="flex shrink-0 justify-center">
 					<div class="flex -space-x-4 mb-0.5" in:fade={{ duration: 100 }}>
 						{#each models as model, modelIdx}
 							<Tooltip

+ 2 - 2
src/lib/components/chat/Settings/About.svelte

@@ -92,7 +92,7 @@
 		</div>
 
 		{#if ollamaVersion}
-			<hr class=" dark:border-gray-850" />
+			<hr class=" border-gray-100 dark:border-gray-850" />
 
 			<div>
 				<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Ollama Version')}</div>
@@ -104,7 +104,7 @@
 			</div>
 		{/if}
 
-		<hr class=" dark:border-gray-850" />
+		<hr class=" border-gray-100 dark:border-gray-850" />
 
 		<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
 			Emoji graphics provided by

+ 3 - 3
src/lib/components/chat/Settings/Account.svelte

@@ -236,7 +236,7 @@
 
 					<div class="flex-1">
 						<input
-							class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
+							class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden"
 							type="text"
 							bind:value={name}
 							required
@@ -251,7 +251,7 @@
 
 					<div class="flex-1">
 						<input
-							class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
+							class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden"
 							type="url"
 							placeholder={$i18n.t('Enter your webhook URL')}
 							bind:value={webhookUrl}
@@ -266,7 +266,7 @@
 			<UpdatePassword />
 		</div>
 
-		<hr class=" dark:border-gray-850 my-4" />
+		<hr class="border-gray-100 dark:border-gray-850 my-4" />
 
 		<div class="flex justify-between items-center text-sm">
 			<div class="  font-medium">{$i18n.t('API keys')}</div>

+ 3 - 3
src/lib/components/chat/Settings/Account/UpdatePassword.svelte

@@ -60,7 +60,7 @@
 
 				<div class="flex-1">
 					<input
-						class="w-full bg-transparent dark:text-gray-300 outline-none placeholder:opacity-30"
+						class="w-full bg-transparent dark:text-gray-300 outline-hidden placeholder:opacity-30"
 						type="password"
 						bind:value={currentPassword}
 						placeholder={$i18n.t('Enter your current password')}
@@ -75,7 +75,7 @@
 
 				<div class="flex-1">
 					<input
-						class="w-full bg-transparent text-sm dark:text-gray-300 outline-none placeholder:opacity-30"
+						class="w-full bg-transparent text-sm dark:text-gray-300 outline-hidden placeholder:opacity-30"
 						type="password"
 						bind:value={newPassword}
 						placeholder={$i18n.t('Enter your new password')}
@@ -90,7 +90,7 @@
 
 				<div class="flex-1">
 					<input
-						class="w-full bg-transparent text-sm dark:text-gray-300 outline-none placeholder:opacity-30"
+						class="w-full bg-transparent text-sm dark:text-gray-300 outline-hidden placeholder:opacity-30"
 						type="password"
 						bind:value={newPasswordConfirm}
 						placeholder={$i18n.t('Confirm your new password')}

部分文件因为文件数量过多而无法显示