Переглянути джерело

Merge branch 'dev' into update-ga-strings-june

Aindriú Mac Giolla Eoin 9 місяців тому
батько
коміт
34ec073b3d
100 змінених файлів з 1431 додано та 1232 видалено
  1. 52 5
      backend/open_webui/config.py
  2. 39 5
      backend/open_webui/env.py
  3. 19 13
      backend/open_webui/internal/db.py
  4. 44 4
      backend/open_webui/main.py
  5. 12 2
      backend/open_webui/models/chats.py
  6. 3 1
      backend/open_webui/retrieval/loaders/main.py
  7. 44 36
      backend/open_webui/retrieval/utils.py
  8. 6 3
      backend/open_webui/retrieval/vector/dbs/opensearch.py
  9. 21 1
      backend/open_webui/retrieval/vector/dbs/qdrant.py
  10. 21 1
      backend/open_webui/retrieval/vector/dbs/qdrant_multitenancy.py
  11. 3 1
      backend/open_webui/retrieval/web/brave.py
  12. 14 9
      backend/open_webui/routers/audio.py
  13. 1 0
      backend/open_webui/routers/auths.py
  14. 4 2
      backend/open_webui/routers/chats.py
  15. 14 7
      backend/open_webui/routers/configs.py
  16. 8 7
      backend/open_webui/routers/files.py
  17. 22 6
      backend/open_webui/routers/images.py
  18. 39 37
      backend/open_webui/routers/ollama.py
  19. 55 44
      backend/open_webui/routers/openai.py
  20. 17 0
      backend/open_webui/routers/retrieval.py
  21. 22 4
      backend/open_webui/socket/main.py
  22. 1 1
      backend/open_webui/utils/chat.py
  23. 34 26
      backend/open_webui/utils/middleware.py
  24. 10 2
      backend/open_webui/utils/models.py
  25. 28 1
      backend/open_webui/utils/telemetry/setup.py
  26. 0 3
      backend/open_webui/utils/tools.py
  27. 5 5
      backend/requirements.txt
  28. 1 1
      backend/start_windows.bat
  29. 24 0
      docker-compose.otel.yaml
  30. 42 8
      package-lock.json
  31. 5 2
      package.json
  32. 4 4
      pyproject.toml
  33. 2 2
      scripts/prepare-pyodide.js
  34. 10 32
      src/app.html
  35. 2 0
      src/lib/apis/auths/index.ts
  36. 6 2
      src/lib/apis/chats/index.ts
  37. 4 4
      src/lib/apis/configs/index.ts
  38. 18 8
      src/lib/apis/index.ts
  39. 1 0
      src/lib/apis/users/index.ts
  40. 4 34
      src/lib/components/AddConnectionModal.svelte
  41. 49 47
      src/lib/components/AddServerModal.svelte
  42. 4 10
      src/lib/components/ChangelogModal.svelte
  43. 4 33
      src/lib/components/ImportModal.svelte
  44. 95 38
      src/lib/components/admin/Evaluations/FeedbackModal.svelte
  45. 2 2
      src/lib/components/admin/Evaluations/Feedbacks.svelte
  46. 6 6
      src/lib/components/admin/Evaluations/Leaderboard.svelte
  47. 6 14
      src/lib/components/admin/Evaluations/LeaderboardModal.svelte
  48. 15 5
      src/lib/components/admin/Functions.svelte
  49. 2 27
      src/lib/components/admin/Settings/Audio.svelte
  50. 160 137
      src/lib/components/admin/Settings/Connections.svelte
  51. 2 10
      src/lib/components/admin/Settings/Connections/ManageOllamaModal.svelte
  52. 7 33
      src/lib/components/admin/Settings/Documents.svelte
  53. 4 33
      src/lib/components/admin/Settings/Evaluations/ArenaModelModal.svelte
  54. 14 10
      src/lib/components/admin/Settings/General.svelte
  55. 9 29
      src/lib/components/admin/Settings/Images.svelte
  56. 3 3
      src/lib/components/admin/Settings/Interface.svelte
  57. 17 16
      src/lib/components/admin/Settings/Interface/Banners.svelte
  58. 1 1
      src/lib/components/admin/Settings/Models.svelte
  59. 4 34
      src/lib/components/admin/Settings/Models/ConfigureModelsModal.svelte
  60. 1 1
      src/lib/components/admin/Settings/Models/Manage/ManageOllama.svelte
  61. 2 10
      src/lib/components/admin/Settings/Models/ManageModelsModal.svelte
  62. 1 1
      src/lib/components/admin/Settings/WebSearch.svelte
  63. 2 12
      src/lib/components/admin/Users/Groups.svelte
  64. 4 33
      src/lib/components/admin/Users/Groups/AddGroupModal.svelte
  65. 4 33
      src/lib/components/admin/Users/Groups/EditGroupModal.svelte
  66. 2 12
      src/lib/components/admin/Users/Groups/Users.svelte
  67. 1 1
      src/lib/components/admin/Users/UserList.svelte
  68. 4 33
      src/lib/components/admin/Users/UserList/AddUserModal.svelte
  69. 2 10
      src/lib/components/admin/Users/UserList/EditUserModal.svelte
  70. 46 34
      src/lib/components/channel/MessageInput.svelte
  71. 11 10
      src/lib/components/chat/Chat.svelte
  72. 7 3
      src/lib/components/chat/ChatPlaceholder.svelte
  73. 4 2
      src/lib/components/chat/Controls/Controls.svelte
  74. 61 38
      src/lib/components/chat/MessageInput.svelte
  75. 1 0
      src/lib/components/chat/MessageInput/CallOverlay.svelte
  76. 34 8
      src/lib/components/chat/MessageInput/Commands/Knowledge.svelte
  77. 28 1
      src/lib/components/chat/MessageInput/Commands/Models.svelte
  78. 30 1
      src/lib/components/chat/MessageInput/Commands/Prompts.svelte
  79. 2 10
      src/lib/components/chat/MessageInput/VoiceRecording.svelte
  80. 1 0
      src/lib/components/chat/Messages.svelte
  81. 0 1
      src/lib/components/chat/Messages/Citations.svelte
  82. 3 10
      src/lib/components/chat/Messages/CitationsModal.svelte
  83. 2 10
      src/lib/components/chat/Messages/CodeExecutionModal.svelte
  84. 11 6
      src/lib/components/chat/Messages/ContentRenderer.svelte
  85. 2 1
      src/lib/components/chat/Messages/Markdown.svelte
  86. 3 0
      src/lib/components/chat/Messages/Message.svelte
  87. 2 0
      src/lib/components/chat/Messages/MultiResponseMessages.svelte
  88. 2 10
      src/lib/components/chat/Messages/RateComment.svelte
  89. 4 2
      src/lib/components/chat/Messages/ResponseMessage.svelte
  90. 2 2
      src/lib/components/chat/Messages/ResponseMessage/WebSearchResults.svelte
  91. 24 39
      src/lib/components/chat/Messages/Skeleton.svelte
  92. 1 1
      src/lib/components/chat/Messages/UserMessage.svelte
  93. 1 1
      src/lib/components/chat/ModelSelector.svelte
  94. 11 28
      src/lib/components/chat/ModelSelector/Selector.svelte
  95. 4 4
      src/lib/components/chat/Navbar.svelte
  96. 11 5
      src/lib/components/chat/Placeholder.svelte
  97. 13 9
      src/lib/components/chat/Settings/About.svelte
  98. 14 5
      src/lib/components/chat/Settings/Chats.svelte
  99. 4 1
      src/lib/components/chat/Settings/General.svelte
  100. 4 33
      src/lib/components/chat/Settings/Personalization/AddMemoryModal.svelte

+ 52 - 5
backend/open_webui/config.py

@@ -19,6 +19,7 @@ from open_webui.env import (
     DATABASE_URL,
     DATABASE_URL,
     ENV,
     ENV,
     REDIS_URL,
     REDIS_URL,
+    REDIS_KEY_PREFIX,
     REDIS_SENTINEL_HOSTS,
     REDIS_SENTINEL_HOSTS,
     REDIS_SENTINEL_PORT,
     REDIS_SENTINEL_PORT,
     FRONTEND_BUILD_DIR,
     FRONTEND_BUILD_DIR,
@@ -211,11 +212,16 @@ class PersistentConfig(Generic[T]):
 class AppConfig:
 class AppConfig:
     _state: dict[str, PersistentConfig]
     _state: dict[str, PersistentConfig]
     _redis: Optional[redis.Redis] = None
     _redis: Optional[redis.Redis] = None
+    _redis_key_prefix: str
 
 
     def __init__(
     def __init__(
-        self, redis_url: Optional[str] = None, redis_sentinels: Optional[list] = []
+        self,
+        redis_url: Optional[str] = None,
+        redis_sentinels: Optional[list] = [],
+        redis_key_prefix: str = "open-webui",
     ):
     ):
         super().__setattr__("_state", {})
         super().__setattr__("_state", {})
+        super().__setattr__("_redis_key_prefix", redis_key_prefix)
         if redis_url:
         if redis_url:
             super().__setattr__(
             super().__setattr__(
                 "_redis",
                 "_redis",
@@ -230,7 +236,7 @@ class AppConfig:
             self._state[key].save()
             self._state[key].save()
 
 
             if self._redis:
             if self._redis:
-                redis_key = f"open-webui:config:{key}"
+                redis_key = f"{self._redis_key_prefix}:config:{key}"
                 self._redis.set(redis_key, json.dumps(self._state[key].value))
                 self._redis.set(redis_key, json.dumps(self._state[key].value))
 
 
     def __getattr__(self, key):
     def __getattr__(self, key):
@@ -239,7 +245,7 @@ class AppConfig:
 
 
         # If Redis is available, check for an updated value
         # If Redis is available, check for an updated value
         if self._redis:
         if self._redis:
-            redis_key = f"open-webui:config:{key}"
+            redis_key = f"{self._redis_key_prefix}:config:{key}"
             redis_value = self._redis.get(redis_key)
             redis_value = self._redis.get(redis_key)
 
 
             if redis_value is not None:
             if redis_value is not None:
@@ -431,6 +437,12 @@ OAUTH_SCOPES = PersistentConfig(
     os.environ.get("OAUTH_SCOPES", "openid email profile"),
     os.environ.get("OAUTH_SCOPES", "openid email profile"),
 )
 )
 
 
+OAUTH_TIMEOUT = PersistentConfig(
+    "OAUTH_TIMEOUT",
+    "oauth.oidc.oauth_timeout",
+    os.environ.get("OAUTH_TIMEOUT", ""),
+)
+
 OAUTH_CODE_CHALLENGE_METHOD = PersistentConfig(
 OAUTH_CODE_CHALLENGE_METHOD = PersistentConfig(
     "OAUTH_CODE_CHALLENGE_METHOD",
     "OAUTH_CODE_CHALLENGE_METHOD",
     "oauth.oidc.code_challenge_method",
     "oauth.oidc.code_challenge_method",
@@ -540,7 +552,14 @@ def load_oauth_providers():
                 client_id=GOOGLE_CLIENT_ID.value,
                 client_id=GOOGLE_CLIENT_ID.value,
                 client_secret=GOOGLE_CLIENT_SECRET.value,
                 client_secret=GOOGLE_CLIENT_SECRET.value,
                 server_metadata_url="https://accounts.google.com/.well-known/openid-configuration",
                 server_metadata_url="https://accounts.google.com/.well-known/openid-configuration",
-                client_kwargs={"scope": GOOGLE_OAUTH_SCOPE.value},
+                client_kwargs={
+                    "scope": GOOGLE_OAUTH_SCOPE.value,
+                    **(
+                        {"timeout": int(OAUTH_TIMEOUT.value)}
+                        if OAUTH_TIMEOUT.value
+                        else {}
+                    ),
+                },
                 redirect_uri=GOOGLE_REDIRECT_URI.value,
                 redirect_uri=GOOGLE_REDIRECT_URI.value,
             )
             )
 
 
@@ -563,6 +582,11 @@ def load_oauth_providers():
                 server_metadata_url=f"{MICROSOFT_CLIENT_LOGIN_BASE_URL.value}/{MICROSOFT_CLIENT_TENANT_ID.value}/v2.0/.well-known/openid-configuration?appid={MICROSOFT_CLIENT_ID.value}",
                 server_metadata_url=f"{MICROSOFT_CLIENT_LOGIN_BASE_URL.value}/{MICROSOFT_CLIENT_TENANT_ID.value}/v2.0/.well-known/openid-configuration?appid={MICROSOFT_CLIENT_ID.value}",
                 client_kwargs={
                 client_kwargs={
                     "scope": MICROSOFT_OAUTH_SCOPE.value,
                     "scope": MICROSOFT_OAUTH_SCOPE.value,
+                    **(
+                        {"timeout": int(OAUTH_TIMEOUT.value)}
+                        if OAUTH_TIMEOUT.value
+                        else {}
+                    ),
                 },
                 },
                 redirect_uri=MICROSOFT_REDIRECT_URI.value,
                 redirect_uri=MICROSOFT_REDIRECT_URI.value,
             )
             )
@@ -584,7 +608,14 @@ def load_oauth_providers():
                 authorize_url="https://github.com/login/oauth/authorize",
                 authorize_url="https://github.com/login/oauth/authorize",
                 api_base_url="https://api.github.com",
                 api_base_url="https://api.github.com",
                 userinfo_endpoint="https://api.github.com/user",
                 userinfo_endpoint="https://api.github.com/user",
-                client_kwargs={"scope": GITHUB_CLIENT_SCOPE.value},
+                client_kwargs={
+                    "scope": GITHUB_CLIENT_SCOPE.value,
+                    **(
+                        {"timeout": int(OAUTH_TIMEOUT.value)}
+                        if OAUTH_TIMEOUT.value
+                        else {}
+                    ),
+                },
                 redirect_uri=GITHUB_CLIENT_REDIRECT_URI.value,
                 redirect_uri=GITHUB_CLIENT_REDIRECT_URI.value,
             )
             )
 
 
@@ -603,6 +634,9 @@ def load_oauth_providers():
         def oidc_oauth_register(client):
         def oidc_oauth_register(client):
             client_kwargs = {
             client_kwargs = {
                 "scope": OAUTH_SCOPES.value,
                 "scope": OAUTH_SCOPES.value,
+                **(
+                    {"timeout": int(OAUTH_TIMEOUT.value)} if OAUTH_TIMEOUT.value else {}
+                ),
             }
             }
 
 
             if (
             if (
@@ -895,6 +929,18 @@ except Exception:
     pass
     pass
 OPENAI_API_BASE_URL = "https://api.openai.com/v1"
 OPENAI_API_BASE_URL = "https://api.openai.com/v1"
 
 
+
+####################################
+# MODELS
+####################################
+
+ENABLE_BASE_MODELS_CACHE = PersistentConfig(
+    "ENABLE_BASE_MODELS_CACHE",
+    "models.base_models_cache",
+    os.environ.get("ENABLE_BASE_MODELS_CACHE", "False").lower() == "true",
+)
+
+
 ####################################
 ####################################
 # TOOL_SERVERS
 # TOOL_SERVERS
 ####################################
 ####################################
@@ -1799,6 +1845,7 @@ QDRANT_GRPC_PORT = int(os.environ.get("QDRANT_GRPC_PORT", "6334"))
 ENABLE_QDRANT_MULTITENANCY_MODE = (
 ENABLE_QDRANT_MULTITENANCY_MODE = (
     os.environ.get("ENABLE_QDRANT_MULTITENANCY_MODE", "false").lower() == "true"
     os.environ.get("ENABLE_QDRANT_MULTITENANCY_MODE", "false").lower() == "true"
 )
 )
+QDRANT_COLLECTION_PREFIX = os.environ.get("QDRANT_COLLECTION_PREFIX", "open-webui")
 
 
 # OpenSearch
 # OpenSearch
 OPENSEARCH_URI = os.environ.get("OPENSEARCH_URI", "https://localhost:9200")
 OPENSEARCH_URI = os.environ.get("OPENSEARCH_URI", "https://localhost:9200")

+ 39 - 5
backend/open_webui/env.py

@@ -199,6 +199,7 @@ CHANGELOG = changelog_json
 
 
 SAFE_MODE = os.environ.get("SAFE_MODE", "false").lower() == "true"
 SAFE_MODE = os.environ.get("SAFE_MODE", "false").lower() == "true"
 
 
+
 ####################################
 ####################################
 # ENABLE_FORWARD_USER_INFO_HEADERS
 # ENABLE_FORWARD_USER_INFO_HEADERS
 ####################################
 ####################################
@@ -272,15 +273,13 @@ if "postgres://" in DATABASE_URL:
 
 
 DATABASE_SCHEMA = os.environ.get("DATABASE_SCHEMA", None)
 DATABASE_SCHEMA = os.environ.get("DATABASE_SCHEMA", None)
 
 
-DATABASE_POOL_SIZE = os.environ.get("DATABASE_POOL_SIZE", 0)
+DATABASE_POOL_SIZE = os.environ.get("DATABASE_POOL_SIZE", None)
 
 
-if DATABASE_POOL_SIZE == "":
-    DATABASE_POOL_SIZE = 0
-else:
+if DATABASE_POOL_SIZE != None:
     try:
     try:
         DATABASE_POOL_SIZE = int(DATABASE_POOL_SIZE)
         DATABASE_POOL_SIZE = int(DATABASE_POOL_SIZE)
     except Exception:
     except Exception:
-        DATABASE_POOL_SIZE = 0
+        DATABASE_POOL_SIZE = None
 
 
 DATABASE_POOL_MAX_OVERFLOW = os.environ.get("DATABASE_POOL_MAX_OVERFLOW", 0)
 DATABASE_POOL_MAX_OVERFLOW = os.environ.get("DATABASE_POOL_MAX_OVERFLOW", 0)
 
 
@@ -325,6 +324,7 @@ ENABLE_REALTIME_CHAT_SAVE = (
 ####################################
 ####################################
 
 
 REDIS_URL = os.environ.get("REDIS_URL", "")
 REDIS_URL = os.environ.get("REDIS_URL", "")
+REDIS_KEY_PREFIX = os.environ.get("REDIS_KEY_PREFIX", "open-webui")
 REDIS_SENTINEL_HOSTS = os.environ.get("REDIS_SENTINEL_HOSTS", "")
 REDIS_SENTINEL_HOSTS = os.environ.get("REDIS_SENTINEL_HOSTS", "")
 REDIS_SENTINEL_PORT = os.environ.get("REDIS_SENTINEL_PORT", "26379")
 REDIS_SENTINEL_PORT = os.environ.get("REDIS_SENTINEL_PORT", "26379")
 
 
@@ -396,10 +396,33 @@ WEBUI_AUTH_COOKIE_SECURE = (
 if WEBUI_AUTH and WEBUI_SECRET_KEY == "":
 if WEBUI_AUTH and WEBUI_SECRET_KEY == "":
     raise ValueError(ERROR_MESSAGES.ENV_VAR_NOT_FOUND)
     raise ValueError(ERROR_MESSAGES.ENV_VAR_NOT_FOUND)
 
 
+ENABLE_COMPRESSION_MIDDLEWARE = (
+    os.environ.get("ENABLE_COMPRESSION_MIDDLEWARE", "True").lower() == "true"
+)
+
+####################################
+# MODELS
+####################################
+
+MODELS_CACHE_TTL = os.environ.get("MODELS_CACHE_TTL", "1")
+if MODELS_CACHE_TTL == "":
+    MODELS_CACHE_TTL = None
+else:
+    try:
+        MODELS_CACHE_TTL = int(MODELS_CACHE_TTL)
+    except Exception:
+        MODELS_CACHE_TTL = 1
+
+
+####################################
+# WEBSOCKET SUPPORT
+####################################
+
 ENABLE_WEBSOCKET_SUPPORT = (
 ENABLE_WEBSOCKET_SUPPORT = (
     os.environ.get("ENABLE_WEBSOCKET_SUPPORT", "True").lower() == "true"
     os.environ.get("ENABLE_WEBSOCKET_SUPPORT", "True").lower() == "true"
 )
 )
 
 
+
 WEBSOCKET_MANAGER = os.environ.get("WEBSOCKET_MANAGER", "")
 WEBSOCKET_MANAGER = os.environ.get("WEBSOCKET_MANAGER", "")
 
 
 WEBSOCKET_REDIS_URL = os.environ.get("WEBSOCKET_REDIS_URL", REDIS_URL)
 WEBSOCKET_REDIS_URL = os.environ.get("WEBSOCKET_REDIS_URL", REDIS_URL)
@@ -543,6 +566,9 @@ ENABLE_OTEL_METRICS = os.environ.get("ENABLE_OTEL_METRICS", "False").lower() ==
 OTEL_EXPORTER_OTLP_ENDPOINT = os.environ.get(
 OTEL_EXPORTER_OTLP_ENDPOINT = os.environ.get(
     "OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4317"
     "OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4317"
 )
 )
+OTEL_EXPORTER_OTLP_INSECURE = (
+    os.environ.get("OTEL_EXPORTER_OTLP_INSECURE", "False").lower() == "true"
+)
 OTEL_SERVICE_NAME = os.environ.get("OTEL_SERVICE_NAME", "open-webui")
 OTEL_SERVICE_NAME = os.environ.get("OTEL_SERVICE_NAME", "open-webui")
 OTEL_RESOURCE_ATTRIBUTES = os.environ.get(
 OTEL_RESOURCE_ATTRIBUTES = os.environ.get(
     "OTEL_RESOURCE_ATTRIBUTES", ""
     "OTEL_RESOURCE_ATTRIBUTES", ""
@@ -550,6 +576,14 @@ OTEL_RESOURCE_ATTRIBUTES = os.environ.get(
 OTEL_TRACES_SAMPLER = os.environ.get(
 OTEL_TRACES_SAMPLER = os.environ.get(
     "OTEL_TRACES_SAMPLER", "parentbased_always_on"
     "OTEL_TRACES_SAMPLER", "parentbased_always_on"
 ).lower()
 ).lower()
+OTEL_BASIC_AUTH_USERNAME = os.environ.get("OTEL_BASIC_AUTH_USERNAME", "")
+OTEL_BASIC_AUTH_PASSWORD = os.environ.get("OTEL_BASIC_AUTH_PASSWORD", "")
+
+
+OTEL_OTLP_SPAN_EXPORTER = os.environ.get(
+    "OTEL_OTLP_SPAN_EXPORTER", "grpc"
+).lower()  # grpc or http
+
 
 
 ####################################
 ####################################
 # TOOLS/FUNCTIONS PIP OPTIONS
 # TOOLS/FUNCTIONS PIP OPTIONS

+ 19 - 13
backend/open_webui/internal/db.py

@@ -62,6 +62,9 @@ def handle_peewee_migration(DATABASE_URL):
 
 
     except Exception as e:
     except Exception as e:
         log.error(f"Failed to initialize the database connection: {e}")
         log.error(f"Failed to initialize the database connection: {e}")
+        log.warning(
+            "Hint: If your database password contains special characters, you may need to URL-encode it."
+        )
         raise
         raise
     finally:
     finally:
         # Properly closing the database connection
         # Properly closing the database connection
@@ -81,20 +84,23 @@ if "sqlite" in SQLALCHEMY_DATABASE_URL:
         SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
         SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
     )
     )
 else:
 else:
-    if DATABASE_POOL_SIZE > 0:
-        engine = create_engine(
-            SQLALCHEMY_DATABASE_URL,
-            pool_size=DATABASE_POOL_SIZE,
-            max_overflow=DATABASE_POOL_MAX_OVERFLOW,
-            pool_timeout=DATABASE_POOL_TIMEOUT,
-            pool_recycle=DATABASE_POOL_RECYCLE,
-            pool_pre_ping=True,
-            poolclass=QueuePool,
-        )
+    if isinstance(DATABASE_POOL_SIZE, int):
+        if DATABASE_POOL_SIZE > 0:
+            engine = create_engine(
+                SQLALCHEMY_DATABASE_URL,
+                pool_size=DATABASE_POOL_SIZE,
+                max_overflow=DATABASE_POOL_MAX_OVERFLOW,
+                pool_timeout=DATABASE_POOL_TIMEOUT,
+                pool_recycle=DATABASE_POOL_RECYCLE,
+                pool_pre_ping=True,
+                poolclass=QueuePool,
+            )
+        else:
+            engine = create_engine(
+                SQLALCHEMY_DATABASE_URL, pool_pre_ping=True, poolclass=NullPool
+            )
     else:
     else:
-        engine = create_engine(
-            SQLALCHEMY_DATABASE_URL, pool_pre_ping=True, poolclass=NullPool
-        )
+        engine = create_engine(SQLALCHEMY_DATABASE_URL, pool_pre_ping=True)
 
 
 
 
 SessionLocal = sessionmaker(
 SessionLocal = sessionmaker(

+ 44 - 4
backend/open_webui/main.py

@@ -36,7 +36,6 @@ from fastapi import (
     applications,
     applications,
     BackgroundTasks,
     BackgroundTasks,
 )
 )
-
 from fastapi.openapi.docs import get_swagger_ui_html
 from fastapi.openapi.docs import get_swagger_ui_html
 
 
 from fastapi.middleware.cors import CORSMiddleware
 from fastapi.middleware.cors import CORSMiddleware
@@ -49,6 +48,7 @@ from starlette.exceptions import HTTPException as StarletteHTTPException
 from starlette.middleware.base import BaseHTTPMiddleware
 from starlette.middleware.base import BaseHTTPMiddleware
 from starlette.middleware.sessions import SessionMiddleware
 from starlette.middleware.sessions import SessionMiddleware
 from starlette.responses import Response, StreamingResponse
 from starlette.responses import Response, StreamingResponse
+from starlette.datastructures import Headers
 
 
 
 
 from open_webui.utils import logger
 from open_webui.utils import logger
@@ -116,6 +116,8 @@ from open_webui.config import (
     OPENAI_API_CONFIGS,
     OPENAI_API_CONFIGS,
     # Direct Connections
     # Direct Connections
     ENABLE_DIRECT_CONNECTIONS,
     ENABLE_DIRECT_CONNECTIONS,
+    # Model list
+    ENABLE_BASE_MODELS_CACHE,
     # Thread pool size for FastAPI/AnyIO
     # Thread pool size for FastAPI/AnyIO
     THREAD_POOL_SIZE,
     THREAD_POOL_SIZE,
     # Tool Server Configs
     # Tool Server Configs
@@ -396,6 +398,7 @@ from open_webui.env import (
     AUDIT_LOG_LEVEL,
     AUDIT_LOG_LEVEL,
     CHANGELOG,
     CHANGELOG,
     REDIS_URL,
     REDIS_URL,
+    REDIS_KEY_PREFIX,
     REDIS_SENTINEL_HOSTS,
     REDIS_SENTINEL_HOSTS,
     REDIS_SENTINEL_PORT,
     REDIS_SENTINEL_PORT,
     GLOBAL_LOG_LEVEL,
     GLOBAL_LOG_LEVEL,
@@ -411,6 +414,7 @@ from open_webui.env import (
     WEBUI_AUTH_TRUSTED_EMAIL_HEADER,
     WEBUI_AUTH_TRUSTED_EMAIL_HEADER,
     WEBUI_AUTH_TRUSTED_NAME_HEADER,
     WEBUI_AUTH_TRUSTED_NAME_HEADER,
     WEBUI_AUTH_SIGNOUT_REDIRECT_URL,
     WEBUI_AUTH_SIGNOUT_REDIRECT_URL,
+    ENABLE_COMPRESSION_MIDDLEWARE,
     ENABLE_WEBSOCKET_SUPPORT,
     ENABLE_WEBSOCKET_SUPPORT,
     BYPASS_MODEL_ACCESS_CONTROL,
     BYPASS_MODEL_ACCESS_CONTROL,
     RESET_CONFIG_ON_START,
     RESET_CONFIG_ON_START,
@@ -533,6 +537,27 @@ async def lifespan(app: FastAPI):
 
 
     asyncio.create_task(periodic_usage_pool_cleanup())
     asyncio.create_task(periodic_usage_pool_cleanup())
 
 
+    if app.state.config.ENABLE_BASE_MODELS_CACHE:
+        await get_all_models(
+            Request(
+                # Creating a mock request object to pass to get_all_models
+                {
+                    "type": "http",
+                    "asgi.version": "3.0",
+                    "asgi.spec_version": "2.0",
+                    "method": "GET",
+                    "path": "/internal",
+                    "query_string": b"",
+                    "headers": Headers({}).raw,
+                    "client": ("127.0.0.1", 12345),
+                    "server": ("127.0.0.1", 80),
+                    "scheme": "http",
+                    "app": app,
+                }
+            ),
+            None,
+        )
+
     yield
     yield
 
 
     if hasattr(app.state, "redis_task_command_listener"):
     if hasattr(app.state, "redis_task_command_listener"):
@@ -553,6 +578,7 @@ app.state.instance_id = None
 app.state.config = AppConfig(
 app.state.config = AppConfig(
     redis_url=REDIS_URL,
     redis_url=REDIS_URL,
     redis_sentinels=get_sentinels_from_env(REDIS_SENTINEL_HOSTS, REDIS_SENTINEL_PORT),
     redis_sentinels=get_sentinels_from_env(REDIS_SENTINEL_HOSTS, REDIS_SENTINEL_PORT),
+    redis_key_prefix=REDIS_KEY_PREFIX,
 )
 )
 app.state.redis = None
 app.state.redis = None
 
 
@@ -615,6 +641,15 @@ app.state.TOOL_SERVERS = []
 
 
 app.state.config.ENABLE_DIRECT_CONNECTIONS = ENABLE_DIRECT_CONNECTIONS
 app.state.config.ENABLE_DIRECT_CONNECTIONS = ENABLE_DIRECT_CONNECTIONS
 
 
+########################################
+#
+# MODELS
+#
+########################################
+
+app.state.config.ENABLE_BASE_MODELS_CACHE = ENABLE_BASE_MODELS_CACHE
+app.state.BASE_MODELS = []
+
 ########################################
 ########################################
 #
 #
 # WEBUI
 # WEBUI
@@ -1072,7 +1107,9 @@ class RedirectMiddleware(BaseHTTPMiddleware):
 
 
 
 
 # Add the middleware to the app
 # Add the middleware to the app
-app.add_middleware(CompressMiddleware)
+if ENABLE_COMPRESSION_MIDDLEWARE:
+    app.add_middleware(CompressMiddleware)
+
 app.add_middleware(RedirectMiddleware)
 app.add_middleware(RedirectMiddleware)
 app.add_middleware(SecurityHeadersMiddleware)
 app.add_middleware(SecurityHeadersMiddleware)
 
 
@@ -1188,7 +1225,9 @@ if audit_level != AuditLevel.NONE:
 
 
 
 
 @app.get("/api/models")
 @app.get("/api/models")
-async def get_models(request: Request, user=Depends(get_verified_user)):
+async def get_models(
+    request: Request, refresh: bool = False, user=Depends(get_verified_user)
+):
     def get_filtered_models(models, user):
     def get_filtered_models(models, user):
         filtered_models = []
         filtered_models = []
         for model in models:
         for model in models:
@@ -1212,7 +1251,7 @@ async def get_models(request: Request, user=Depends(get_verified_user)):
 
 
         return filtered_models
         return filtered_models
 
 
-    all_models = await get_all_models(request, user=user)
+    all_models = await get_all_models(request, refresh=refresh, user=user)
 
 
     models = []
     models = []
     for model in all_models:
     for model in all_models:
@@ -1507,6 +1546,7 @@ async def get_app_config(request: Request):
         "name": app.state.WEBUI_NAME,
         "name": app.state.WEBUI_NAME,
         "version": VERSION,
         "version": VERSION,
         "default_locale": str(DEFAULT_LOCALE),
         "default_locale": str(DEFAULT_LOCALE),
+        "offline_mode": OFFLINE_MODE,
         "oauth": {
         "oauth": {
             "providers": {
             "providers": {
                 name: config.get("name", name)
                 name: config.get("name", name)

+ 12 - 2
backend/open_webui/models/chats.py

@@ -72,6 +72,8 @@ class ChatImportForm(ChatForm):
     meta: Optional[dict] = {}
     meta: Optional[dict] = {}
     pinned: Optional[bool] = False
     pinned: Optional[bool] = False
     folder_id: Optional[str] = None
     folder_id: Optional[str] = None
+    created_at: Optional[int] = None
+    updated_at: Optional[int] = None
 
 
 
 
 class ChatTitleMessagesForm(BaseModel):
 class ChatTitleMessagesForm(BaseModel):
@@ -147,8 +149,16 @@ class ChatTable:
                     "meta": form_data.meta,
                     "meta": form_data.meta,
                     "pinned": form_data.pinned,
                     "pinned": form_data.pinned,
                     "folder_id": form_data.folder_id,
                     "folder_id": form_data.folder_id,
-                    "created_at": int(time.time()),
-                    "updated_at": int(time.time()),
+                    "created_at": (
+                        form_data.created_at
+                        if form_data.created_at
+                        else int(time.time())
+                    ),
+                    "updated_at": (
+                        form_data.updated_at
+                        if form_data.updated_at
+                        else int(time.time())
+                    ),
                 }
                 }
             )
             )
 
 

+ 3 - 1
backend/open_webui/retrieval/loaders/main.py

@@ -14,7 +14,7 @@ from langchain_community.document_loaders import (
     TextLoader,
     TextLoader,
     UnstructuredEPubLoader,
     UnstructuredEPubLoader,
     UnstructuredExcelLoader,
     UnstructuredExcelLoader,
-    UnstructuredMarkdownLoader,
+    UnstructuredODTLoader,
     UnstructuredPowerPointLoader,
     UnstructuredPowerPointLoader,
     UnstructuredRSTLoader,
     UnstructuredRSTLoader,
     UnstructuredXMLLoader,
     UnstructuredXMLLoader,
@@ -389,6 +389,8 @@ class Loader:
                 loader = UnstructuredPowerPointLoader(file_path)
                 loader = UnstructuredPowerPointLoader(file_path)
             elif file_ext == "msg":
             elif file_ext == "msg":
                 loader = OutlookMessageLoader(file_path)
                 loader = OutlookMessageLoader(file_path)
+            elif file_ext == "odt":
+                loader = UnstructuredODTLoader(file_path)
             elif self._is_text_file(file_ext, file_content_type):
             elif self._is_text_file(file_ext, file_content_type):
                 loader = TextLoader(file_path, autodetect_encoding=True)
                 loader = TextLoader(file_path, autodetect_encoding=True)
             else:
             else:

+ 44 - 36
backend/open_webui/retrieval/utils.py

@@ -7,6 +7,7 @@ import hashlib
 from concurrent.futures import ThreadPoolExecutor
 from concurrent.futures import ThreadPoolExecutor
 import time
 import time
 
 
+from urllib.parse import quote
 from huggingface_hub import snapshot_download
 from huggingface_hub import snapshot_download
 from langchain.retrievers import ContextualCompressionRetriever, EnsembleRetriever
 from langchain.retrievers import ContextualCompressionRetriever, EnsembleRetriever
 from langchain_community.retrievers import BM25Retriever
 from langchain_community.retrievers import BM25Retriever
@@ -459,20 +460,19 @@ def get_sources_from_files(
     )
     )
 
 
     extracted_collections = []
     extracted_collections = []
-    relevant_contexts = []
+    query_results = []
 
 
     for file in files:
     for file in files:
-
-        context = None
+        query_result = None
         if file.get("docs"):
         if file.get("docs"):
             # BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL
             # BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL
-            context = {
+            query_result = {
                 "documents": [[doc.get("content") for doc in file.get("docs")]],
                 "documents": [[doc.get("content") for doc in file.get("docs")]],
                 "metadatas": [[doc.get("metadata") for doc in file.get("docs")]],
                 "metadatas": [[doc.get("metadata") for doc in file.get("docs")]],
             }
             }
         elif file.get("context") == "full":
         elif file.get("context") == "full":
             # Manual Full Mode Toggle
             # Manual Full Mode Toggle
-            context = {
+            query_result = {
                 "documents": [[file.get("file").get("data", {}).get("content")]],
                 "documents": [[file.get("file").get("data", {}).get("content")]],
                 "metadatas": [[{"file_id": file.get("id"), "name": file.get("name")}]],
                 "metadatas": [[{"file_id": file.get("id"), "name": file.get("name")}]],
             }
             }
@@ -499,7 +499,7 @@ def get_sources_from_files(
                             }
                             }
                         )
                         )
 
 
-                context = {
+                query_result = {
                     "documents": [documents],
                     "documents": [documents],
                     "metadatas": [metadatas],
                     "metadatas": [metadatas],
                 }
                 }
@@ -507,7 +507,7 @@ def get_sources_from_files(
             elif file.get("id"):
             elif file.get("id"):
                 file_object = Files.get_file_by_id(file.get("id"))
                 file_object = Files.get_file_by_id(file.get("id"))
                 if file_object:
                 if file_object:
-                    context = {
+                    query_result = {
                         "documents": [[file_object.data.get("content", "")]],
                         "documents": [[file_object.data.get("content", "")]],
                         "metadatas": [
                         "metadatas": [
                             [
                             [
@@ -520,7 +520,7 @@ def get_sources_from_files(
                         ],
                         ],
                     }
                     }
             elif file.get("file").get("data"):
             elif file.get("file").get("data"):
-                context = {
+                query_result = {
                     "documents": [[file.get("file").get("data", {}).get("content")]],
                     "documents": [[file.get("file").get("data", {}).get("content")]],
                     "metadatas": [
                     "metadatas": [
                         [file.get("file").get("data", {}).get("metadata", {})]
                         [file.get("file").get("data", {}).get("metadata", {})]
@@ -548,19 +548,27 @@ def get_sources_from_files(
 
 
             if full_context:
             if full_context:
                 try:
                 try:
-                    context = get_all_items_from_collections(collection_names)
+                    query_result = get_all_items_from_collections(collection_names)
                 except Exception as e:
                 except Exception as e:
                     log.exception(e)
                     log.exception(e)
 
 
             else:
             else:
                 try:
                 try:
-                    context = None
+                    query_result = None
                     if file.get("type") == "text":
                     if file.get("type") == "text":
-                        context = file["content"]
+                        # Not sure when this is used, but it seems to be a fallback
+                        query_result = {
+                            "documents": [
+                                [file.get("file").get("data", {}).get("content")]
+                            ],
+                            "metadatas": [
+                                [file.get("file").get("data", {}).get("meta", {})]
+                            ],
+                        }
                     else:
                     else:
                         if hybrid_search:
                         if hybrid_search:
                             try:
                             try:
-                                context = query_collection_with_hybrid_search(
+                                query_result = query_collection_with_hybrid_search(
                                     collection_names=collection_names,
                                     collection_names=collection_names,
                                     queries=queries,
                                     queries=queries,
                                     embedding_function=embedding_function,
                                     embedding_function=embedding_function,
@@ -576,8 +584,8 @@ def get_sources_from_files(
                                     " non hybrid search as fallback."
                                     " non hybrid search as fallback."
                                 )
                                 )
 
 
-                        if (not hybrid_search) or (context is None):
-                            context = query_collection(
+                        if (not hybrid_search) or (query_result is None):
+                            query_result = query_collection(
                                 collection_names=collection_names,
                                 collection_names=collection_names,
                                 queries=queries,
                                 queries=queries,
                                 embedding_function=embedding_function,
                                 embedding_function=embedding_function,
@@ -588,24 +596,24 @@ def get_sources_from_files(
 
 
             extracted_collections.extend(collection_names)
             extracted_collections.extend(collection_names)
 
 
-        if context:
+        if query_result:
             if "data" in file:
             if "data" in file:
                 del file["data"]
                 del file["data"]
 
 
-            relevant_contexts.append({**context, "file": file})
+            query_results.append({**query_result, "file": file})
 
 
     sources = []
     sources = []
-    for context in relevant_contexts:
+    for query_result in query_results:
         try:
         try:
-            if "documents" in context:
-                if "metadatas" in context:
+            if "documents" in query_result:
+                if "metadatas" in query_result:
                     source = {
                     source = {
-                        "source": context["file"],
-                        "document": context["documents"][0],
-                        "metadata": context["metadatas"][0],
+                        "source": query_result["file"],
+                        "document": query_result["documents"][0],
+                        "metadata": query_result["metadatas"][0],
                     }
                     }
-                    if "distances" in context and context["distances"]:
-                        source["distances"] = context["distances"][0]
+                    if "distances" in query_result and query_result["distances"]:
+                        source["distances"] = query_result["distances"][0]
 
 
                     sources.append(source)
                     sources.append(source)
         except Exception as e:
         except Exception as e:
@@ -678,10 +686,10 @@ def generate_openai_batch_embeddings(
                 "Authorization": f"Bearer {key}",
                 "Authorization": f"Bearer {key}",
                 **(
                 **(
                     {
                     {
-                        "X-OpenWebUI-User-Name": user.name,
-                        "X-OpenWebUI-User-Id": user.id,
-                        "X-OpenWebUI-User-Email": user.email,
-                        "X-OpenWebUI-User-Role": user.role,
+                        "X-OpenWebUI-User-Name": quote(user.name),
+                        "X-OpenWebUI-User-Id": quote(user.id),
+                        "X-OpenWebUI-User-Email": quote(user.email),
+                        "X-OpenWebUI-User-Role": quote(user.role),
                     }
                     }
                     if ENABLE_FORWARD_USER_INFO_HEADERS and user
                     if ENABLE_FORWARD_USER_INFO_HEADERS and user
                     else {}
                     else {}
@@ -727,10 +735,10 @@ def generate_azure_openai_batch_embeddings(
                     "api-key": key,
                     "api-key": key,
                     **(
                     **(
                         {
                         {
-                            "X-OpenWebUI-User-Name": user.name,
-                            "X-OpenWebUI-User-Id": user.id,
-                            "X-OpenWebUI-User-Email": user.email,
-                            "X-OpenWebUI-User-Role": user.role,
+                            "X-OpenWebUI-User-Name": quote(user.name),
+                            "X-OpenWebUI-User-Id": quote(user.id),
+                            "X-OpenWebUI-User-Email": quote(user.email),
+                            "X-OpenWebUI-User-Role": quote(user.role),
                         }
                         }
                         if ENABLE_FORWARD_USER_INFO_HEADERS and user
                         if ENABLE_FORWARD_USER_INFO_HEADERS and user
                         else {}
                         else {}
@@ -777,10 +785,10 @@ def generate_ollama_batch_embeddings(
                 "Authorization": f"Bearer {key}",
                 "Authorization": f"Bearer {key}",
                 **(
                 **(
                     {
                     {
-                        "X-OpenWebUI-User-Name": user.name,
-                        "X-OpenWebUI-User-Id": user.id,
-                        "X-OpenWebUI-User-Email": user.email,
-                        "X-OpenWebUI-User-Role": user.role,
+                        "X-OpenWebUI-User-Name": quote(user.name),
+                        "X-OpenWebUI-User-Id": quote(user.id),
+                        "X-OpenWebUI-User-Email": quote(user.email),
+                        "X-OpenWebUI-User-Role": quote(user.role),
                     }
                     }
                     if ENABLE_FORWARD_USER_INFO_HEADERS
                     if ENABLE_FORWARD_USER_INFO_HEADERS
                     else {}
                     else {}

+ 6 - 3
backend/open_webui/retrieval/vector/dbs/opensearch.py

@@ -157,10 +157,10 @@ class OpenSearchClient(VectorDBBase):
 
 
         for field, value in filter.items():
         for field, value in filter.items():
             query_body["query"]["bool"]["filter"].append(
             query_body["query"]["bool"]["filter"].append(
-                {"match": {"metadata." + str(field): value}}
+                {"term": {"metadata." + str(field) + ".keyword": value}}
             )
             )
 
 
-        size = limit if limit else 10
+        size = limit if limit else 10000
 
 
         try:
         try:
             result = self.client.search(
             result = self.client.search(
@@ -206,6 +206,7 @@ class OpenSearchClient(VectorDBBase):
                 for item in batch
                 for item in batch
             ]
             ]
             bulk(self.client, actions)
             bulk(self.client, actions)
+        self.client.indices.refresh(self._get_index_name(collection_name))
 
 
     def upsert(self, collection_name: str, items: list[VectorItem]):
     def upsert(self, collection_name: str, items: list[VectorItem]):
         self._create_index_if_not_exists(
         self._create_index_if_not_exists(
@@ -228,6 +229,7 @@ class OpenSearchClient(VectorDBBase):
                 for item in batch
                 for item in batch
             ]
             ]
             bulk(self.client, actions)
             bulk(self.client, actions)
+        self.client.indices.refresh(self._get_index_name(collection_name))
 
 
     def delete(
     def delete(
         self,
         self,
@@ -251,11 +253,12 @@ class OpenSearchClient(VectorDBBase):
             }
             }
             for field, value in filter.items():
             for field, value in filter.items():
                 query_body["query"]["bool"]["filter"].append(
                 query_body["query"]["bool"]["filter"].append(
-                    {"match": {"metadata." + str(field): value}}
+                    {"term": {"metadata." + str(field) + ".keyword": value}}
                 )
                 )
             self.client.delete_by_query(
             self.client.delete_by_query(
                 index=self._get_index_name(collection_name), body=query_body
                 index=self._get_index_name(collection_name), body=query_body
             )
             )
+        self.client.indices.refresh(self._get_index_name(collection_name))
 
 
     def reset(self):
     def reset(self):
         indices = self.client.indices.get(index=f"{self.index_prefix}_*")
         indices = self.client.indices.get(index=f"{self.index_prefix}_*")

+ 21 - 1
backend/open_webui/retrieval/vector/dbs/qdrant.py

@@ -18,6 +18,7 @@ from open_webui.config import (
     QDRANT_ON_DISK,
     QDRANT_ON_DISK,
     QDRANT_GRPC_PORT,
     QDRANT_GRPC_PORT,
     QDRANT_PREFER_GRPC,
     QDRANT_PREFER_GRPC,
+    QDRANT_COLLECTION_PREFIX,
 )
 )
 from open_webui.env import SRC_LOG_LEVELS
 from open_webui.env import SRC_LOG_LEVELS
 
 
@@ -29,7 +30,7 @@ log.setLevel(SRC_LOG_LEVELS["RAG"])
 
 
 class QdrantClient(VectorDBBase):
 class QdrantClient(VectorDBBase):
     def __init__(self):
     def __init__(self):
-        self.collection_prefix = "open-webui"
+        self.collection_prefix = QDRANT_COLLECTION_PREFIX
         self.QDRANT_URI = QDRANT_URI
         self.QDRANT_URI = QDRANT_URI
         self.QDRANT_API_KEY = QDRANT_API_KEY
         self.QDRANT_API_KEY = QDRANT_API_KEY
         self.QDRANT_ON_DISK = QDRANT_ON_DISK
         self.QDRANT_ON_DISK = QDRANT_ON_DISK
@@ -86,6 +87,25 @@ class QdrantClient(VectorDBBase):
             ),
             ),
         )
         )
 
 
+        # Create payload indexes for efficient filtering
+        self.client.create_payload_index(
+            collection_name=collection_name_with_prefix,
+            field_name="metadata.hash",
+            field_schema=models.KeywordIndexParams(
+                type=models.KeywordIndexType.KEYWORD,
+                is_tenant=False,
+                on_disk=self.QDRANT_ON_DISK,
+            ),
+        )
+        self.client.create_payload_index(
+            collection_name=collection_name_with_prefix,
+            field_name="metadata.file_id",
+            field_schema=models.KeywordIndexParams(
+                type=models.KeywordIndexType.KEYWORD,
+                is_tenant=False,
+                on_disk=self.QDRANT_ON_DISK,
+            ),
+        )
         log.info(f"collection {collection_name_with_prefix} successfully created!")
         log.info(f"collection {collection_name_with_prefix} successfully created!")
 
 
     def _create_collection_if_not_exists(self, collection_name, dimension):
     def _create_collection_if_not_exists(self, collection_name, dimension):

+ 21 - 1
backend/open_webui/retrieval/vector/dbs/qdrant_multitenancy.py

@@ -9,6 +9,7 @@ from open_webui.config import (
     QDRANT_ON_DISK,
     QDRANT_ON_DISK,
     QDRANT_PREFER_GRPC,
     QDRANT_PREFER_GRPC,
     QDRANT_URI,
     QDRANT_URI,
+    QDRANT_COLLECTION_PREFIX,
 )
 )
 from open_webui.env import SRC_LOG_LEVELS
 from open_webui.env import SRC_LOG_LEVELS
 from open_webui.retrieval.vector.main import (
 from open_webui.retrieval.vector.main import (
@@ -30,7 +31,7 @@ log.setLevel(SRC_LOG_LEVELS["RAG"])
 
 
 class QdrantClient(VectorDBBase):
 class QdrantClient(VectorDBBase):
     def __init__(self):
     def __init__(self):
-        self.collection_prefix = "open-webui"
+        self.collection_prefix = QDRANT_COLLECTION_PREFIX
         self.QDRANT_URI = QDRANT_URI
         self.QDRANT_URI = QDRANT_URI
         self.QDRANT_API_KEY = QDRANT_API_KEY
         self.QDRANT_API_KEY = QDRANT_API_KEY
         self.QDRANT_ON_DISK = QDRANT_ON_DISK
         self.QDRANT_ON_DISK = QDRANT_ON_DISK
@@ -228,6 +229,25 @@ class QdrantClient(VectorDBBase):
                 ),
                 ),
                 wait=True,
                 wait=True,
             )
             )
+            # Create payload indexes for efficient filtering on metadata.hash and metadata.file_id
+            self.client.create_payload_index(
+                collection_name=mt_collection_name,
+                field_name="metadata.hash",
+                field_schema=models.KeywordIndexParams(
+                    type=models.KeywordIndexType.KEYWORD,
+                    is_tenant=False,
+                    on_disk=self.QDRANT_ON_DISK,
+                ),
+            )
+            self.client.create_payload_index(
+                collection_name=mt_collection_name,
+                field_name="metadata.file_id",
+                field_schema=models.KeywordIndexParams(
+                    type=models.KeywordIndexType.KEYWORD,
+                    is_tenant=False,
+                    on_disk=self.QDRANT_ON_DISK,
+                ),
+            )
 
 
             log.info(
             log.info(
                 f"Multi-tenant collection {mt_collection_name} created with dimension {dimension}!"
                 f"Multi-tenant collection {mt_collection_name} created with dimension {dimension}!"

+ 3 - 1
backend/open_webui/retrieval/web/brave.py

@@ -36,7 +36,9 @@ def search_brave(
 
 
     return [
     return [
         SearchResult(
         SearchResult(
-            link=result["url"], title=result.get("title"), snippet=result.get("snippet")
+            link=result["url"],
+            title=result.get("title"),
+            snippet=result.get("description"),
         )
         )
         for result in results[:count]
         for result in results[:count]
     ]
     ]

+ 14 - 9
backend/open_webui/routers/audio.py

@@ -15,6 +15,7 @@ import aiohttp
 import aiofiles
 import aiofiles
 import requests
 import requests
 import mimetypes
 import mimetypes
+from urllib.parse import quote
 
 
 from fastapi import (
 from fastapi import (
     Depends,
     Depends,
@@ -343,10 +344,10 @@ async def speech(request: Request, user=Depends(get_verified_user)):
                         "Authorization": f"Bearer {request.app.state.config.TTS_OPENAI_API_KEY}",
                         "Authorization": f"Bearer {request.app.state.config.TTS_OPENAI_API_KEY}",
                         **(
                         **(
                             {
                             {
-                                "X-OpenWebUI-User-Name": user.name,
-                                "X-OpenWebUI-User-Id": user.id,
-                                "X-OpenWebUI-User-Email": user.email,
-                                "X-OpenWebUI-User-Role": user.role,
+                                "X-OpenWebUI-User-Name": quote(user.name),
+                                "X-OpenWebUI-User-Id": quote(user.id),
+                                "X-OpenWebUI-User-Email": quote(user.email),
+                                "X-OpenWebUI-User-Role": quote(user.role),
                             }
                             }
                             if ENABLE_FORWARD_USER_INFO_HEADERS
                             if ENABLE_FORWARD_USER_INFO_HEADERS
                             else {}
                             else {}
@@ -919,14 +920,18 @@ def transcription(
 ):
 ):
     log.info(f"file.content_type: {file.content_type}")
     log.info(f"file.content_type: {file.content_type}")
 
 
-    supported_content_types = request.app.state.config.STT_SUPPORTED_CONTENT_TYPES or [
-        "audio/*",
-        "video/webm",
-    ]
+    stt_supported_content_types = getattr(
+        request.app.state.config, "STT_SUPPORTED_CONTENT_TYPES", []
+    )
 
 
     if not any(
     if not any(
         fnmatch(file.content_type, content_type)
         fnmatch(file.content_type, content_type)
-        for content_type in supported_content_types
+        for content_type in (
+            stt_supported_content_types
+            if stt_supported_content_types
+            and any(t.strip() for t in stt_supported_content_types)
+            else ["audio/*", "video/webm"]
+        )
     ):
     ):
         raise HTTPException(
         raise HTTPException(
             status_code=status.HTTP_400_BAD_REQUEST,
             status_code=status.HTTP_400_BAD_REQUEST,

+ 1 - 0
backend/open_webui/routers/auths.py

@@ -669,6 +669,7 @@ async def signup(request: Request, response: Response, form_data: SignupForm):
 @router.get("/signout")
 @router.get("/signout")
 async def signout(request: Request, response: Response):
 async def signout(request: Request, response: Response):
     response.delete_cookie("token")
     response.delete_cookie("token")
+    response.delete_cookie("oui-session")
 
 
     if ENABLE_OAUTH_SIGNUP.value:
     if ENABLE_OAUTH_SIGNUP.value:
         oauth_id_token = request.cookies.get("oauth_id_token")
         oauth_id_token = request.cookies.get("oauth_id_token")

+ 4 - 2
backend/open_webui/routers/chats.py

@@ -684,8 +684,10 @@ async def archive_chat_by_id(id: str, user=Depends(get_verified_user)):
 
 
 @router.post("/{id}/share", response_model=Optional[ChatResponse])
 @router.post("/{id}/share", response_model=Optional[ChatResponse])
 async def share_chat_by_id(request: Request, id: str, user=Depends(get_verified_user)):
 async def share_chat_by_id(request: Request, id: str, user=Depends(get_verified_user)):
-    if not has_permission(
-        user.id, "chat.share", request.app.state.config.USER_PERMISSIONS
+    if (user.role != "admin") and (
+        not has_permission(
+            user.id, "chat.share", request.app.state.config.USER_PERMISSIONS
+        )
     ):
     ):
         raise HTTPException(
         raise HTTPException(
             status_code=status.HTTP_401_UNAUTHORIZED,
             status_code=status.HTTP_401_UNAUTHORIZED,

+ 14 - 7
backend/open_webui/routers/configs.py

@@ -39,32 +39,39 @@ async def export_config(user=Depends(get_admin_user)):
 
 
 
 
 ############################
 ############################
-# Direct Connections Config
+# Connections Config
 ############################
 ############################
 
 
 
 
-class DirectConnectionsConfigForm(BaseModel):
+class ConnectionsConfigForm(BaseModel):
     ENABLE_DIRECT_CONNECTIONS: bool
     ENABLE_DIRECT_CONNECTIONS: bool
+    ENABLE_BASE_MODELS_CACHE: bool
 
 
 
 
-@router.get("/direct_connections", response_model=DirectConnectionsConfigForm)
-async def get_direct_connections_config(request: Request, user=Depends(get_admin_user)):
+@router.get("/connections", response_model=ConnectionsConfigForm)
+async def get_connections_config(request: Request, user=Depends(get_admin_user)):
     return {
     return {
         "ENABLE_DIRECT_CONNECTIONS": request.app.state.config.ENABLE_DIRECT_CONNECTIONS,
         "ENABLE_DIRECT_CONNECTIONS": request.app.state.config.ENABLE_DIRECT_CONNECTIONS,
+        "ENABLE_BASE_MODELS_CACHE": request.app.state.config.ENABLE_BASE_MODELS_CACHE,
     }
     }
 
 
 
 
-@router.post("/direct_connections", response_model=DirectConnectionsConfigForm)
-async def set_direct_connections_config(
+@router.post("/connections", response_model=ConnectionsConfigForm)
+async def set_connections_config(
     request: Request,
     request: Request,
-    form_data: DirectConnectionsConfigForm,
+    form_data: ConnectionsConfigForm,
     user=Depends(get_admin_user),
     user=Depends(get_admin_user),
 ):
 ):
     request.app.state.config.ENABLE_DIRECT_CONNECTIONS = (
     request.app.state.config.ENABLE_DIRECT_CONNECTIONS = (
         form_data.ENABLE_DIRECT_CONNECTIONS
         form_data.ENABLE_DIRECT_CONNECTIONS
     )
     )
+    request.app.state.config.ENABLE_BASE_MODELS_CACHE = (
+        form_data.ENABLE_BASE_MODELS_CACHE
+    )
+
     return {
     return {
         "ENABLE_DIRECT_CONNECTIONS": request.app.state.config.ENABLE_DIRECT_CONNECTIONS,
         "ENABLE_DIRECT_CONNECTIONS": request.app.state.config.ENABLE_DIRECT_CONNECTIONS,
+        "ENABLE_BASE_MODELS_CACHE": request.app.state.config.ENABLE_BASE_MODELS_CACHE,
     }
     }
 
 
 
 

+ 8 - 7
backend/open_webui/routers/files.py

@@ -155,17 +155,18 @@ def upload_file(
         if process:
         if process:
             try:
             try:
                 if file.content_type:
                 if file.content_type:
-                    stt_supported_content_types = (
-                        request.app.state.config.STT_SUPPORTED_CONTENT_TYPES
-                        or [
-                            "audio/*",
-                            "video/webm",
-                        ]
+                    stt_supported_content_types = getattr(
+                        request.app.state.config, "STT_SUPPORTED_CONTENT_TYPES", []
                     )
                     )
 
 
                     if any(
                     if any(
                         fnmatch(file.content_type, content_type)
                         fnmatch(file.content_type, content_type)
-                        for content_type in stt_supported_content_types
+                        for content_type in (
+                            stt_supported_content_types
+                            if stt_supported_content_types
+                            and any(t.strip() for t in stt_supported_content_types)
+                            else ["audio/*", "video/webm"]
+                        )
                     ):
                     ):
                         file_path = Storage.get_file(file_path)
                         file_path = Storage.get_file(file_path)
                         result = transcribe(request, file_path, file_metadata)
                         result = transcribe(request, file_path, file_metadata)

+ 22 - 6
backend/open_webui/routers/images.py

@@ -8,6 +8,7 @@ import re
 from pathlib import Path
 from pathlib import Path
 from typing import Optional
 from typing import Optional
 
 
+from urllib.parse import quote
 import requests
 import requests
 from fastapi import APIRouter, Depends, HTTPException, Request, UploadFile
 from fastapi import APIRouter, Depends, HTTPException, Request, UploadFile
 from open_webui.config import CACHE_DIR
 from open_webui.config import CACHE_DIR
@@ -302,8 +303,16 @@ async def update_image_config(
 ):
 ):
     set_image_model(request, form_data.MODEL)
     set_image_model(request, form_data.MODEL)
 
 
+    if form_data.IMAGE_SIZE == "auto" and form_data.MODEL != "gpt-image-1":
+        raise HTTPException(
+            status_code=400,
+            detail=ERROR_MESSAGES.INCORRECT_FORMAT(
+                "  (auto is only allowed with gpt-image-1)."
+            ),
+        )
+
     pattern = r"^\d+x\d+$"
     pattern = r"^\d+x\d+$"
-    if re.match(pattern, form_data.IMAGE_SIZE):
+    if form_data.IMAGE_SIZE == "auto" or re.match(pattern, form_data.IMAGE_SIZE):
         request.app.state.config.IMAGE_SIZE = form_data.IMAGE_SIZE
         request.app.state.config.IMAGE_SIZE = form_data.IMAGE_SIZE
     else:
     else:
         raise HTTPException(
         raise HTTPException(
@@ -471,7 +480,14 @@ async def image_generations(
     form_data: GenerateImageForm,
     form_data: GenerateImageForm,
     user=Depends(get_verified_user),
     user=Depends(get_verified_user),
 ):
 ):
-    width, height = tuple(map(int, request.app.state.config.IMAGE_SIZE.split("x")))
+    # if IMAGE_SIZE = 'auto', default WidthxHeight to the 512x512 default
+    # This is only relevant when the user has set IMAGE_SIZE to 'auto' with an
+    # image model other than gpt-image-1, which is warned about on settings save
+    width, height = (
+        tuple(map(int, request.app.state.config.IMAGE_SIZE.split("x")))
+        if "x" in request.app.state.config.IMAGE_SIZE
+        else (512, 512)
+    )
 
 
     r = None
     r = None
     try:
     try:
@@ -483,10 +499,10 @@ async def image_generations(
             headers["Content-Type"] = "application/json"
             headers["Content-Type"] = "application/json"
 
 
             if ENABLE_FORWARD_USER_INFO_HEADERS:
             if ENABLE_FORWARD_USER_INFO_HEADERS:
-                headers["X-OpenWebUI-User-Name"] = user.name
-                headers["X-OpenWebUI-User-Id"] = user.id
-                headers["X-OpenWebUI-User-Email"] = user.email
-                headers["X-OpenWebUI-User-Role"] = user.role
+                headers["X-OpenWebUI-User-Name"] = quote(user.name)
+                headers["X-OpenWebUI-User-Id"] = quote(user.id)
+                headers["X-OpenWebUI-User-Email"] = quote(user.email)
+                headers["X-OpenWebUI-User-Role"] = quote(user.role)
 
 
             data = {
             data = {
                 "model": (
                 "model": (

+ 39 - 37
backend/open_webui/routers/ollama.py

@@ -16,6 +16,7 @@ from urllib.parse import urlparse
 import aiohttp
 import aiohttp
 from aiocache import cached
 from aiocache import cached
 import requests
 import requests
+from urllib.parse import quote
 
 
 from open_webui.models.chats import Chats
 from open_webui.models.chats import Chats
 from open_webui.models.users import UserModel
 from open_webui.models.users import UserModel
@@ -58,6 +59,7 @@ from open_webui.config import (
 from open_webui.env import (
 from open_webui.env import (
     ENV,
     ENV,
     SRC_LOG_LEVELS,
     SRC_LOG_LEVELS,
+    MODELS_CACHE_TTL,
     AIOHTTP_CLIENT_SESSION_SSL,
     AIOHTTP_CLIENT_SESSION_SSL,
     AIOHTTP_CLIENT_TIMEOUT,
     AIOHTTP_CLIENT_TIMEOUT,
     AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST,
     AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST,
@@ -87,10 +89,10 @@ async def send_get_request(url, key=None, user: UserModel = None):
                     **({"Authorization": f"Bearer {key}"} if key else {}),
                     **({"Authorization": f"Bearer {key}"} if key else {}),
                     **(
                     **(
                         {
                         {
-                            "X-OpenWebUI-User-Name": user.name,
-                            "X-OpenWebUI-User-Id": user.id,
-                            "X-OpenWebUI-User-Email": user.email,
-                            "X-OpenWebUI-User-Role": user.role,
+                            "X-OpenWebUI-User-Name": quote(user.name),
+                            "X-OpenWebUI-User-Id": quote(user.id),
+                            "X-OpenWebUI-User-Email": quote(user.email),
+                            "X-OpenWebUI-User-Role": quote(user.role),
                         }
                         }
                         if ENABLE_FORWARD_USER_INFO_HEADERS and user
                         if ENABLE_FORWARD_USER_INFO_HEADERS and user
                         else {}
                         else {}
@@ -138,10 +140,10 @@ async def send_post_request(
                 **({"Authorization": f"Bearer {key}"} if key else {}),
                 **({"Authorization": f"Bearer {key}"} if key else {}),
                 **(
                 **(
                     {
                     {
-                        "X-OpenWebUI-User-Name": user.name,
-                        "X-OpenWebUI-User-Id": user.id,
-                        "X-OpenWebUI-User-Email": user.email,
-                        "X-OpenWebUI-User-Role": user.role,
+                        "X-OpenWebUI-User-Name": quote(user.name),
+                        "X-OpenWebUI-User-Id": quote(user.id),
+                        "X-OpenWebUI-User-Email": quote(user.email),
+                        "X-OpenWebUI-User-Role": quote(user.role),
                     }
                     }
                     if ENABLE_FORWARD_USER_INFO_HEADERS and user
                     if ENABLE_FORWARD_USER_INFO_HEADERS and user
                     else {}
                     else {}
@@ -242,10 +244,10 @@ async def verify_connection(
                     **({"Authorization": f"Bearer {key}"} if key else {}),
                     **({"Authorization": f"Bearer {key}"} if key else {}),
                     **(
                     **(
                         {
                         {
-                            "X-OpenWebUI-User-Name": user.name,
-                            "X-OpenWebUI-User-Id": user.id,
-                            "X-OpenWebUI-User-Email": user.email,
-                            "X-OpenWebUI-User-Role": user.role,
+                            "X-OpenWebUI-User-Name": quote(user.name),
+                            "X-OpenWebUI-User-Id": quote(user.id),
+                            "X-OpenWebUI-User-Email": quote(user.email),
+                            "X-OpenWebUI-User-Role": quote(user.role),
                         }
                         }
                         if ENABLE_FORWARD_USER_INFO_HEADERS and user
                         if ENABLE_FORWARD_USER_INFO_HEADERS and user
                         else {}
                         else {}
@@ -329,7 +331,7 @@ def merge_ollama_models_lists(model_lists):
     return list(merged_models.values())
     return list(merged_models.values())
 
 
 
 
-@cached(ttl=1)
+@cached(ttl=MODELS_CACHE_TTL)
 async def get_all_models(request: Request, user: UserModel = None):
 async def get_all_models(request: Request, user: UserModel = None):
     log.info("get_all_models()")
     log.info("get_all_models()")
     if request.app.state.config.ENABLE_OLLAMA_API:
     if request.app.state.config.ENABLE_OLLAMA_API:
@@ -462,10 +464,10 @@ async def get_ollama_tags(
                     **({"Authorization": f"Bearer {key}"} if key else {}),
                     **({"Authorization": f"Bearer {key}"} if key else {}),
                     **(
                     **(
                         {
                         {
-                            "X-OpenWebUI-User-Name": user.name,
-                            "X-OpenWebUI-User-Id": user.id,
-                            "X-OpenWebUI-User-Email": user.email,
-                            "X-OpenWebUI-User-Role": user.role,
+                            "X-OpenWebUI-User-Name": quote(user.name),
+                            "X-OpenWebUI-User-Id": quote(user.id),
+                            "X-OpenWebUI-User-Email": quote(user.email),
+                            "X-OpenWebUI-User-Role": quote(user.role),
                         }
                         }
                         if ENABLE_FORWARD_USER_INFO_HEADERS and user
                         if ENABLE_FORWARD_USER_INFO_HEADERS and user
                         else {}
                         else {}
@@ -824,10 +826,10 @@ async def copy_model(
                 **({"Authorization": f"Bearer {key}"} if key else {}),
                 **({"Authorization": f"Bearer {key}"} if key else {}),
                 **(
                 **(
                     {
                     {
-                        "X-OpenWebUI-User-Name": user.name,
-                        "X-OpenWebUI-User-Id": user.id,
-                        "X-OpenWebUI-User-Email": user.email,
-                        "X-OpenWebUI-User-Role": user.role,
+                        "X-OpenWebUI-User-Name": quote(user.name),
+                        "X-OpenWebUI-User-Id": quote(user.id),
+                        "X-OpenWebUI-User-Email": quote(user.email),
+                        "X-OpenWebUI-User-Role": quote(user.role),
                     }
                     }
                     if ENABLE_FORWARD_USER_INFO_HEADERS and user
                     if ENABLE_FORWARD_USER_INFO_HEADERS and user
                     else {}
                     else {}
@@ -890,10 +892,10 @@ async def delete_model(
                 **({"Authorization": f"Bearer {key}"} if key else {}),
                 **({"Authorization": f"Bearer {key}"} if key else {}),
                 **(
                 **(
                     {
                     {
-                        "X-OpenWebUI-User-Name": user.name,
-                        "X-OpenWebUI-User-Id": user.id,
-                        "X-OpenWebUI-User-Email": user.email,
-                        "X-OpenWebUI-User-Role": user.role,
+                        "X-OpenWebUI-User-Name": quote(user.name),
+                        "X-OpenWebUI-User-Id": quote(user.id),
+                        "X-OpenWebUI-User-Email": quote(user.email),
+                        "X-OpenWebUI-User-Role": quote(user.role),
                     }
                     }
                     if ENABLE_FORWARD_USER_INFO_HEADERS and user
                     if ENABLE_FORWARD_USER_INFO_HEADERS and user
                     else {}
                     else {}
@@ -949,10 +951,10 @@ async def show_model_info(
                 **({"Authorization": f"Bearer {key}"} if key else {}),
                 **({"Authorization": f"Bearer {key}"} if key else {}),
                 **(
                 **(
                     {
                     {
-                        "X-OpenWebUI-User-Name": user.name,
-                        "X-OpenWebUI-User-Id": user.id,
-                        "X-OpenWebUI-User-Email": user.email,
-                        "X-OpenWebUI-User-Role": user.role,
+                        "X-OpenWebUI-User-Name": quote(user.name),
+                        "X-OpenWebUI-User-Id": quote(user.id),
+                        "X-OpenWebUI-User-Email": quote(user.email),
+                        "X-OpenWebUI-User-Role": quote(user.role),
                     }
                     }
                     if ENABLE_FORWARD_USER_INFO_HEADERS and user
                     if ENABLE_FORWARD_USER_INFO_HEADERS and user
                     else {}
                     else {}
@@ -1036,10 +1038,10 @@ async def embed(
                 **({"Authorization": f"Bearer {key}"} if key else {}),
                 **({"Authorization": f"Bearer {key}"} if key else {}),
                 **(
                 **(
                     {
                     {
-                        "X-OpenWebUI-User-Name": user.name,
-                        "X-OpenWebUI-User-Id": user.id,
-                        "X-OpenWebUI-User-Email": user.email,
-                        "X-OpenWebUI-User-Role": user.role,
+                        "X-OpenWebUI-User-Name": quote(user.name),
+                        "X-OpenWebUI-User-Id": quote(user.id),
+                        "X-OpenWebUI-User-Email": quote(user.email),
+                        "X-OpenWebUI-User-Role": quote(user.role),
                     }
                     }
                     if ENABLE_FORWARD_USER_INFO_HEADERS and user
                     if ENABLE_FORWARD_USER_INFO_HEADERS and user
                     else {}
                     else {}
@@ -1123,10 +1125,10 @@ async def embeddings(
                 **({"Authorization": f"Bearer {key}"} if key else {}),
                 **({"Authorization": f"Bearer {key}"} if key else {}),
                 **(
                 **(
                     {
                     {
-                        "X-OpenWebUI-User-Name": user.name,
-                        "X-OpenWebUI-User-Id": user.id,
-                        "X-OpenWebUI-User-Email": user.email,
-                        "X-OpenWebUI-User-Role": user.role,
+                        "X-OpenWebUI-User-Name": quote(user.name),
+                        "X-OpenWebUI-User-Id": quote(user.id),
+                        "X-OpenWebUI-User-Email": quote(user.email),
+                        "X-OpenWebUI-User-Role": quote(user.role),
                     }
                     }
                     if ENABLE_FORWARD_USER_INFO_HEADERS and user
                     if ENABLE_FORWARD_USER_INFO_HEADERS and user
                     else {}
                     else {}

+ 55 - 44
backend/open_webui/routers/openai.py

@@ -8,7 +8,7 @@ from typing import Literal, Optional, overload
 import aiohttp
 import aiohttp
 from aiocache import cached
 from aiocache import cached
 import requests
 import requests
-
+from urllib.parse import quote
 
 
 from fastapi import Depends, FastAPI, HTTPException, Request, APIRouter
 from fastapi import Depends, FastAPI, HTTPException, Request, APIRouter
 from fastapi.middleware.cors import CORSMiddleware
 from fastapi.middleware.cors import CORSMiddleware
@@ -21,6 +21,7 @@ from open_webui.config import (
     CACHE_DIR,
     CACHE_DIR,
 )
 )
 from open_webui.env import (
 from open_webui.env import (
+    MODELS_CACHE_TTL,
     AIOHTTP_CLIENT_SESSION_SSL,
     AIOHTTP_CLIENT_SESSION_SSL,
     AIOHTTP_CLIENT_TIMEOUT,
     AIOHTTP_CLIENT_TIMEOUT,
     AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST,
     AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST,
@@ -66,10 +67,10 @@ async def send_get_request(url, key=None, user: UserModel = None):
                     **({"Authorization": f"Bearer {key}"} if key else {}),
                     **({"Authorization": f"Bearer {key}"} if key else {}),
                     **(
                     **(
                         {
                         {
-                            "X-OpenWebUI-User-Name": user.name,
-                            "X-OpenWebUI-User-Id": user.id,
-                            "X-OpenWebUI-User-Email": user.email,
-                            "X-OpenWebUI-User-Role": user.role,
+                            "X-OpenWebUI-User-Name": quote(user.name),
+                            "X-OpenWebUI-User-Id": quote(user.id),
+                            "X-OpenWebUI-User-Email": quote(user.email),
+                            "X-OpenWebUI-User-Role": quote(user.role),
                         }
                         }
                         if ENABLE_FORWARD_USER_INFO_HEADERS and user
                         if ENABLE_FORWARD_USER_INFO_HEADERS and user
                         else {}
                         else {}
@@ -225,10 +226,10 @@ async def speech(request: Request, user=Depends(get_verified_user)):
                     ),
                     ),
                     **(
                     **(
                         {
                         {
-                            "X-OpenWebUI-User-Name": user.name,
-                            "X-OpenWebUI-User-Id": user.id,
-                            "X-OpenWebUI-User-Email": user.email,
-                            "X-OpenWebUI-User-Role": user.role,
+                            "X-OpenWebUI-User-Name": quote(user.name),
+                            "X-OpenWebUI-User-Id": quote(user.id),
+                            "X-OpenWebUI-User-Email": quote(user.email),
+                            "X-OpenWebUI-User-Role": quote(user.role),
                         }
                         }
                         if ENABLE_FORWARD_USER_INFO_HEADERS
                         if ENABLE_FORWARD_USER_INFO_HEADERS
                         else {}
                         else {}
@@ -386,7 +387,7 @@ async def get_filtered_models(models, user):
     return filtered_models
     return filtered_models
 
 
 
 
-@cached(ttl=1)
+@cached(ttl=MODELS_CACHE_TTL)
 async def get_all_models(request: Request, user: UserModel) -> dict[str, list]:
 async def get_all_models(request: Request, user: UserModel) -> dict[str, list]:
     log.info("get_all_models()")
     log.info("get_all_models()")
 
 
@@ -478,10 +479,10 @@ async def get_models(
                     "Content-Type": "application/json",
                     "Content-Type": "application/json",
                     **(
                     **(
                         {
                         {
-                            "X-OpenWebUI-User-Name": user.name,
-                            "X-OpenWebUI-User-Id": user.id,
-                            "X-OpenWebUI-User-Email": user.email,
-                            "X-OpenWebUI-User-Role": user.role,
+                            "X-OpenWebUI-User-Name": quote(user.name),
+                            "X-OpenWebUI-User-Id": quote(user.id),
+                            "X-OpenWebUI-User-Email": quote(user.email),
+                            "X-OpenWebUI-User-Role": quote(user.role),
                         }
                         }
                         if ENABLE_FORWARD_USER_INFO_HEADERS
                         if ENABLE_FORWARD_USER_INFO_HEADERS
                         else {}
                         else {}
@@ -573,10 +574,10 @@ async def verify_connection(
                 "Content-Type": "application/json",
                 "Content-Type": "application/json",
                 **(
                 **(
                     {
                     {
-                        "X-OpenWebUI-User-Name": user.name,
-                        "X-OpenWebUI-User-Id": user.id,
-                        "X-OpenWebUI-User-Email": user.email,
-                        "X-OpenWebUI-User-Role": user.role,
+                        "X-OpenWebUI-User-Name": quote(user.name),
+                        "X-OpenWebUI-User-Id": quote(user.id),
+                        "X-OpenWebUI-User-Email": quote(user.email),
+                        "X-OpenWebUI-User-Role": quote(user.role),
                     }
                     }
                     if ENABLE_FORWARD_USER_INFO_HEADERS
                     if ENABLE_FORWARD_USER_INFO_HEADERS
                     else {}
                     else {}
@@ -633,13 +634,7 @@ async def verify_connection(
             raise HTTPException(status_code=500, detail=error_detail)
             raise HTTPException(status_code=500, detail=error_detail)
 
 
 
 
-def convert_to_azure_payload(
-    url,
-    payload: dict,
-):
-    model = payload.get("model", "")
-
-    # Filter allowed parameters based on Azure OpenAI API
+def get_azure_allowed_params(api_version: str) -> set[str]:
     allowed_params = {
     allowed_params = {
         "messages",
         "messages",
         "temperature",
         "temperature",
@@ -669,6 +664,23 @@ def convert_to_azure_payload(
         "max_completion_tokens",
         "max_completion_tokens",
     }
     }
 
 
+    try:
+        if api_version >= "2024-09-01-preview":
+            allowed_params.add("stream_options")
+    except ValueError:
+        log.debug(
+            f"Invalid API version {api_version} for Azure OpenAI. Defaulting to allowed parameters."
+        )
+
+    return allowed_params
+
+
+def convert_to_azure_payload(url, payload: dict, api_version: str):
+    model = payload.get("model", "")
+
+    # Filter allowed parameters based on Azure OpenAI API
+    allowed_params = get_azure_allowed_params(api_version)
+
     # Special handling for o-series models
     # Special handling for o-series models
     if model.startswith("o") and model.endswith("-mini"):
     if model.startswith("o") and model.endswith("-mini"):
         # Convert max_tokens to max_completion_tokens for o-series models
         # Convert max_tokens to max_completion_tokens for o-series models
@@ -806,10 +818,10 @@ async def generate_chat_completion(
         ),
         ),
         **(
         **(
             {
             {
-                "X-OpenWebUI-User-Name": user.name,
-                "X-OpenWebUI-User-Id": user.id,
-                "X-OpenWebUI-User-Email": user.email,
-                "X-OpenWebUI-User-Role": user.role,
+                "X-OpenWebUI-User-Name": quote(user.name),
+                "X-OpenWebUI-User-Id": quote(user.id),
+                "X-OpenWebUI-User-Email": quote(user.email),
+                "X-OpenWebUI-User-Role": quote(user.role),
             }
             }
             if ENABLE_FORWARD_USER_INFO_HEADERS
             if ENABLE_FORWARD_USER_INFO_HEADERS
             else {}
             else {}
@@ -817,8 +829,8 @@ async def generate_chat_completion(
     }
     }
 
 
     if api_config.get("azure", False):
     if api_config.get("azure", False):
-        request_url, payload = convert_to_azure_payload(url, payload)
-        api_version = api_config.get("api_version", "") or "2023-03-15-preview"
+        api_version = api_config.get("api_version", "2023-03-15-preview")
+        request_url, payload = convert_to_azure_payload(url, payload, api_version)
         headers["api-key"] = key
         headers["api-key"] = key
         headers["api-version"] = api_version
         headers["api-version"] = api_version
         request_url = f"{request_url}/chat/completions?api-version={api_version}"
         request_url = f"{request_url}/chat/completions?api-version={api_version}"
@@ -924,10 +936,10 @@ async def embeddings(request: Request, form_data: dict, user):
                 "Content-Type": "application/json",
                 "Content-Type": "application/json",
                 **(
                 **(
                     {
                     {
-                        "X-OpenWebUI-User-Name": user.name,
-                        "X-OpenWebUI-User-Id": user.id,
-                        "X-OpenWebUI-User-Email": user.email,
-                        "X-OpenWebUI-User-Role": user.role,
+                        "X-OpenWebUI-User-Name": quote(user.name),
+                        "X-OpenWebUI-User-Id": quote(user.id),
+                        "X-OpenWebUI-User-Email": quote(user.email),
+                        "X-OpenWebUI-User-Role": quote(user.role),
                     }
                     }
                     if ENABLE_FORWARD_USER_INFO_HEADERS and user
                     if ENABLE_FORWARD_USER_INFO_HEADERS and user
                     else {}
                     else {}
@@ -996,10 +1008,10 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
             "Content-Type": "application/json",
             "Content-Type": "application/json",
             **(
             **(
                 {
                 {
-                    "X-OpenWebUI-User-Name": user.name,
-                    "X-OpenWebUI-User-Id": user.id,
-                    "X-OpenWebUI-User-Email": user.email,
-                    "X-OpenWebUI-User-Role": user.role,
+                    "X-OpenWebUI-User-Name": quote(user.name),
+                    "X-OpenWebUI-User-Id": quote(user.id),
+                    "X-OpenWebUI-User-Email": quote(user.email),
+                    "X-OpenWebUI-User-Role": quote(user.role),
                 }
                 }
                 if ENABLE_FORWARD_USER_INFO_HEADERS
                 if ENABLE_FORWARD_USER_INFO_HEADERS
                 else {}
                 else {}
@@ -1007,16 +1019,15 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
         }
         }
 
 
         if api_config.get("azure", False):
         if api_config.get("azure", False):
+            api_version = api_config.get("api_version", "2023-03-15-preview")
             headers["api-key"] = key
             headers["api-key"] = key
-            headers["api-version"] = (
-                api_config.get("api_version", "") or "2023-03-15-preview"
-            )
+            headers["api-version"] = api_version
 
 
             payload = json.loads(body)
             payload = json.loads(body)
-            url, payload = convert_to_azure_payload(url, payload)
+            url, payload = convert_to_azure_payload(url, payload, api_version)
             body = json.dumps(payload).encode()
             body = json.dumps(payload).encode()
 
 
-            request_url = f"{url}/{path}?api-version={api_config.get('api_version', '2023-03-15-preview')}"
+            request_url = f"{url}/{path}?api-version={api_version}"
         else:
         else:
             headers["Authorization"] = f"Bearer {key}"
             headers["Authorization"] = f"Bearer {key}"
             request_url = f"{url}/{path}"
             request_url = f"{url}/{path}"

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

@@ -1747,6 +1747,16 @@ def search_web(request: Request, engine: str, query: str) -> list[SearchResult]:
             )
             )
         else:
         else:
             raise Exception("No TAVILY_API_KEY found in environment variables")
             raise Exception("No TAVILY_API_KEY found in environment variables")
+    elif engine == "exa":
+        if request.app.state.config.EXA_API_KEY:
+            return search_exa(
+                request.app.state.config.EXA_API_KEY,
+                query,
+                request.app.state.config.WEB_SEARCH_RESULT_COUNT,
+                request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST,
+            )
+        else:
+            raise Exception("No EXA_API_KEY found in environment variables")
     elif engine == "searchapi":
     elif engine == "searchapi":
         if request.app.state.config.SEARCHAPI_API_KEY:
         if request.app.state.config.SEARCHAPI_API_KEY:
             return search_searchapi(
             return search_searchapi(
@@ -1784,6 +1794,13 @@ def search_web(request: Request, engine: str, query: str) -> list[SearchResult]:
             request.app.state.config.WEB_SEARCH_RESULT_COUNT,
             request.app.state.config.WEB_SEARCH_RESULT_COUNT,
             request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST,
             request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST,
         )
         )
+    elif engine == "exa":
+        return search_exa(
+            request.app.state.config.EXA_API_KEY,
+            query,
+            request.app.state.config.WEB_SEARCH_RESULT_COUNT,
+            request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST,
+        )
     elif engine == "perplexity":
     elif engine == "perplexity":
         return search_perplexity(
         return search_perplexity(
             request.app.state.config.PERPLEXITY_API_KEY,
             request.app.state.config.PERPLEXITY_API_KEY,

+ 22 - 4
backend/open_webui/socket/main.py

@@ -1,4 +1,6 @@
 import asyncio
 import asyncio
+import random
+
 import socketio
 import socketio
 import logging
 import logging
 import sys
 import sys
@@ -105,10 +107,26 @@ else:
 
 
 
 
 async def periodic_usage_pool_cleanup():
 async def periodic_usage_pool_cleanup():
-    if not aquire_func():
-        log.debug("Usage pool cleanup lock already exists. Not running it.")
-        return
-    log.debug("Running periodic_usage_pool_cleanup")
+    max_retries = 2
+    retry_delay = random.uniform(
+        WEBSOCKET_REDIS_LOCK_TIMEOUT / 2, WEBSOCKET_REDIS_LOCK_TIMEOUT
+    )
+    for attempt in range(max_retries + 1):
+        if aquire_func():
+            break
+        else:
+            if attempt < max_retries:
+                log.debug(
+                    f"Cleanup lock already exists. Retry {attempt + 1} after {retry_delay}s..."
+                )
+                await asyncio.sleep(retry_delay)
+            else:
+                log.warning(
+                    "Failed to acquire cleanup lock after retries. Skipping cleanup."
+                )
+                return
+
+    log.debug("Running periodic_cleanup")
     try:
     try:
         while True:
         while True:
             if not renew_func():
             if not renew_func():

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

@@ -419,7 +419,7 @@ async def chat_action(request: Request, action_id: str, form_data: dict, user: A
                     params[key] = value
                     params[key] = value
 
 
             if "__user__" in sig.parameters:
             if "__user__" in sig.parameters:
-                __user__ = (user.model_dump() if isinstance(user, UserModel) else {},)
+                __user__ = user.model_dump() if isinstance(user, UserModel) else {}
 
 
                 try:
                 try:
                     if hasattr(function_module, "UserValves"):
                     if hasattr(function_module, "UserValves"):

+ 34 - 26
backend/open_webui/utils/middleware.py

@@ -248,6 +248,7 @@ async def chat_completion_tools_handler(
                         if tool_id
                         if tool_id
                         else f"{tool_function_name}"
                         else f"{tool_function_name}"
                     )
                     )
+
                     if tool.get("metadata", {}).get("citation", False) or tool.get(
                     if tool.get("metadata", {}).get("citation", False) or tool.get(
                         "direct", False
                         "direct", False
                     ):
                     ):
@@ -718,6 +719,10 @@ def apply_params_to_form_data(form_data, model):
 
 
 
 
 async def process_chat_payload(request, form_data, user, metadata, model):
 async def process_chat_payload(request, form_data, user, metadata, model):
+    # Pipeline Inlet -> Filter Inlet -> Chat Memory -> Chat Web Search -> Chat Image Generation
+    # -> Chat Code Interpreter (Form Data Update) -> (Default) Chat Tools Function Calling
+    # -> Chat Files
+
     form_data = apply_params_to_form_data(form_data, model)
     form_data = apply_params_to_form_data(form_data, model)
     log.debug(f"form_data: {form_data}")
     log.debug(f"form_data: {form_data}")
 
 
@@ -804,7 +809,6 @@ async def process_chat_payload(request, form_data, user, metadata, model):
         raise e
         raise e
 
 
     try:
     try:
-
         filter_functions = [
         filter_functions = [
             Functions.get_function_by_id(filter_id)
             Functions.get_function_by_id(filter_id)
             for filter_id in get_sorted_filter_ids(
             for filter_id in get_sorted_filter_ids(
@@ -912,7 +916,6 @@ async def process_chat_payload(request, form_data, user, metadata, model):
                     request, form_data, extra_params, user, models, tools_dict
                     request, form_data, extra_params, user, models, tools_dict
                 )
                 )
                 sources.extend(flags.get("sources", []))
                 sources.extend(flags.get("sources", []))
-
             except Exception as e:
             except Exception as e:
                 log.exception(e)
                 log.exception(e)
 
 
@@ -925,24 +928,27 @@ async def process_chat_payload(request, form_data, user, metadata, model):
     # If context is not empty, insert it into the messages
     # If context is not empty, insert it into the messages
     if len(sources) > 0:
     if len(sources) > 0:
         context_string = ""
         context_string = ""
-        citation_idx = {}
+        citation_idx_map = {}
+
         for source in sources:
         for source in sources:
             if "document" in source:
             if "document" in source:
-                for doc_context, doc_meta in zip(
+                for document_text, document_metadata in zip(
                     source["document"], source["metadata"]
                     source["document"], source["metadata"]
                 ):
                 ):
                     source_name = source.get("source", {}).get("name", None)
                     source_name = source.get("source", {}).get("name", None)
-                    citation_id = (
-                        doc_meta.get("source", None)
+                    source_id = (
+                        document_metadata.get("source", None)
                         or source.get("source", {}).get("id", None)
                         or source.get("source", {}).get("id", None)
                         or "N/A"
                         or "N/A"
                     )
                     )
-                    if citation_id not in citation_idx:
-                        citation_idx[citation_id] = len(citation_idx) + 1
+
+                    if source_id not in citation_idx_map:
+                        citation_idx_map[source_id] = len(citation_idx_map) + 1
+
                     context_string += (
                     context_string += (
-                        f'<source id="{citation_idx[citation_id]}"'
+                        f'<source id="{citation_idx_map[source_id]}"'
                         + (f' name="{source_name}"' if source_name else "")
                         + (f' name="{source_name}"' if source_name else "")
-                        + f">{doc_context}</source>\n"
+                        + f">{document_text}</source>\n"
                     )
                     )
 
 
         context_string = context_string.strip()
         context_string = context_string.strip()
@@ -1370,7 +1376,7 @@ async def process_chat_response(
             return len(backtick_segments) > 1 and len(backtick_segments) % 2 == 0
             return len(backtick_segments) > 1 and len(backtick_segments) % 2 == 0
 
 
         # Handle as a background task
         # Handle as a background task
-        async def post_response_handler(response, events):
+        async def response_handler(response, events):
             def serialize_content_blocks(content_blocks, raw=False):
             def serialize_content_blocks(content_blocks, raw=False):
                 content = ""
                 content = ""
 
 
@@ -1741,7 +1747,7 @@ async def process_chat_response(
                         },
                         },
                     )
                     )
 
 
-                async def stream_body_handler(response):
+                async def stream_body_handler(response, form_data):
                     nonlocal content
                     nonlocal content
                     nonlocal content_blocks
                     nonlocal content_blocks
 
 
@@ -1770,7 +1776,7 @@ async def process_chat_response(
                                 filter_functions=filter_functions,
                                 filter_functions=filter_functions,
                                 filter_type="stream",
                                 filter_type="stream",
                                 form_data=data,
                                 form_data=data,
-                                extra_params=extra_params,
+                                extra_params={"__body__": form_data, **extra_params},
                             )
                             )
 
 
                             if data:
                             if data:
@@ -2032,7 +2038,7 @@ async def process_chat_response(
                     if response.background:
                     if response.background:
                         await response.background()
                         await response.background()
 
 
-                await stream_body_handler(response)
+                await stream_body_handler(response, form_data)
 
 
                 MAX_TOOL_CALL_RETRIES = 10
                 MAX_TOOL_CALL_RETRIES = 10
                 tool_call_retries = 0
                 tool_call_retries = 0
@@ -2181,22 +2187,24 @@ async def process_chat_response(
                     )
                     )
 
 
                     try:
                     try:
+                        new_form_data = {
+                            "model": model_id,
+                            "stream": True,
+                            "tools": form_data["tools"],
+                            "messages": [
+                                *form_data["messages"],
+                                *convert_content_blocks_to_messages(content_blocks),
+                            ],
+                        }
+
                         res = await generate_chat_completion(
                         res = await generate_chat_completion(
                             request,
                             request,
-                            {
-                                "model": model_id,
-                                "stream": True,
-                                "tools": form_data["tools"],
-                                "messages": [
-                                    *form_data["messages"],
-                                    *convert_content_blocks_to_messages(content_blocks),
-                                ],
-                            },
+                            new_form_data,
                             user,
                             user,
                         )
                         )
 
 
                         if isinstance(res, StreamingResponse):
                         if isinstance(res, StreamingResponse):
-                            await stream_body_handler(res)
+                            await stream_body_handler(res, new_form_data)
                         else:
                         else:
                             break
                             break
                     except Exception as e:
                     except Exception as e:
@@ -2427,9 +2435,9 @@ async def process_chat_response(
             if response.background is not None:
             if response.background is not None:
                 await response.background()
                 await response.background()
 
 
-        # background_tasks.add_task(post_response_handler, response, events)
+        # background_tasks.add_task(response_handler, response, events)
         task_id, _ = await create_task(
         task_id, _ = await create_task(
-            request, post_response_handler(response, events), id=metadata["chat_id"]
+            request, response_handler(response, events), id=metadata["chat_id"]
         )
         )
         return {"status": True, "task_id": task_id}
         return {"status": True, "task_id": task_id}
 
 

+ 10 - 2
backend/open_webui/utils/models.py

@@ -76,8 +76,16 @@ async def get_all_base_models(request: Request, user: UserModel = None):
     return function_models + openai_models + ollama_models
     return function_models + openai_models + ollama_models
 
 
 
 
-async def get_all_models(request, user: UserModel = None):
-    models = await get_all_base_models(request, user=user)
+async def get_all_models(request, refresh: bool = False, user: UserModel = None):
+    if (
+        request.app.state.MODELS
+        and request.app.state.BASE_MODELS
+        and (request.app.state.config.ENABLE_BASE_MODELS_CACHE and not refresh)
+    ):
+        models = request.app.state.BASE_MODELS
+    else:
+        models = await get_all_base_models(request, user=user)
+        request.app.state.BASE_MODELS = models
 
 
     # If there are no models, return an empty list
     # If there are no models, return an empty list
     if len(models) == 0:
     if len(models) == 0:

+ 28 - 1
backend/open_webui/utils/telemetry/setup.py

@@ -1,9 +1,13 @@
 from fastapi import FastAPI
 from fastapi import FastAPI
 from opentelemetry import trace
 from opentelemetry import trace
 from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
 from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
+from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
+    OTLPSpanExporter as HttpOTLPSpanExporter,
+)
 from opentelemetry.sdk.resources import SERVICE_NAME, Resource
 from opentelemetry.sdk.resources import SERVICE_NAME, Resource
 from opentelemetry.sdk.trace import TracerProvider
 from opentelemetry.sdk.trace import TracerProvider
 from sqlalchemy import Engine
 from sqlalchemy import Engine
+from base64 import b64encode
 
 
 from open_webui.utils.telemetry.exporters import LazyBatchSpanProcessor
 from open_webui.utils.telemetry.exporters import LazyBatchSpanProcessor
 from open_webui.utils.telemetry.instrumentors import Instrumentor
 from open_webui.utils.telemetry.instrumentors import Instrumentor
@@ -11,7 +15,11 @@ from open_webui.utils.telemetry.metrics import setup_metrics
 from open_webui.env import (
 from open_webui.env import (
     OTEL_SERVICE_NAME,
     OTEL_SERVICE_NAME,
     OTEL_EXPORTER_OTLP_ENDPOINT,
     OTEL_EXPORTER_OTLP_ENDPOINT,
+    OTEL_EXPORTER_OTLP_INSECURE,
     ENABLE_OTEL_METRICS,
     ENABLE_OTEL_METRICS,
+    OTEL_BASIC_AUTH_USERNAME,
+    OTEL_BASIC_AUTH_PASSWORD,
+    OTEL_OTLP_SPAN_EXPORTER,
 )
 )
 
 
 
 
@@ -22,8 +30,27 @@ def setup(app: FastAPI, db_engine: Engine):
             resource=Resource.create(attributes={SERVICE_NAME: OTEL_SERVICE_NAME})
             resource=Resource.create(attributes={SERVICE_NAME: OTEL_SERVICE_NAME})
         )
         )
     )
     )
+
+    # Add basic auth header only if both username and password are not empty
+    headers = []
+    if OTEL_BASIC_AUTH_USERNAME and OTEL_BASIC_AUTH_PASSWORD:
+        auth_string = f"{OTEL_BASIC_AUTH_USERNAME}:{OTEL_BASIC_AUTH_PASSWORD}"
+        auth_header = b64encode(auth_string.encode()).decode()
+        headers = [("authorization", f"Basic {auth_header}")]
+
     # otlp export
     # otlp export
-    exporter = OTLPSpanExporter(endpoint=OTEL_EXPORTER_OTLP_ENDPOINT)
+    if OTEL_OTLP_SPAN_EXPORTER == "http":
+        exporter = HttpOTLPSpanExporter(
+            endpoint=OTEL_EXPORTER_OTLP_ENDPOINT,
+            insecure=OTEL_EXPORTER_OTLP_INSECURE,
+            headers=headers,
+        )
+    else:
+        exporter = OTLPSpanExporter(
+            endpoint=OTEL_EXPORTER_OTLP_ENDPOINT,
+            insecure=OTEL_EXPORTER_OTLP_INSECURE,
+            headers=headers,
+        )
     trace.get_tracer_provider().add_span_processor(LazyBatchSpanProcessor(exporter))
     trace.get_tracer_provider().add_span_processor(LazyBatchSpanProcessor(exporter))
     Instrumentor(app=app, db_engine=db_engine).instrument()
     Instrumentor(app=app, db_engine=db_engine).instrument()
 
 

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

@@ -101,9 +101,6 @@ def get_tools(
 
 
                     def make_tool_function(function_name, token, tool_server_data):
                     def make_tool_function(function_name, token, tool_server_data):
                         async def tool_function(**kwargs):
                         async def tool_function(**kwargs):
-                            print(
-                                f"Executing tool function {function_name} with params: {kwargs}"
-                            )
                             return await execute_tool_server(
                             return await execute_tool_server(
                                 token=token,
                                 token=token,
                                 url=tool_server_data["url"],
                                 url=tool_server_data["url"],

+ 5 - 5
backend/requirements.txt

@@ -1,6 +1,6 @@
 fastapi==0.115.7
 fastapi==0.115.7
-uvicorn[standard]==0.34.2
-pydantic==2.10.6
+uvicorn[standard]==0.35.0
+pydantic==2.11.7
 python-multipart==0.0.20
 python-multipart==0.0.20
 
 
 python-socketio==5.13.0
 python-socketio==5.13.0
@@ -42,8 +42,8 @@ google-genai==1.15.0
 google-generativeai==0.8.5
 google-generativeai==0.8.5
 tiktoken
 tiktoken
 
 
-langchain==0.3.24
-langchain-community==0.3.23
+langchain==0.3.26
+langchain-community==0.3.26
 
 
 fake-useragent==2.1.0
 fake-useragent==2.1.0
 chromadb==0.6.3
 chromadb==0.6.3
@@ -114,7 +114,7 @@ pytest-docker~=3.1.1
 googleapis-common-protos==1.63.2
 googleapis-common-protos==1.63.2
 google-cloud-storage==2.19.0
 google-cloud-storage==2.19.0
 
 
-azure-identity==1.21.0
+azure-identity==1.23.0
 azure-storage-blob==12.24.1
 azure-storage-blob==12.24.1
 
 
 
 

+ 1 - 1
backend/start_windows.bat

@@ -28,7 +28,7 @@ SET "WEBUI_SECRET_KEY=%WEBUI_SECRET_KEY%"
 SET "WEBUI_JWT_SECRET_KEY=%WEBUI_JWT_SECRET_KEY%"
 SET "WEBUI_JWT_SECRET_KEY=%WEBUI_JWT_SECRET_KEY%"
 
 
 :: Check if WEBUI_SECRET_KEY and WEBUI_JWT_SECRET_KEY are not set
 :: Check if WEBUI_SECRET_KEY and WEBUI_JWT_SECRET_KEY are not set
-IF "%WEBUI_SECRET_KEY%%WEBUI_JWT_SECRET_KEY%" == " " (
+IF "%WEBUI_SECRET_KEY% %WEBUI_JWT_SECRET_KEY%" == " " (
     echo Loading WEBUI_SECRET_KEY from file, not provided as an environment variable.
     echo Loading WEBUI_SECRET_KEY from file, not provided as an environment variable.
 
 
     IF NOT EXIST "%KEY_FILE%" (
     IF NOT EXIST "%KEY_FILE%" (

+ 24 - 0
docker-compose.otel.yaml

@@ -0,0 +1,24 @@
+services:
+  grafana:
+    image: grafana/otel-lgtm:latest
+    container_name: lgtm
+    ports:
+      - "3000:3000"   # Grafana UI
+      - "4317:4317"   # OTLP/gRPC
+      - "4318:4318"   # OTLP/HTTP
+    restart: unless-stopped
+
+  open-webui:
+    image: ghcr.io/open-webui/open-webui:main
+    container_name: open-webui
+    depends_on: [grafana]
+    environment:
+      - ENABLE_OTEL=true
+      - OTEL_EXPORTER_OTLP_ENDPOINT=http://grafana:4317 
+      - OTEL_SERVICE_NAME=open-webui
+    ports:
+      - "8088:8080"
+    networks: [default]
+
+networks:
+  default:

+ 42 - 8
package-lock.json

@@ -32,6 +32,7 @@
 				"@xyflow/svelte": "^0.1.19",
 				"@xyflow/svelte": "^0.1.19",
 				"async": "^3.2.5",
 				"async": "^3.2.5",
 				"bits-ui": "^0.21.15",
 				"bits-ui": "^0.21.15",
+				"chart.js": "^4.5.0",
 				"codemirror": "^6.0.1",
 				"codemirror": "^6.0.1",
 				"codemirror-lang-elixir": "^4.0.0",
 				"codemirror-lang-elixir": "^4.0.0",
 				"codemirror-lang-hcl": "^0.1.0",
 				"codemirror-lang-hcl": "^0.1.0",
@@ -42,9 +43,10 @@
 				"file-saver": "^2.0.5",
 				"file-saver": "^2.0.5",
 				"focus-trap": "^7.6.4",
 				"focus-trap": "^7.6.4",
 				"fuse.js": "^7.0.0",
 				"fuse.js": "^7.0.0",
+				"heic2any": "^0.0.4",
 				"highlight.js": "^11.9.0",
 				"highlight.js": "^11.9.0",
 				"html-entities": "^2.5.3",
 				"html-entities": "^2.5.3",
-				"html2canvas-pro": "^1.5.8",
+				"html2canvas-pro": "^1.5.11",
 				"i18next": "^23.10.0",
 				"i18next": "^23.10.0",
 				"i18next-browser-languagedetector": "^7.2.0",
 				"i18next-browser-languagedetector": "^7.2.0",
 				"i18next-resources-to-backend": "^1.2.0",
 				"i18next-resources-to-backend": "^1.2.0",
@@ -53,6 +55,7 @@
 				"jspdf": "^3.0.0",
 				"jspdf": "^3.0.0",
 				"katex": "^0.16.22",
 				"katex": "^0.16.22",
 				"kokoro-js": "^1.1.1",
 				"kokoro-js": "^1.1.1",
+				"leaflet": "^1.9.4",
 				"marked": "^9.1.0",
 				"marked": "^9.1.0",
 				"mermaid": "^11.6.0",
 				"mermaid": "^11.6.0",
 				"paneforge": "^0.0.6",
 				"paneforge": "^0.0.6",
@@ -70,7 +73,7 @@
 				"prosemirror-view": "^1.34.3",
 				"prosemirror-view": "^1.34.3",
 				"pyodide": "^0.27.3",
 				"pyodide": "^0.27.3",
 				"socket.io-client": "^4.2.0",
 				"socket.io-client": "^4.2.0",
-				"sortablejs": "^1.15.2",
+				"sortablejs": "^1.15.6",
 				"svelte-sonner": "^0.3.19",
 				"svelte-sonner": "^0.3.19",
 				"tippy.js": "^6.3.7",
 				"tippy.js": "^6.3.7",
 				"turndown": "^7.2.0",
 				"turndown": "^7.2.0",
@@ -1870,6 +1873,12 @@
 				"@jridgewell/sourcemap-codec": "^1.4.14"
 				"@jridgewell/sourcemap-codec": "^1.4.14"
 			}
 			}
 		},
 		},
+		"node_modules/@kurkle/color": {
+			"version": "0.3.4",
+			"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
+			"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
+			"license": "MIT"
+		},
 		"node_modules/@lezer/common": {
 		"node_modules/@lezer/common": {
 			"version": "1.2.1",
 			"version": "1.2.1",
 			"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.1.tgz",
 			"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.1.tgz",
@@ -4723,6 +4732,18 @@
 				"url": "https://github.com/chalk/chalk?sponsor=1"
 				"url": "https://github.com/chalk/chalk?sponsor=1"
 			}
 			}
 		},
 		},
+		"node_modules/chart.js": {
+			"version": "4.5.0",
+			"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz",
+			"integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==",
+			"license": "MIT",
+			"dependencies": {
+				"@kurkle/color": "^0.3.0"
+			},
+			"engines": {
+				"pnpm": ">=8"
+			}
+		},
 		"node_modules/check-error": {
 		"node_modules/check-error": {
 			"version": "1.0.3",
 			"version": "1.0.3",
 			"resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz",
 			"resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz",
@@ -7295,6 +7316,12 @@
 				"node": ">= 0.4"
 				"node": ">= 0.4"
 			}
 			}
 		},
 		},
+		"node_modules/heic2any": {
+			"version": "0.0.4",
+			"resolved": "https://registry.npmjs.org/heic2any/-/heic2any-0.0.4.tgz",
+			"integrity": "sha512-3lLnZiDELfabVH87htnRolZ2iehX9zwpRyGNz22GKXIu0fznlblf0/ftppXKNqS26dqFSeqfIBhAmAj/uSp0cA==",
+			"license": "MIT"
+		},
 		"node_modules/heimdalljs": {
 		"node_modules/heimdalljs": {
 			"version": "0.2.6",
 			"version": "0.2.6",
 			"resolved": "https://registry.npmjs.org/heimdalljs/-/heimdalljs-0.2.6.tgz",
 			"resolved": "https://registry.npmjs.org/heimdalljs/-/heimdalljs-0.2.6.tgz",
@@ -7379,9 +7406,9 @@
 			}
 			}
 		},
 		},
 		"node_modules/html2canvas-pro": {
 		"node_modules/html2canvas-pro": {
-			"version": "1.5.8",
-			"resolved": "https://registry.npmjs.org/html2canvas-pro/-/html2canvas-pro-1.5.8.tgz",
-			"integrity": "sha512-bVGAU7IvhBwBlRAmX6QhekX8lsaxmYoF6zIwf/HNlHscjx+KN8jw/U4PQRYqeEVm9+m13hcS1l5ChJB9/e29Lw==",
+			"version": "1.5.11",
+			"resolved": "https://registry.npmjs.org/html2canvas-pro/-/html2canvas-pro-1.5.11.tgz",
+			"integrity": "sha512-W4pEeKLG8+9a54RDOSiEKq7gRXXDzt0ORMaLXX+l6a3urSKbmnkmyzcRDCtgTOzmHLaZTLG2wiTQMJqKLlSh3w==",
 			"license": "MIT",
 			"license": "MIT",
 			"dependencies": {
 			"dependencies": {
 				"css-line-break": "^2.1.0",
 				"css-line-break": "^2.1.0",
@@ -8046,6 +8073,12 @@
 				"node": ">=10.13.0"
 				"node": ">=10.13.0"
 			}
 			}
 		},
 		},
+		"node_modules/leaflet": {
+			"version": "1.9.4",
+			"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
+			"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
+			"license": "BSD-2-Clause"
+		},
 		"node_modules/levn": {
 		"node_modules/levn": {
 			"version": "0.4.1",
 			"version": "0.4.1",
 			"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
 			"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
@@ -11138,9 +11171,10 @@
 			}
 			}
 		},
 		},
 		"node_modules/sortablejs": {
 		"node_modules/sortablejs": {
-			"version": "1.15.2",
-			"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.2.tgz",
-			"integrity": "sha512-FJF5jgdfvoKn1MAKSdGs33bIqLi3LmsgVTliuX6iITj834F+JRQZN90Z93yql8h0K2t0RwDPBmxwlbZfDcxNZA=="
+			"version": "1.15.6",
+			"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.6.tgz",
+			"integrity": "sha512-aNfiuwMEpfBM/CN6LY0ibyhxPfPbyFeBTYJKCvzkJ2GkUpazIt3H+QIPAMHwqQ7tMKaHz1Qj+rJJCqljnf4p3A==",
+			"license": "MIT"
 		},
 		},
 		"node_modules/source-map-js": {
 		"node_modules/source-map-js": {
 			"version": "1.2.1",
 			"version": "1.2.1",

+ 5 - 2
package.json

@@ -76,6 +76,7 @@
 		"@xyflow/svelte": "^0.1.19",
 		"@xyflow/svelte": "^0.1.19",
 		"async": "^3.2.5",
 		"async": "^3.2.5",
 		"bits-ui": "^0.21.15",
 		"bits-ui": "^0.21.15",
+		"chart.js": "^4.5.0",
 		"codemirror": "^6.0.1",
 		"codemirror": "^6.0.1",
 		"codemirror-lang-elixir": "^4.0.0",
 		"codemirror-lang-elixir": "^4.0.0",
 		"codemirror-lang-hcl": "^0.1.0",
 		"codemirror-lang-hcl": "^0.1.0",
@@ -86,9 +87,10 @@
 		"file-saver": "^2.0.5",
 		"file-saver": "^2.0.5",
 		"focus-trap": "^7.6.4",
 		"focus-trap": "^7.6.4",
 		"fuse.js": "^7.0.0",
 		"fuse.js": "^7.0.0",
+		"heic2any": "^0.0.4",
 		"highlight.js": "^11.9.0",
 		"highlight.js": "^11.9.0",
 		"html-entities": "^2.5.3",
 		"html-entities": "^2.5.3",
-		"html2canvas-pro": "^1.5.8",
+		"html2canvas-pro": "^1.5.11",
 		"i18next": "^23.10.0",
 		"i18next": "^23.10.0",
 		"i18next-browser-languagedetector": "^7.2.0",
 		"i18next-browser-languagedetector": "^7.2.0",
 		"i18next-resources-to-backend": "^1.2.0",
 		"i18next-resources-to-backend": "^1.2.0",
@@ -97,6 +99,7 @@
 		"jspdf": "^3.0.0",
 		"jspdf": "^3.0.0",
 		"katex": "^0.16.22",
 		"katex": "^0.16.22",
 		"kokoro-js": "^1.1.1",
 		"kokoro-js": "^1.1.1",
+		"leaflet": "^1.9.4",
 		"marked": "^9.1.0",
 		"marked": "^9.1.0",
 		"mermaid": "^11.6.0",
 		"mermaid": "^11.6.0",
 		"paneforge": "^0.0.6",
 		"paneforge": "^0.0.6",
@@ -114,7 +117,7 @@
 		"prosemirror-view": "^1.34.3",
 		"prosemirror-view": "^1.34.3",
 		"pyodide": "^0.27.3",
 		"pyodide": "^0.27.3",
 		"socket.io-client": "^4.2.0",
 		"socket.io-client": "^4.2.0",
-		"sortablejs": "^1.15.2",
+		"sortablejs": "^1.15.6",
 		"svelte-sonner": "^0.3.19",
 		"svelte-sonner": "^0.3.19",
 		"tippy.js": "^6.3.7",
 		"tippy.js": "^6.3.7",
 		"turndown": "^7.2.0",
 		"turndown": "^7.2.0",

+ 4 - 4
pyproject.toml

@@ -8,7 +8,7 @@ license = { file = "LICENSE" }
 dependencies = [
 dependencies = [
     "fastapi==0.115.7",
     "fastapi==0.115.7",
     "uvicorn[standard]==0.34.2",
     "uvicorn[standard]==0.34.2",
-    "pydantic==2.10.6",
+    "pydantic==2.11.7",
     "python-multipart==0.0.20",
     "python-multipart==0.0.20",
 
 
     "python-socketio==5.13.0",
     "python-socketio==5.13.0",
@@ -50,8 +50,8 @@ dependencies = [
     "google-generativeai==0.8.5",
     "google-generativeai==0.8.5",
     "tiktoken",
     "tiktoken",
 
 
-    "langchain==0.3.24",
-    "langchain-community==0.3.23",
+    "langchain==0.3.26",
+    "langchain-community==0.3.26",
 
 
     "fake-useragent==2.1.0",
     "fake-useragent==2.1.0",
     "chromadb==0.6.3",
     "chromadb==0.6.3",
@@ -138,7 +138,7 @@ requires-python = ">= 3.11, < 3.13.0a1"
 dynamic = ["version"]
 dynamic = ["version"]
 classifiers = [
 classifiers = [
     "Development Status :: 4 - Beta",
     "Development Status :: 4 - Beta",
-    "License :: OSI Approved :: MIT License",
+    "License :: Other/Proprietary License",
     "Programming Language :: Python :: 3",
     "Programming Language :: Python :: 3",
     "Programming Language :: Python :: 3.11",
     "Programming Language :: Python :: 3.11",
     "Programming Language :: Python :: 3.12",
     "Programming Language :: Python :: 3.12",

+ 2 - 2
scripts/prepare-pyodide.js

@@ -74,8 +74,8 @@ async function downloadPackages() {
 			console.log('Pyodide version mismatch, removing static/pyodide directory');
 			console.log('Pyodide version mismatch, removing static/pyodide directory');
 			await rmdir('static/pyodide', { recursive: true });
 			await rmdir('static/pyodide', { recursive: true });
 		}
 		}
-	} catch (e) {
-		console.log('Pyodide package not found, proceeding with download.');
+	} catch (err) {
+		console.log('Pyodide package not found, proceeding with download.', err);
 	}
 	}
 
 
 	try {
 	try {

+ 10 - 32
src/app.html

@@ -33,6 +33,7 @@
 		</script>
 		</script>
 
 
 		<script>
 		<script>
+
 			// On page load or when changing themes, best to add inline in `head` to avoid FOUC
 			// On page load or when changing themes, best to add inline in `head` to avoid FOUC
 			(() => {
 			(() => {
 				const metaThemeColorTag = document.querySelector('meta[name="theme-color"]');
 				const metaThemeColorTag = document.querySelector('meta[name="theme-color"]');
@@ -77,28 +78,17 @@
 						}
 						}
 					}
 					}
 				});
 				});
+				const isDarkMode = document.documentElement.classList.contains('dark');
 
 
-				function setSplashImage() {
-					const logo = document.getElementById('logo');
-					const isDarkMode = document.documentElement.classList.contains('dark');
-
-					if (isDarkMode) {
-						const darkImage = new Image();
-						darkImage.src = '/static/splash-dark.png';
-
-						darkImage.onload = () => {
-							logo.src = '/static/splash-dark.png';
-							logo.style.filter = ''; // Ensure no inversion is applied if splash-dark.png exists
-						};
-
-						darkImage.onerror = () => {
-							logo.style.filter = 'invert(1)'; // Invert image if splash-dark.png is missing
-						};
-					}
-				}
+				const logo = document.createElement('img');
+				logo.id = 'logo';
+				logo.style = "position: absolute; width: auto; height: 6rem; top: 44%; left: 50%; transform: translateX(-50%); display:block;";
+				logo.src = isDarkMode ? '/static/splash-dark.png' : '/static/splash.png';
 
 
-				// Runs after classes are assigned
-				window.onload = setSplashImage;
+				document.addEventListener('DOMContentLoaded', function() {
+					const splash = document.getElementById('splash-screen');
+					if (splash) splash.prepend(logo);
+				});
 			})();
 			})();
 		</script>
 		</script>
 
 
@@ -120,18 +110,6 @@
 				}
 				}
 			</style>
 			</style>
 
 
-			<img
-				id="logo"
-				style="
-					position: absolute;
-					width: auto;
-					height: 6rem;
-					top: 44%;
-					left: 50%;
-					transform: translateX(-50%);
-				"
-				src="/static/splash.png"
-			/>
 
 
 			<div
 			<div
 				style="
 				style="

+ 2 - 0
src/lib/apis/auths/index.ts

@@ -347,6 +347,8 @@ export const userSignOut = async () => {
 	if (error) {
 	if (error) {
 		throw error;
 		throw error;
 	}
 	}
+
+	sessionStorage.clear();
 	return res;
 	return res;
 };
 };
 
 

+ 6 - 2
src/lib/apis/chats/index.ts

@@ -37,7 +37,9 @@ export const importChat = async (
 	chat: object,
 	chat: object,
 	meta: object | null,
 	meta: object | null,
 	pinned?: boolean,
 	pinned?: boolean,
-	folderId?: string | null
+	folderId?: string | null,
+	createdAt: number | null = null,
+	updatedAt: number | null = null
 ) => {
 ) => {
 	let error = null;
 	let error = null;
 
 
@@ -52,7 +54,9 @@ export const importChat = async (
 			chat: chat,
 			chat: chat,
 			meta: meta ?? {},
 			meta: meta ?? {},
 			pinned: pinned,
 			pinned: pinned,
-			folder_id: folderId
+			folder_id: folderId,
+			created_at: createdAt ?? null,
+			updated_at: updatedAt ?? null
 		})
 		})
 	})
 	})
 		.then(async (res) => {
 		.then(async (res) => {

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

@@ -58,10 +58,10 @@ export const exportConfig = async (token: string) => {
 	return res;
 	return res;
 };
 };
 
 
-export const getDirectConnectionsConfig = async (token: string) => {
+export const getConnectionsConfig = async (token: string) => {
 	let error = null;
 	let error = null;
 
 
-	const res = await fetch(`${WEBUI_API_BASE_URL}/configs/direct_connections`, {
+	const res = await fetch(`${WEBUI_API_BASE_URL}/configs/connections`, {
 		method: 'GET',
 		method: 'GET',
 		headers: {
 		headers: {
 			'Content-Type': 'application/json',
 			'Content-Type': 'application/json',
@@ -85,10 +85,10 @@ export const getDirectConnectionsConfig = async (token: string) => {
 	return res;
 	return res;
 };
 };
 
 
-export const setDirectConnectionsConfig = async (token: string, config: object) => {
+export const setConnectionsConfig = async (token: string, config: object) => {
 	let error = null;
 	let error = null;
 
 
-	const res = await fetch(`${WEBUI_API_BASE_URL}/configs/direct_connections`, {
+	const res = await fetch(`${WEBUI_API_BASE_URL}/configs/connections`, {
 		method: 'POST',
 		method: 'POST',
 		headers: {
 		headers: {
 			'Content-Type': 'application/json',
 			'Content-Type': 'application/json',

+ 18 - 8
src/lib/apis/index.ts

@@ -8,17 +8,26 @@ import { toast } from 'svelte-sonner';
 export const getModels = async (
 export const getModels = async (
 	token: string = '',
 	token: string = '',
 	connections: object | null = null,
 	connections: object | null = null,
-	base: boolean = false
+	base: boolean = false,
+	refresh: boolean = false
 ) => {
 ) => {
+	const searchParams = new URLSearchParams();
+	if (refresh) {
+		searchParams.append('refresh', 'true');
+	}
+
 	let error = null;
 	let error = null;
-	const res = await fetch(`${WEBUI_BASE_URL}/api/models${base ? '/base' : ''}`, {
-		method: 'GET',
-		headers: {
-			Accept: 'application/json',
-			'Content-Type': 'application/json',
-			...(token && { authorization: `Bearer ${token}` })
+	const res = await fetch(
+		`${WEBUI_BASE_URL}/api/models${base ? '/base' : ''}?${searchParams.toString()}`,
+		{
+			method: 'GET',
+			headers: {
+				Accept: 'application/json',
+				'Content-Type': 'application/json',
+				...(token && { authorization: `Bearer ${token}` })
+			}
 		}
 		}
-	})
+	)
 		.then(async (res) => {
 		.then(async (res) => {
 			if (!res.ok) throw await res.json();
 			if (!res.ok) throw await res.json();
 			return res.json();
 			return res.json();
@@ -1587,6 +1596,7 @@ export interface ModelConfig {
 }
 }
 
 
 export interface ModelMeta {
 export interface ModelMeta {
+	toolIds: never[];
 	description?: string;
 	description?: string;
 	capabilities?: object;
 	capabilities?: object;
 	profile_image_url?: string;
 	profile_image_url?: string;

+ 1 - 0
src/lib/apis/users/index.ts

@@ -403,6 +403,7 @@ export const deleteUserById = async (token: string, userId: string) => {
 };
 };
 
 
 type UserUpdateForm = {
 type UserUpdateForm = {
+	role: string;
 	profile_image_url: string;
 	profile_image_url: string;
 	email: string;
 	email: string;
 	name: string;
 	name: string;

+ 4 - 34
src/lib/components/AddConnectionModal.svelte

@@ -15,6 +15,8 @@
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
 	import Switch from '$lib/components/common/Switch.svelte';
 	import Switch from '$lib/components/common/Switch.svelte';
 	import Tags from './common/Tags.svelte';
 	import Tags from './common/Tags.svelte';
+	import Spinner from '$lib/components/common/Spinner.svelte';
+	import XMark from '$lib/components/icons/XMark.svelte';
 
 
 	export let onSubmit: Function = () => {};
 	export let onSubmit: Function = () => {};
 	export let onDelete: Function = () => {};
 	export let onDelete: Function = () => {};
@@ -208,17 +210,7 @@
 					show = false;
 					show = false;
 				}}
 				}}
 			>
 			>
-				<svg
-					xmlns="http://www.w3.org/2000/svg"
-					viewBox="0 0 20 20"
-					fill="currentColor"
-					aria-hidden="true"
-					class="w-5 h-5"
-				>
-					<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>
+				<XMark className={'size-5'} />
 			</button>
 			</button>
 		</div>
 		</div>
 
 
@@ -524,29 +516,7 @@
 
 
 							{#if loading}
 							{#if loading}
 								<div class="ml-2 self-center">
 								<div class="ml-2 self-center">
-									<svg
-										class=" w-4 h-4"
-										viewBox="0 0 24 24"
-										fill="currentColor"
-										xmlns="http://www.w3.org/2000/svg"
-										><style>
-											.spinner_ajPY {
-												transform-origin: center;
-												animation: spinner_AtaB 0.75s infinite linear;
-											}
-											@keyframes spinner_AtaB {
-												100% {
-													transform: rotate(360deg);
-												}
-											}
-										</style><path
-											d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
-											opacity=".25"
-										/><path
-											d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
-											class="spinner_ajPY"
-										/></svg
-									>
+									<Spinner />
 								</div>
 								</div>
 							{/if}
 							{/if}
 						</button>
 						</button>

+ 49 - 47
src/lib/components/AddServerModal.svelte

@@ -3,6 +3,7 @@
 	import { getContext, onMount } from 'svelte';
 	import { getContext, onMount } from 'svelte';
 	const i18n = getContext('i18n');
 	const i18n = getContext('i18n');
 
 
+	import { settings } from '$lib/stores';
 	import Modal from '$lib/components/common/Modal.svelte';
 	import Modal from '$lib/components/common/Modal.svelte';
 	import Plus from '$lib/components/icons/Plus.svelte';
 	import Plus from '$lib/components/icons/Plus.svelte';
 	import Minus from '$lib/components/icons/Minus.svelte';
 	import Minus from '$lib/components/icons/Minus.svelte';
@@ -14,6 +15,8 @@
 	import { getToolServerData } from '$lib/apis';
 	import { getToolServerData } from '$lib/apis';
 	import { verifyToolServerConnection } from '$lib/apis/configs';
 	import { verifyToolServerConnection } from '$lib/apis/configs';
 	import AccessControl from './workspace/common/AccessControl.svelte';
 	import AccessControl from './workspace/common/AccessControl.svelte';
+	import Spinner from '$lib/components/common/Spinner.svelte';
+	import XMark from '$lib/components/icons/XMark.svelte';
 
 
 	export let onSubmit: Function = () => {};
 	export let onSubmit: Function = () => {};
 	export let onDelete: Function = () => {};
 	export let onDelete: Function = () => {};
@@ -153,29 +156,21 @@
 <Modal size="sm" bind:show>
 <Modal size="sm" bind:show>
 	<div>
 	<div>
 		<div class=" flex justify-between dark:text-gray-100 px-5 pt-4 pb-2">
 		<div class=" flex justify-between dark:text-gray-100 px-5 pt-4 pb-2">
-			<div class=" text-lg font-medium self-center font-primary">
+			<h1 class=" text-lg font-medium self-center font-primary">
 				{#if edit}
 				{#if edit}
 					{$i18n.t('Edit Connection')}
 					{$i18n.t('Edit Connection')}
 				{:else}
 				{:else}
 					{$i18n.t('Add Connection')}
 					{$i18n.t('Add Connection')}
 				{/if}
 				{/if}
-			</div>
+			</h1>
 			<button
 			<button
 				class="self-center"
 				class="self-center"
+				aria-label={$i18n.t('Close Configure Connection Modal')}
 				on:click={() => {
 				on:click={() => {
 					show = false;
 					show = false;
 				}}
 				}}
 			>
 			>
-				<svg
-					xmlns="http://www.w3.org/2000/svg"
-					viewBox="0 0 20 20"
-					fill="currentColor"
-					class="w-5 h-5"
-				>
-					<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>
+				<XMark className={'size-5'} />
 			</button>
 			</button>
 		</div>
 		</div>
 
 
@@ -192,12 +187,17 @@
 						<div class="flex gap-2">
 						<div class="flex gap-2">
 							<div class="flex flex-col w-full">
 							<div class="flex flex-col w-full">
 								<div class="flex justify-between mb-0.5">
 								<div class="flex justify-between mb-0.5">
-									<div class=" text-xs text-gray-500">{$i18n.t('URL')}</div>
+									<label
+										for="api-base-url"
+										class={`text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
+										>{$i18n.t('URL')}</label
+									>
 								</div>
 								</div>
 
 
 								<div class="flex flex-1 items-center">
 								<div class="flex flex-1 items-center">
 									<input
 									<input
-										class="w-full flex-1 text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
+										id="api-base-url"
+										class={`w-full flex-1 text-sm bg-transparent ${($settings?.highContrastMode ?? false) ? 'placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
 										type="text"
 										type="text"
 										bind:value={url}
 										bind:value={url}
 										placeholder={$i18n.t('API Base URL')}
 										placeholder={$i18n.t('API Base URL')}
@@ -214,6 +214,7 @@
 											on:click={() => {
 											on:click={() => {
 												verifyHandler();
 												verifyHandler();
 											}}
 											}}
+											aria-label={$i18n.t('Verify Connection')}
 											type="button"
 											type="button"
 										>
 										>
 											<svg
 											<svg
@@ -221,6 +222,7 @@
 												viewBox="0 0 20 20"
 												viewBox="0 0 20 20"
 												fill="currentColor"
 												fill="currentColor"
 												class="w-4 h-4"
 												class="w-4 h-4"
+												aria-hidden="true"
 											>
 											>
 												<path
 												<path
 													fill-rule="evenodd"
 													fill-rule="evenodd"
@@ -237,9 +239,13 @@
 								</div>
 								</div>
 
 
 								<div class="flex-1 flex items-center">
 								<div class="flex-1 flex items-center">
+									<label for="url-or-path" class="sr-only"
+										>{$i18n.t('openapi.json URL or Path')}</label
+									>
 									<input
 									<input
-										class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
+										class={`w-full text-sm bg-transparent ${($settings?.highContrastMode ?? false) ? 'placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
 										type="text"
 										type="text"
+										id="url-or-path"
 										bind:value={path}
 										bind:value={path}
 										placeholder={$i18n.t('openapi.json URL or Path')}
 										placeholder={$i18n.t('openapi.json URL or Path')}
 										autocomplete="off"
 										autocomplete="off"
@@ -249,7 +255,9 @@
 							</div>
 							</div>
 						</div>
 						</div>
 
 
-						<div class="text-xs text-gray-500 mt-1">
+						<div
+							class={`text-xs mt-1 ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
+						>
 							{$i18n.t(`WebUI will make requests to "{{url}}"`, {
 							{$i18n.t(`WebUI will make requests to "{{url}}"`, {
 								url: path.includes('://') ? path : `${url}${path.startsWith('/') ? '' : '/'}${path}`
 								url: path.includes('://') ? path : `${url}${path.startsWith('/') ? '' : '/'}${path}`
 							})}
 							})}
@@ -257,12 +265,17 @@
 
 
 						<div class="flex gap-2 mt-2">
 						<div class="flex gap-2 mt-2">
 							<div class="flex flex-col w-full">
 							<div class="flex flex-col w-full">
-								<div class="  text-xs text-gray-500">{$i18n.t('Auth')}</div>
+								<label
+									for="select-bearer-or-session"
+									class={`text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
+									>{$i18n.t('Auth')}</label
+								>
 
 
 								<div class="flex gap-2">
 								<div class="flex gap-2">
 									<div class="flex-shrink-0 self-start">
 									<div class="flex-shrink-0 self-start">
 										<select
 										<select
-											class="w-full text-sm bg-transparent dark:bg-gray-900 placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden pr-5"
+											id="select-bearer-or-session"
+											class={`w-full text-sm bg-transparent pr-5 ${($settings?.highContrastMode ?? false) ? 'placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
 											bind:value={auth_type}
 											bind:value={auth_type}
 										>
 										>
 											<option value="bearer">Bearer</option>
 											<option value="bearer">Bearer</option>
@@ -273,13 +286,14 @@
 									<div class="flex flex-1 items-center">
 									<div class="flex flex-1 items-center">
 										{#if auth_type === 'bearer'}
 										{#if auth_type === 'bearer'}
 											<SensitiveInput
 											<SensitiveInput
-												className="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
 												bind:value={key}
 												bind:value={key}
 												placeholder={$i18n.t('API Key')}
 												placeholder={$i18n.t('API Key')}
 												required={false}
 												required={false}
 											/>
 											/>
 										{:else if auth_type === 'session'}
 										{:else if auth_type === 'session'}
-											<div class="text-xs text-gray-500 self-center translate-y-[1px]">
+											<div
+												class={`text-xs self-center translate-y-[1px] ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
+											>
 												{$i18n.t('Forwards system user session credentials to authenticate')}
 												{$i18n.t('Forwards system user session credentials to authenticate')}
 											</div>
 											</div>
 										{/if}
 										{/if}
@@ -293,11 +307,16 @@
 
 
 							<div class="flex gap-2">
 							<div class="flex gap-2">
 								<div class="flex flex-col w-full">
 								<div class="flex flex-col w-full">
-									<div class=" mb-0.5 text-xs text-gray-500">{$i18n.t('Name')}</div>
+									<label
+										for="enter-name"
+										class={`mb-0.5 text-xs" ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
+										>{$i18n.t('Name')}</label
+									>
 
 
 									<div class="flex-1">
 									<div class="flex-1">
 										<input
 										<input
-											class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
+											id="enter-name"
+											class={`w-full text-sm bg-transparent ${($settings?.highContrastMode ?? false) ? 'placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
 											type="text"
 											type="text"
 											bind:value={name}
 											bind:value={name}
 											placeholder={$i18n.t('Enter name')}
 											placeholder={$i18n.t('Enter name')}
@@ -309,11 +328,16 @@
 							</div>
 							</div>
 
 
 							<div class="flex flex-col w-full mt-2">
 							<div class="flex flex-col w-full mt-2">
-								<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Description')}</div>
+								<label
+									for="description"
+									class={`mb-1 text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100 placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700 text-gray-500'}`}
+									>{$i18n.t('Description')}</label
+								>
 
 
 								<div class="flex-1">
 								<div class="flex-1">
 									<input
 									<input
-										class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
+										id="description"
+										class={`w-full text-sm bg-transparent ${($settings?.highContrastMode ?? false) ? 'placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
 										type="text"
 										type="text"
 										bind:value={description}
 										bind:value={description}
 										placeholder={$i18n.t('Enter description')}
 										placeholder={$i18n.t('Enter description')}
@@ -357,29 +381,7 @@
 
 
 							{#if loading}
 							{#if loading}
 								<div class="ml-2 self-center">
 								<div class="ml-2 self-center">
-									<svg
-										class=" w-4 h-4"
-										viewBox="0 0 24 24"
-										fill="currentColor"
-										xmlns="http://www.w3.org/2000/svg"
-										><style>
-											.spinner_ajPY {
-												transform-origin: center;
-												animation: spinner_AtaB 0.75s infinite linear;
-											}
-											@keyframes spinner_AtaB {
-												100% {
-													transform: rotate(360deg);
-												}
-											}
-										</style><path
-											d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
-											opacity=".25"
-										/><path
-											d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
-											class="spinner_ajPY"
-										/></svg
-									>
+									<Spinner />
 								</div>
 								</div>
 							{/if}
 							{/if}
 						</button>
 						</button>

+ 4 - 10
src/lib/components/ChangelogModal.svelte

@@ -9,6 +9,7 @@
 
 
 	import Modal from './common/Modal.svelte';
 	import Modal from './common/Modal.svelte';
 	import { updateUserSettings } from '$lib/apis/users';
 	import { updateUserSettings } from '$lib/apis/users';
+	import XMark from '$lib/components/icons/XMark.svelte';
 
 
 	const i18n = getContext('i18n');
 	const i18n = getContext('i18n');
 
 
@@ -36,18 +37,11 @@
 					localStorage.version = $config.version;
 					localStorage.version = $config.version;
 					show = false;
 					show = false;
 				}}
 				}}
+				aria-label={$i18n.t('Close')}
 			>
 			>
-				<svg
-					xmlns="http://www.w3.org/2000/svg"
-					viewBox="0 0 20 20"
-					fill="currentColor"
-					class="w-5 h-5"
-				>
+				<XMark className={'size-5'}>
 					<p class="sr-only">{$i18n.t('Close')}</p>
 					<p class="sr-only">{$i18n.t('Close')}</p>
-					<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>
+				</XMark>
 			</button>
 			</button>
 		</div>
 		</div>
 		<div class="flex items-center mt-1">
 		<div class="flex items-center mt-1">

+ 4 - 33
src/lib/components/ImportModal.svelte

@@ -3,7 +3,9 @@
 	import { getContext, onMount } from 'svelte';
 	import { getContext, onMount } from 'svelte';
 	const i18n = getContext('i18n');
 	const i18n = getContext('i18n');
 
 
+	import Spinner from '$lib/components/common/Spinner.svelte';
 	import Modal from '$lib/components/common/Modal.svelte';
 	import Modal from '$lib/components/common/Modal.svelte';
+	import XMark from '$lib/components/icons/XMark.svelte';
 	import { extractFrontmatter } from '$lib/utils';
 	import { extractFrontmatter } from '$lib/utils';
 
 
 	export let show = false;
 	export let show = false;
@@ -69,16 +71,7 @@
 					show = false;
 					show = false;
 				}}
 				}}
 			>
 			>
-				<svg
-					xmlns="http://www.w3.org/2000/svg"
-					viewBox="0 0 20 20"
-					fill="currentColor"
-					class="w-5 h-5"
-				>
-					<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>
+				<XMark className={'size-5'} />
 			</button>
 			</button>
 		</div>
 		</div>
 
 
@@ -120,29 +113,7 @@
 
 
 							{#if loading}
 							{#if loading}
 								<div class="ml-2 self-center">
 								<div class="ml-2 self-center">
-									<svg
-										class=" w-4 h-4"
-										viewBox="0 0 24 24"
-										fill="currentColor"
-										xmlns="http://www.w3.org/2000/svg"
-										><style>
-											.spinner_ajPY {
-												transform-origin: center;
-												animation: spinner_AtaB 0.75s infinite linear;
-											}
-											@keyframes spinner_AtaB {
-												100% {
-													transform: rotate(360deg);
-												}
-											}
-										</style><path
-											d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
-											opacity=".25"
-										/><path
-											d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
-											class="spinner_ajPY"
-										/></svg
-									>
+									<Spinner />
 								</div>
 								</div>
 							{/if}
 							{/if}
 						</button>
 						</button>

+ 95 - 38
src/lib/components/admin/Evaluations/FeedbackModal.svelte

@@ -2,16 +2,42 @@
 	import Modal from '$lib/components/common/Modal.svelte';
 	import Modal from '$lib/components/common/Modal.svelte';
 	import { getContext } from 'svelte';
 	import { getContext } from 'svelte';
 	const i18n = getContext('i18n');
 	const i18n = getContext('i18n');
+	import XMark from '$lib/components/icons/XMark.svelte';
+	import { getFeedbackById } from '$lib/apis/evaluations';
+	import { toast } from 'svelte-sonner';
+	import Spinner from '$lib/components/common/Spinner.svelte';
 
 
 	export let show = false;
 	export let show = false;
 	export let selectedFeedback = null;
 	export let selectedFeedback = null;
 
 
 	export let onClose: () => void = () => {};
 	export let onClose: () => void = () => {};
 
 
+	let loaded = false;
+
+	let feedbackData = null;
+
 	const close = () => {
 	const close = () => {
 		show = false;
 		show = false;
 		onClose();
 		onClose();
 	};
 	};
+
+	const init = async () => {
+		loaded = false;
+		feedbackData = null;
+		if (selectedFeedback) {
+			feedbackData = await getFeedbackById(localStorage.token, selectedFeedback.id).catch((err) => {
+				toast.error(err);
+				return null;
+			});
+
+			console.log('Feedback Data:', selectedFeedback, feedbackData);
+		}
+		loaded = true;
+	};
+
+	$: if (show) {
+		init();
+	}
 </script>
 </script>
 
 
 <Modal size="sm" bind:show>
 <Modal size="sm" bind:show>
@@ -22,58 +48,89 @@
 					{$i18n.t('Feedback Details')}
 					{$i18n.t('Feedback Details')}
 				</div>
 				</div>
 				<button class="self-center" on:click={close} aria-label="Close">
 				<button class="self-center" on:click={close} aria-label="Close">
-					<svg
-						xmlns="http://www.w3.org/2000/svg"
-						viewBox="0 0 20 20"
-						fill="currentColor"
-						class="w-5 h-5"
-					>
-						<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>
+					<XMark className={'size-5'} />
 				</button>
 				</button>
 			</div>
 			</div>
 
 
 			<div class="flex flex-col md:flex-row w-full px-5 pb-4 md:space-x-4 dark:text-gray-200">
 			<div class="flex flex-col md:flex-row w-full px-5 pb-4 md:space-x-4 dark:text-gray-200">
-				<div class="flex flex-col w-full">
-					<div class="flex flex-col w-full mb-2">
-						<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Rating')}</div>
+				{#if loaded}
+					<div class="flex flex-col w-full">
+						{#if feedbackData}
+							{@const messageId = feedbackData?.meta?.message_id}
+							{@const messages = feedbackData?.snapshot?.chat?.chat?.history.messages}
+
+							{#if messages[messages[messageId]?.parentId]}
+								<div class="flex flex-col w-full mb-2">
+									<div class="mb-1 text-xs text-gray-500">{$i18n.t('Prompt')}</div>
+
+									<div class="flex-1 text-xs whitespace-pre-line break-words">
+										<span>{messages[messages[messageId]?.parentId]?.content || '-'}</span>
+									</div>
+								</div>
+							{/if}
+
+							{#if messages[messageId]}
+								<div class="flex flex-col w-full mb-2">
+									<div class="mb-1 text-xs text-gray-500">{$i18n.t('Response')}</div>
+									<div
+										class="flex-1 text-xs whitespace-pre-line break-words max-h-32 overflow-y-auto"
+									>
+										<span>{messages[messageId]?.content || '-'}</span>
+									</div>
+								</div>
+							{/if}
+						{/if}
+
+						<div class="flex flex-col w-full mb-2">
+							<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Rating')}</div>
 
 
-						<div class="flex-1">
-							<span>{selectedFeedback?.data?.details?.rating ?? '-'}</span>
+							<div class="flex-1 text-xs">
+								<span>{selectedFeedback?.data?.details?.rating ?? '-'}</span>
+							</div>
 						</div>
 						</div>
-					</div>
-					<div class="flex flex-col w-full mb-2">
-						<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Reason')}</div>
+						<div class="flex flex-col w-full mb-2">
+							<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Reason')}</div>
 
 
-						<div class="flex-1">
-							<span>{selectedFeedback?.data?.reason || '-'}</span>
+							<div class="flex-1 text-xs">
+								<span>{selectedFeedback?.data?.reason || '-'}</span>
+							</div>
+						</div>
+
+						<div class="flex flex-col w-full mb-2">
+							<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Comment')}</div>
+
+							<div class="flex-1 text-xs">
+								<span>{selectedFeedback?.data?.comment || '-'}</span>
+							</div>
 						</div>
 						</div>
-					</div>
 
 
-					<div class="mb-2">
 						{#if selectedFeedback?.data?.tags && selectedFeedback?.data?.tags.length}
 						{#if selectedFeedback?.data?.tags && selectedFeedback?.data?.tags.length}
-							<div class="flex flex-wrap gap-1 mt-1">
-								{#each selectedFeedback?.data?.tags as tag}
-									<span class="px-2 py-0.5 rounded bg-gray-100 dark:bg-gray-800 text-xs">{tag}</span
-									>
-								{/each}
+							<div class="mb-2 -mx-1">
+								<div class="flex flex-wrap gap-1 mt-1">
+									{#each selectedFeedback?.data?.tags as tag}
+										<span class="px-2 py-0.5 rounded-full bg-gray-100 dark:bg-gray-850 text-[9px]"
+											>{tag}</span
+										>
+									{/each}
+								</div>
 							</div>
 							</div>
-						{:else}
-							<span>-</span>
 						{/if}
 						{/if}
+
+						<div class="flex justify-end pt-2">
+							<button
+								class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
+								type="button"
+								on:click={close}
+							>
+								{$i18n.t('Close')}
+							</button>
+						</div>
 					</div>
 					</div>
-					<div class="flex justify-end pt-3">
-						<button
-							class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
-							type="button"
-							on:click={close}
-						>
-							{$i18n.t('Close')}
-						</button>
+				{:else}
+					<div class="flex items-center justify-center w-full h-32">
+						<Spinner className={'size-5'} />
 					</div>
 					</div>
-				</div>
+				{/if}
 			</div>
 			</div>
 		</div>
 		</div>
 	{/if}
 	{/if}

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

@@ -305,7 +305,7 @@
 			<tbody class="">
 			<tbody class="">
 				{#each paginatedFeedbacks as feedback (feedback.id)}
 				{#each paginatedFeedbacks as feedback (feedback.id)}
 					<tr
 					<tr
-						class="bg-white dark:bg-gray-900 dark:border-gray-850 text-xs cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 transition"
+						class="bg-white dark:bg-gray-900 dark:border-gray-850 text-xs cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-850/50 transition"
 						on:click={() => openFeedbackModal(feedback)}
 						on:click={() => openFeedbackModal(feedback)}
 					>
 					>
 						<td class=" py-0.5 text-right font-semibold">
 						<td class=" py-0.5 text-right font-semibold">
@@ -369,7 +369,7 @@
 							{dayjs(feedback.updated_at * 1000).fromNow()}
 							{dayjs(feedback.updated_at * 1000).fromNow()}
 						</td>
 						</td>
 
 
-						<td class=" px-3 py-1 text-right font-semibold">
+						<td class=" px-3 py-1 text-right font-semibold" on:click={(e) => e.stopPropagation()}>
 							<FeedbackMenu
 							<FeedbackMenu
 								on:delete={(e) => {
 								on:delete={(e) => {
 									deleteFeedbackHandler(feedback.id);
 									deleteFeedbackHandler(feedback.id);

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

@@ -11,7 +11,7 @@
 
 
 	import Spinner from '$lib/components/common/Spinner.svelte';
 	import Spinner from '$lib/components/common/Spinner.svelte';
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
-	import MagnifyingGlass from '$lib/components/icons/MagnifyingGlass.svelte';
+	import Search from '$lib/components/icons/Search.svelte';
 
 
 	import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
 	import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
 	import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
 	import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
@@ -77,7 +77,7 @@
 	let showLeaderboardModal = false;
 	let showLeaderboardModal = false;
 	let selectedModel = null;
 	let selectedModel = null;
 
 
-	const openFeedbackModal = (model) => {
+	const openLeaderboardModelModal = (model) => {
 		showLeaderboardModal = true;
 		showLeaderboardModal = true;
 		selectedModel = model;
 		selectedModel = model;
 	};
 	};
@@ -350,7 +350,7 @@
 		<Tooltip content={$i18n.t('Re-rank models by topic similarity')}>
 		<Tooltip content={$i18n.t('Re-rank models by topic similarity')}>
 			<div class="flex flex-1">
 			<div class="flex flex-1">
 				<div class=" self-center ml-1 mr-3">
 				<div class=" self-center ml-1 mr-3">
-					<MagnifyingGlass className="size-3" />
+					<Search className="size-3" />
 				</div>
 				</div>
 				<input
 				<input
 					class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-hidden bg-transparent"
 					class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-hidden bg-transparent"
@@ -371,7 +371,7 @@
 	{#if loadingLeaderboard}
 	{#if loadingLeaderboard}
 		<div class=" absolute top-0 bottom-0 left-0 right-0 flex">
 		<div class=" absolute top-0 bottom-0 left-0 right-0 flex">
 			<div class="m-auto">
 			<div class="m-auto">
-				<Spinner />
+				<Spinner className="size-5" />
 			</div>
 			</div>
 		</div>
 		</div>
 	{/if}
 	{/if}
@@ -504,8 +504,8 @@
 			<tbody class="">
 			<tbody class="">
 				{#each sortedModels as model, modelIdx (model.id)}
 				{#each sortedModels as model, modelIdx (model.id)}
 					<tr
 					<tr
-						class="bg-white dark:bg-gray-900 dark:border-gray-850 text-xs group cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 transition"
-						on:click={() => openFeedbackModal(model)}
+						class="bg-white dark:bg-gray-900 dark:border-gray-850 text-xs group cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-850/50 transition"
+						on:click={() => openLeaderboardModelModal(model)}
 					>
 					>
 						<td class="px-3 py-1.5 text-left font-medium text-gray-900 dark:text-white w-fit">
 						<td class="px-3 py-1.5 text-left font-medium text-gray-900 dark:text-white w-fit">
 							<div class=" line-clamp-1">
 							<div class=" line-clamp-1">

+ 6 - 14
src/lib/components/admin/Evaluations/LeaderboardModal.svelte

@@ -6,6 +6,7 @@
 	export let feedbacks = [];
 	export let feedbacks = [];
 	export let onClose: () => void = () => {};
 	export let onClose: () => void = () => {};
 	const i18n = getContext('i18n');
 	const i18n = getContext('i18n');
+	import XMark from '$lib/components/icons/XMark.svelte';
 
 
 	const close = () => {
 	const close = () => {
 		show = false;
 		show = false;
@@ -37,25 +38,16 @@
 				{model.name}
 				{model.name}
 			</div>
 			</div>
 			<button class="self-center" on:click={close} aria-label="Close">
 			<button class="self-center" on:click={close} aria-label="Close">
-				<svg
-					xmlns="http://www.w3.org/2000/svg"
-					viewBox="0 0 20 20"
-					fill="currentColor"
-					class="w-5 h-5"
-				>
-					<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>
+				<XMark className={'size-5'} />
 			</button>
 			</button>
 		</div>
 		</div>
 		<div class="px-5 pb-4 dark:text-gray-200">
 		<div class="px-5 pb-4 dark:text-gray-200">
 			<div class="mb-2">
 			<div class="mb-2">
 				{#if topTags.length}
 				{#if topTags.length}
-					<div class="flex flex-wrap gap-1 mt-1">
+					<div class="flex flex-wrap gap-1 mt-1 -mx-1">
 						{#each topTags as tagInfo}
 						{#each topTags as tagInfo}
-							<span class="px-2 py-0.5 rounded bg-gray-100 dark:bg-gray-800 text-xs">
-								{tagInfo.tag} <span class="text-gray-500">({tagInfo.count})</span>
+							<span class="px-2 py-0.5 rounded-full bg-gray-100 dark:bg-gray-850 text-xs">
+								{tagInfo.tag} <span class="text-gray-500 font-medium">{tagInfo.count}</span>
 							</span>
 							</span>
 						{/each}
 						{/each}
 					</div>
 					</div>
@@ -63,7 +55,7 @@
 					<span>-</span>
 					<span>-</span>
 				{/if}
 				{/if}
 			</div>
 			</div>
-			<div class="flex justify-end pt-3">
+			<div class="flex justify-end pt-2">
 				<button
 				<button
 					class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
 					class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
 					type="button"
 					type="button"

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

@@ -135,7 +135,9 @@
 			models.set(
 			models.set(
 				await getModels(
 				await getModels(
 					localStorage.token,
 					localStorage.token,
-					$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
+					$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null),
+					false,
+					true
 				)
 				)
 			);
 			);
 		}
 		}
@@ -161,7 +163,9 @@
 			models.set(
 			models.set(
 				await getModels(
 				await getModels(
 					localStorage.token,
 					localStorage.token,
-					$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
+					$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null),
+					false,
+					true
 				)
 				)
 			);
 			);
 		}
 		}
@@ -413,7 +417,9 @@
 									await getModels(
 									await getModels(
 										localStorage.token,
 										localStorage.token,
 										$config?.features?.enable_direct_connections &&
 										$config?.features?.enable_direct_connections &&
-											($settings?.directConnections ?? null)
+											($settings?.directConnections ?? null),
+										false,
+										true
 									)
 									)
 								);
 								);
 							}}
 							}}
@@ -559,7 +565,9 @@
 		models.set(
 		models.set(
 			await getModels(
 			await getModels(
 				localStorage.token,
 				localStorage.token,
-				$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
+				$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null),
+				false,
+				true
 			)
 			)
 		);
 		);
 	}}
 	}}
@@ -585,7 +593,9 @@
 			models.set(
 			models.set(
 				await getModels(
 				await getModels(
 					localStorage.token,
 					localStorage.token,
-					$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
+					$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null),
+					false,
+					true
 				)
 				)
 			);
 			);
 		};
 		};

+ 2 - 27
src/lib/components/admin/Settings/Audio.svelte

@@ -12,6 +12,7 @@
 	} from '$lib/apis/audio';
 	} from '$lib/apis/audio';
 	import { config, settings } from '$lib/stores';
 	import { config, settings } from '$lib/stores';
 
 
+	import Spinner from '$lib/components/common/Spinner.svelte';
 	import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
 	import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
 
 
 	import { TTS_RESPONSE_SPLIT } from '$lib/types';
 	import { TTS_RESPONSE_SPLIT } from '$lib/types';
@@ -373,33 +374,7 @@
 							>
 							>
 								{#if STT_WHISPER_MODEL_LOADING}
 								{#if STT_WHISPER_MODEL_LOADING}
 									<div class="self-center">
 									<div class="self-center">
-										<svg
-											class=" w-4 h-4"
-											viewBox="0 0 24 24"
-											fill="currentColor"
-											xmlns="http://www.w3.org/2000/svg"
-										>
-											<style>
-												.spinner_ajPY {
-													transform-origin: center;
-													animation: spinner_AtaB 0.75s infinite linear;
-												}
-
-												@keyframes spinner_AtaB {
-													100% {
-														transform: rotate(360deg);
-													}
-												}
-											</style>
-											<path
-												d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
-												opacity=".25"
-											/>
-											<path
-												d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
-												class="spinner_ajPY"
-											/>
-										</svg>
+										<Spinner />
 									</div>
 									</div>
 								{:else}
 								{:else}
 									<svg
 									<svg

+ 160 - 137
src/lib/components/admin/Settings/Connections.svelte

@@ -7,7 +7,7 @@
 	import { getOllamaConfig, updateOllamaConfig } from '$lib/apis/ollama';
 	import { getOllamaConfig, updateOllamaConfig } from '$lib/apis/ollama';
 	import { getOpenAIConfig, updateOpenAIConfig, getOpenAIModels } from '$lib/apis/openai';
 	import { getOpenAIConfig, updateOpenAIConfig, getOpenAIModels } from '$lib/apis/openai';
 	import { getModels as _getModels } from '$lib/apis';
 	import { getModels as _getModels } from '$lib/apis';
-	import { getDirectConnectionsConfig, setDirectConnectionsConfig } from '$lib/apis/configs';
+	import { getConnectionsConfig, setConnectionsConfig } from '$lib/apis/configs';
 
 
 	import { config, models, settings, user } from '$lib/stores';
 	import { config, models, settings, user } from '$lib/stores';
 
 
@@ -25,7 +25,9 @@
 	const getModels = async () => {
 	const getModels = async () => {
 		const models = await _getModels(
 		const models = await _getModels(
 			localStorage.token,
 			localStorage.token,
-			$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
+			$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null),
+			false,
+			true
 		);
 		);
 		return models;
 		return models;
 	};
 	};
@@ -41,7 +43,7 @@
 	let ENABLE_OPENAI_API: null | boolean = null;
 	let ENABLE_OPENAI_API: null | boolean = null;
 	let ENABLE_OLLAMA_API: null | boolean = null;
 	let ENABLE_OLLAMA_API: null | boolean = null;
 
 
-	let directConnectionsConfig = null;
+	let connectionsConfig = null;
 
 
 	let pipelineUrls = {};
 	let pipelineUrls = {};
 	let showAddOpenAIConnectionModal = false;
 	let showAddOpenAIConnectionModal = false;
@@ -104,15 +106,13 @@
 		}
 		}
 	};
 	};
 
 
-	const updateDirectConnectionsHandler = async () => {
-		const res = await setDirectConnectionsConfig(localStorage.token, directConnectionsConfig).catch(
-			(error) => {
-				toast.error(`${error}`);
-			}
-		);
+	const updateConnectionsHandler = async () => {
+		const res = await setConnectionsConfig(localStorage.token, connectionsConfig).catch((error) => {
+			toast.error(`${error}`);
+		});
 
 
 		if (res) {
 		if (res) {
-			toast.success($i18n.t('Direct Connections settings updated'));
+			toast.success($i18n.t('Connections settings updated'));
 			await models.set(await getModels());
 			await models.set(await getModels());
 		}
 		}
 	};
 	};
@@ -148,7 +148,7 @@
 					openaiConfig = await getOpenAIConfig(localStorage.token);
 					openaiConfig = await getOpenAIConfig(localStorage.token);
 				})(),
 				})(),
 				(async () => {
 				(async () => {
-					directConnectionsConfig = await getDirectConnectionsConfig(localStorage.token);
+					connectionsConfig = await getConnectionsConfig(localStorage.token);
 				})()
 				})()
 			]);
 			]);
 
 
@@ -215,36 +215,103 @@
 
 
 <form class="flex flex-col h-full justify-between text-sm" on:submit|preventDefault={submitHandler}>
 <form class="flex flex-col h-full justify-between text-sm" on:submit|preventDefault={submitHandler}>
 	<div class=" overflow-y-scroll scrollbar-hidden h-full">
 	<div class=" overflow-y-scroll scrollbar-hidden h-full">
-		{#if ENABLE_OPENAI_API !== null && ENABLE_OLLAMA_API !== null && directConnectionsConfig !== null}
-			<div class="my-2">
-				<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('OpenAI API')}</div>
+		{#if ENABLE_OPENAI_API !== null && ENABLE_OLLAMA_API !== null && connectionsConfig !== null}
+			<div class="mb-3.5">
+				<div class=" mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
+
+				<hr class=" border-gray-100 dark:border-gray-850 my-2" />
+
+				<div class="my-2">
+					<div class="mt-2 space-y-2">
+						<div class="flex justify-between items-center text-sm">
+							<div class="  font-medium">{$i18n.t('OpenAI API')}</div>
+
+							<div class="flex items-center">
+								<div class="">
+									<Switch
+										bind:state={ENABLE_OPENAI_API}
+										on:change={async () => {
+											updateOpenAIHandler();
+										}}
+									/>
+								</div>
+							</div>
+						</div>
 
 
-						<div class="flex items-center">
+						{#if ENABLE_OPENAI_API}
 							<div class="">
 							<div class="">
-								<Switch
-									bind:state={ENABLE_OPENAI_API}
-									on:change={async () => {
-										updateOpenAIHandler();
-									}}
-								/>
+								<div class="flex justify-between items-center">
+									<div class="font-medium text-xs">{$i18n.t('Manage OpenAI API Connections')}</div>
+
+									<Tooltip content={$i18n.t(`Add Connection`)}>
+										<button
+											class="px-1"
+											on:click={() => {
+												showAddOpenAIConnectionModal = true;
+											}}
+											type="button"
+										>
+											<Plus />
+										</button>
+									</Tooltip>
+								</div>
+
+								<div class="flex flex-col gap-1.5 mt-1.5">
+									{#each OPENAI_API_BASE_URLS as url, idx}
+										<OpenAIConnection
+											pipeline={pipelineUrls[url] ? true : false}
+											bind:url
+											bind:key={OPENAI_API_KEYS[idx]}
+											bind:config={OPENAI_API_CONFIGS[idx]}
+											onSubmit={() => {
+												updateOpenAIHandler();
+											}}
+											onDelete={() => {
+												OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS.filter(
+													(url, urlIdx) => idx !== urlIdx
+												);
+												OPENAI_API_KEYS = OPENAI_API_KEYS.filter((key, keyIdx) => idx !== keyIdx);
+
+												let newConfig = {};
+												OPENAI_API_BASE_URLS.forEach((url, newIdx) => {
+													newConfig[newIdx] =
+														OPENAI_API_CONFIGS[newIdx < idx ? newIdx : newIdx + 1];
+												});
+												OPENAI_API_CONFIGS = newConfig;
+												updateOpenAIHandler();
+											}}
+										/>
+									{/each}
+								</div>
 							</div>
 							</div>
-						</div>
+						{/if}
 					</div>
 					</div>
+				</div>
 
 
-					{#if ENABLE_OPENAI_API}
-						<hr class=" border-gray-100 dark:border-gray-850" />
+				<div class=" my-2">
+					<div class="flex justify-between items-center text-sm mb-2">
+						<div class="  font-medium">{$i18n.t('Ollama API')}</div>
+
+						<div class="mt-1">
+							<Switch
+								bind:state={ENABLE_OLLAMA_API}
+								on:change={async () => {
+									updateOllamaHandler();
+								}}
+							/>
+						</div>
+					</div>
 
 
+					{#if ENABLE_OLLAMA_API}
 						<div class="">
 						<div class="">
 							<div class="flex justify-between items-center">
 							<div class="flex justify-between items-center">
-								<div class="font-medium">{$i18n.t('Manage OpenAI API Connections')}</div>
+								<div class="font-medium text-xs">{$i18n.t('Manage Ollama API Connections')}</div>
 
 
 								<Tooltip content={$i18n.t(`Add Connection`)}>
 								<Tooltip content={$i18n.t(`Add Connection`)}>
 									<button
 									<button
 										class="px-1"
 										class="px-1"
 										on:click={() => {
 										on:click={() => {
-											showAddOpenAIConnectionModal = true;
+											showAddOllamaConnectionModal = true;
 										}}
 										}}
 										type="button"
 										type="button"
 									>
 									>
@@ -253,133 +320,89 @@
 								</Tooltip>
 								</Tooltip>
 							</div>
 							</div>
 
 
-							<div class="flex flex-col gap-1.5 mt-1.5">
-								{#each OPENAI_API_BASE_URLS as url, idx}
-									<OpenAIConnection
-										pipeline={pipelineUrls[url] ? true : false}
-										bind:url
-										bind:key={OPENAI_API_KEYS[idx]}
-										bind:config={OPENAI_API_CONFIGS[idx]}
-										onSubmit={() => {
-											updateOpenAIHandler();
-										}}
-										onDelete={() => {
-											OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS.filter(
-												(url, urlIdx) => idx !== urlIdx
-											);
-											OPENAI_API_KEYS = OPENAI_API_KEYS.filter((key, keyIdx) => idx !== keyIdx);
-
-											let newConfig = {};
-											OPENAI_API_BASE_URLS.forEach((url, newIdx) => {
-												newConfig[newIdx] = OPENAI_API_CONFIGS[newIdx < idx ? newIdx : newIdx + 1];
-											});
-											OPENAI_API_CONFIGS = newConfig;
-											updateOpenAIHandler();
-										}}
-									/>
-								{/each}
+							<div class="flex w-full gap-1.5">
+								<div class="flex-1 flex flex-col gap-1.5 mt-1.5">
+									{#each OLLAMA_BASE_URLS as url, idx}
+										<OllamaConnection
+											bind:url
+											bind:config={OLLAMA_API_CONFIGS[idx]}
+											{idx}
+											onSubmit={() => {
+												updateOllamaHandler();
+											}}
+											onDelete={() => {
+												OLLAMA_BASE_URLS = OLLAMA_BASE_URLS.filter((url, urlIdx) => idx !== urlIdx);
+
+												let newConfig = {};
+												OLLAMA_BASE_URLS.forEach((url, newIdx) => {
+													newConfig[newIdx] =
+														OLLAMA_API_CONFIGS[newIdx < idx ? newIdx : newIdx + 1];
+												});
+												OLLAMA_API_CONFIGS = newConfig;
+											}}
+										/>
+									{/each}
+								</div>
+							</div>
+
+							<div class="mt-1 text-xs text-gray-400 dark:text-gray-500">
+								{$i18n.t('Trouble accessing Ollama?')}
+								<a
+									class=" text-gray-300 font-medium underline"
+									href="https://github.com/open-webui/open-webui#troubleshooting"
+									target="_blank"
+								>
+									{$i18n.t('Click here for help.')}
+								</a>
 							</div>
 							</div>
 						</div>
 						</div>
 					{/if}
 					{/if}
 				</div>
 				</div>
-			</div>
-
-			<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">
-					<div class="  font-medium">{$i18n.t('Ollama API')}</div>
-
-					<div class="mt-1">
-						<Switch
-							bind:state={ENABLE_OLLAMA_API}
-							on:change={async () => {
-								updateOllamaHandler();
-							}}
-						/>
-					</div>
-				</div>
-
-				{#if ENABLE_OLLAMA_API}
-					<hr class=" border-gray-100 dark:border-gray-850 my-2" />
 
 
-					<div class="">
-						<div class="flex justify-between items-center">
-							<div class="font-medium">{$i18n.t('Manage Ollama API Connections')}</div>
+				<div class="my-2">
+					<div class="flex justify-between items-center text-sm">
+						<div class="  font-medium">{$i18n.t('Direct Connections')}</div>
 
 
-							<Tooltip content={$i18n.t(`Add Connection`)}>
-								<button
-									class="px-1"
-									on:click={() => {
-										showAddOllamaConnectionModal = true;
+						<div class="flex items-center">
+							<div class="">
+								<Switch
+									bind:state={connectionsConfig.ENABLE_DIRECT_CONNECTIONS}
+									on:change={async () => {
+										updateConnectionsHandler();
 									}}
 									}}
-									type="button"
-								>
-									<Plus />
-								</button>
-							</Tooltip>
-						</div>
-
-						<div class="flex w-full gap-1.5">
-							<div class="flex-1 flex flex-col gap-1.5 mt-1.5">
-								{#each OLLAMA_BASE_URLS as url, idx}
-									<OllamaConnection
-										bind:url
-										bind:config={OLLAMA_API_CONFIGS[idx]}
-										{idx}
-										onSubmit={() => {
-											updateOllamaHandler();
-										}}
-										onDelete={() => {
-											OLLAMA_BASE_URLS = OLLAMA_BASE_URLS.filter((url, urlIdx) => idx !== urlIdx);
-
-											let newConfig = {};
-											OLLAMA_BASE_URLS.forEach((url, newIdx) => {
-												newConfig[newIdx] = OLLAMA_API_CONFIGS[newIdx < idx ? newIdx : newIdx + 1];
-											});
-											OLLAMA_API_CONFIGS = newConfig;
-										}}
-									/>
-								{/each}
+								/>
 							</div>
 							</div>
 						</div>
 						</div>
+					</div>
 
 
-						<div class="mt-1 text-xs text-gray-400 dark:text-gray-500">
-							{$i18n.t('Trouble accessing Ollama?')}
-							<a
-								class=" text-gray-300 font-medium underline"
-								href="https://github.com/open-webui/open-webui#troubleshooting"
-								target="_blank"
-							>
-								{$i18n.t('Click here for help.')}
-							</a>
-						</div>
+					<div class="mt-1 text-xs text-gray-400 dark:text-gray-500">
+						{$i18n.t(
+							'Direct Connections allow users to connect to their own OpenAI compatible API endpoints.'
+						)}
 					</div>
 					</div>
-				{/if}
-			</div>
+				</div>
 
 
-			<hr class=" border-gray-100 dark:border-gray-850" />
+				<hr class=" border-gray-100 dark:border-gray-850 my-2" />
 
 
-			<div class="pr-1.5 my-2">
-				<div class="flex justify-between items-center text-sm">
-					<div class="  font-medium">{$i18n.t('Direct Connections')}</div>
+				<div class="my-2">
+					<div class="flex justify-between items-center text-sm">
+						<div class=" text-xs font-medium">{$i18n.t('Cache Base Model List')}</div>
 
 
-					<div class="flex items-center">
-						<div class="">
-							<Switch
-								bind:state={directConnectionsConfig.ENABLE_DIRECT_CONNECTIONS}
-								on:change={async () => {
-									updateDirectConnectionsHandler();
-								}}
-							/>
+						<div class="flex items-center">
+							<div class="">
+								<Switch
+									bind:state={connectionsConfig.ENABLE_BASE_MODELS_CACHE}
+									on:change={async () => {
+										updateConnectionsHandler();
+									}}
+								/>
+							</div>
 						</div>
 						</div>
 					</div>
 					</div>
-				</div>
 
 
-				<div class="mt-1.5">
-					<div class="text-xs text-gray-500">
+					<div class="mt-1 text-xs text-gray-400 dark:text-gray-500">
 						{$i18n.t(
 						{$i18n.t(
-							'Direct Connections allow users to connect to their own OpenAI compatible API endpoints.'
+							'Base Model List Cache speeds up access by fetching base models only at startup or on settings save—faster, but may not show recent base model changes.'
 						)}
 						)}
 					</div>
 					</div>
 				</div>
 				</div>

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

@@ -5,6 +5,7 @@
 
 
 	import Modal from '$lib/components/common/Modal.svelte';
 	import Modal from '$lib/components/common/Modal.svelte';
 	import ManageOllama from '../Models/Manage/ManageOllama.svelte';
 	import ManageOllama from '../Models/Manage/ManageOllama.svelte';
+	import XMark from '$lib/components/icons/XMark.svelte';
 
 
 	export let show = false;
 	export let show = false;
 	export let urlIdx: number | null = null;
 	export let urlIdx: number | null = null;
@@ -26,16 +27,7 @@
 					show = false;
 					show = false;
 				}}
 				}}
 			>
 			>
-				<svg
-					xmlns="http://www.w3.org/2000/svg"
-					viewBox="0 0 20 20"
-					fill="currentColor"
-					class="w-5 h-5"
-				>
-					<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>
+				<XMark className={'size-5'} />
 			</button>
 			</button>
 		</div>
 		</div>
 
 

+ 7 - 33
src/lib/components/admin/Settings/Documents.svelte

@@ -90,10 +90,6 @@
 			return;
 			return;
 		}
 		}
 
 
-		if (embeddingEngine === 'openai' && (OpenAIKey === '' || OpenAIUrl === '')) {
-			toast.error($i18n.t('OpenAI URL/Key required.'));
-			return;
-		}
 		if (
 		if (
 			embeddingEngine === 'azure_openai' &&
 			embeddingEngine === 'azure_openai' &&
 			(AzureOpenAIKey === '' || AzureOpenAIUrl === '' || AzureOpenAIVersion === '')
 			(AzureOpenAIKey === '' || AzureOpenAIUrl === '' || AzureOpenAIVersion === '')
@@ -731,7 +727,11 @@
 										required
 										required
 									/>
 									/>
 
 
-									<SensitiveInput placeholder={$i18n.t('API Key')} bind:value={OpenAIKey} />
+									<SensitiveInput
+										placeholder={$i18n.t('API Key')}
+										bind:value={OpenAIKey}
+										required={false}
+									/>
 								</div>
 								</div>
 							{:else if embeddingEngine === 'ollama'}
 							{:else if embeddingEngine === 'ollama'}
 								<div class="my-0.5 flex gap-2 pr-2">
 								<div class="my-0.5 flex gap-2 pr-2">
@@ -808,33 +808,7 @@
 											>
 											>
 												{#if updateEmbeddingModelLoading}
 												{#if updateEmbeddingModelLoading}
 													<div class="self-center">
 													<div class="self-center">
-														<svg
-															class=" w-4 h-4"
-															viewBox="0 0 24 24"
-															fill="currentColor"
-															xmlns="http://www.w3.org/2000/svg"
-														>
-															<style>
-																.spinner_ajPY {
-																	transform-origin: center;
-																	animation: spinner_AtaB 0.75s infinite linear;
-																}
-
-																@keyframes spinner_AtaB {
-																	100% {
-																		transform: rotate(360deg);
-																	}
-																}
-															</style>
-															<path
-																d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
-																opacity=".25"
-															/>
-															<path
-																d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
-																class="spinner_ajPY"
-															/>
-														</svg>
+														<Spinner />
 													</div>
 													</div>
 												{:else}
 												{:else}
 													<svg
 													<svg
@@ -1272,7 +1246,7 @@
 		</div>
 		</div>
 	{:else}
 	{:else}
 		<div class="flex items-center justify-center h-full">
 		<div class="flex items-center justify-center h-full">
-			<Spinner />
+			<Spinner className="size-5" />
 		</div>
 		</div>
 	{/if}
 	{/if}
 </form>
 </form>

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

@@ -3,6 +3,7 @@
 	const i18n = getContext('i18n');
 	const i18n = getContext('i18n');
 	const dispatch = createEventDispatcher();
 	const dispatch = createEventDispatcher();
 
 
+	import Spinner from '$lib/components/common/Spinner.svelte';
 	import Modal from '$lib/components/common/Modal.svelte';
 	import Modal from '$lib/components/common/Modal.svelte';
 	import { models } from '$lib/stores';
 	import { models } from '$lib/stores';
 	import Plus from '$lib/components/icons/Plus.svelte';
 	import Plus from '$lib/components/icons/Plus.svelte';
@@ -11,6 +12,7 @@
 	import { toast } from 'svelte-sonner';
 	import { toast } from 'svelte-sonner';
 	import AccessControl from '$lib/components/workspace/common/AccessControl.svelte';
 	import AccessControl from '$lib/components/workspace/common/AccessControl.svelte';
 	import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
 	import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
+	import XMark from '$lib/components/icons/XMark.svelte';
 
 
 	export let show = false;
 	export let show = false;
 	export let edit = false;
 	export let edit = false;
@@ -141,16 +143,7 @@
 					show = false;
 					show = false;
 				}}
 				}}
 			>
 			>
-				<svg
-					xmlns="http://www.w3.org/2000/svg"
-					viewBox="0 0 20 20"
-					fill="currentColor"
-					class="w-5 h-5"
-				>
-					<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>
+				<XMark className={'size-5'} />
 			</button>
 			</button>
 		</div>
 		</div>
 
 
@@ -406,29 +399,7 @@
 
 
 							{#if loading}
 							{#if loading}
 								<div class="ml-2 self-center">
 								<div class="ml-2 self-center">
-									<svg
-										class=" w-4 h-4"
-										viewBox="0 0 24 24"
-										fill="currentColor"
-										xmlns="http://www.w3.org/2000/svg"
-										><style>
-											.spinner_ajPY {
-												transform-origin: center;
-												animation: spinner_AtaB 0.75s infinite linear;
-											}
-											@keyframes spinner_AtaB {
-												100% {
-													transform: rotate(360deg);
-												}
-											}
-										</style><path
-											d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
-											opacity=".25"
-										/><path
-											d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
-											class="spinner_ajPY"
-										/></svg
-									>
+									<Spinner />
 								</div>
 								</div>
 							{/if}
 							{/if}
 						</button>
 						</button>

+ 14 - 10
src/lib/components/admin/Settings/General.svelte

@@ -90,7 +90,9 @@
 	};
 	};
 
 
 	onMount(async () => {
 	onMount(async () => {
-		checkForVersionUpdates();
+		if (!$config?.offline_mode) {
+			checkForVersionUpdates();
+		}
 
 
 		await Promise.all([
 		await Promise.all([
 			(async () => {
 			(async () => {
@@ -160,15 +162,17 @@
 								</button>
 								</button>
 							</div>
 							</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>
+							{#if !$config?.offline_mode}
+								<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>
+							{/if}
 						</div>
 						</div>
 					</div>
 					</div>
 
 

+ 9 - 29
src/lib/components/admin/Settings/Images.svelte

@@ -13,9 +13,11 @@
 		updateConfig,
 		updateConfig,
 		verifyConfigUrl
 		verifyConfigUrl
 	} from '$lib/apis/images';
 	} from '$lib/apis/images';
+	import Spinner from '$lib/components/common/Spinner.svelte';
 	import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
 	import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
 	import Switch from '$lib/components/common/Switch.svelte';
 	import Switch from '$lib/components/common/Switch.svelte';
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
+	import Textarea from '$lib/components/common/Textarea.svelte';
 	const dispatch = createEventDispatcher();
 	const dispatch = createEventDispatcher();
 
 
 	const i18n = getContext('i18n');
 	const i18n = getContext('i18n');
@@ -504,7 +506,7 @@
 						<div class=" mb-2 text-sm font-medium">{$i18n.t('ComfyUI Workflow')}</div>
 						<div class=" mb-2 text-sm font-medium">{$i18n.t('ComfyUI Workflow')}</div>
 
 
 						{#if config.comfyui.COMFYUI_WORKFLOW}
 						{#if config.comfyui.COMFYUI_WORKFLOW}
-							<textarea
+							<Textarea
 								class="w-full rounded-lg mb-1 py-2 px-4 text-xs bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden disabled:text-gray-600 resize-none"
 								class="w-full rounded-lg mb-1 py-2 px-4 text-xs bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden disabled:text-gray-600 resize-none"
 								rows="10"
 								rows="10"
 								bind:value={config.comfyui.COMFYUI_WORKFLOW}
 								bind:value={config.comfyui.COMFYUI_WORKFLOW}
@@ -533,7 +535,7 @@
 								/>
 								/>
 
 
 								<button
 								<button
-									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"
+									class="w-full text-sm font-medium py-2 bg-transparent hover:bg-gray-50 border border-dashed border-gray-50 dark:border-gray-850 dark:hover:bg-gray-850 text-center rounded-xl"
 									type="button"
 									type="button"
 									on:click={() => {
 									on:click={() => {
 										document.getElementById('upload-comfyui-workflow-input')?.click();
 										document.getElementById('upload-comfyui-workflow-input')?.click();
@@ -555,10 +557,10 @@
 
 
 							<div class="text-xs flex flex-col gap-1.5">
 							<div class="text-xs flex flex-col gap-1.5">
 								{#each requiredWorkflowNodes as node}
 								{#each requiredWorkflowNodes as node}
-									<div class="flex w-full items-center border dark:border-gray-850 rounded-lg">
+									<div class="flex w-full items-center">
 										<div class="shrink-0">
 										<div class="shrink-0">
 											<div
 											<div
-												class=" capitalize line-clamp-1 font-medium px-3 py-1 w-20 text-center rounded-l-lg bg-green-500/10 text-green-700 dark:text-green-200"
+												class=" capitalize line-clamp-1 font-medium px-3 py-1 w-20 text-center bg-green-500/10 text-green-700 dark:text-green-200"
 											>
 											>
 												{node.type}{node.type === 'prompt' ? '*' : ''}
 												{node.type}{node.type === 'prompt' ? '*' : ''}
 											</div>
 											</div>
@@ -566,7 +568,7 @@
 										<div class="">
 										<div class="">
 											<Tooltip content="Input Key (e.g. text, unet_name, steps)">
 											<Tooltip content="Input Key (e.g. text, unet_name, steps)">
 												<input
 												<input
-													class="py-1 px-3 w-24 text-xs text-center bg-transparent outline-hidden border-r dark:border-gray-850"
+													class="py-1 px-3 w-24 text-xs text-center bg-transparent outline-hidden border-r border-gray-50 dark:border-gray-850"
 													placeholder="Key"
 													placeholder="Key"
 													bind:value={node.key}
 													bind:value={node.key}
 													required
 													required
@@ -580,7 +582,7 @@
 												placement="top-start"
 												placement="top-start"
 											>
 											>
 												<input
 												<input
-													class="w-full py-1 px-4 rounded-r-lg text-xs bg-transparent outline-hidden"
+													class="w-full py-1 px-4 text-xs bg-transparent outline-hidden"
 													placeholder="Node Ids"
 													placeholder="Node Ids"
 													bind:value={node.node_ids}
 													bind:value={node.node_ids}
 												/>
 												/>
@@ -711,29 +713,7 @@
 
 
 			{#if loading}
 			{#if loading}
 				<div class="ml-2 self-center">
 				<div class="ml-2 self-center">
-					<svg
-						class=" w-4 h-4"
-						viewBox="0 0 24 24"
-						fill="currentColor"
-						xmlns="http://www.w3.org/2000/svg"
-						><style>
-							.spinner_ajPY {
-								transform-origin: center;
-								animation: spinner_AtaB 0.75s infinite linear;
-							}
-							@keyframes spinner_AtaB {
-								100% {
-									transform: rotate(360deg);
-								}
-							}
-						</style><path
-							d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
-							opacity=".25"
-						/><path
-							d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
-							class="spinner_ajPY"
-						/></svg
-					>
+					<Spinner />
 				</div>
 				</div>
 			{/if}
 			{/if}
 		</button>
 		</button>

+ 3 - 3
src/lib/components/admin/Settings/Interface.svelte

@@ -390,7 +390,7 @@
 
 
 				<div class="mb-2.5">
 				<div class="mb-2.5">
 					<div class="flex w-full justify-between">
 					<div class="flex w-full justify-between">
-						<div class=" self-center text-sm">
+						<div class=" self-center text-xs">
 							{$i18n.t('Banners')}
 							{$i18n.t('Banners')}
 						</div>
 						</div>
 
 
@@ -432,7 +432,7 @@
 				{#if $user?.role === 'admin'}
 				{#if $user?.role === 'admin'}
 					<div class=" space-y-3">
 					<div class=" space-y-3">
 						<div class="flex w-full justify-between mb-2">
 						<div class="flex w-full justify-between mb-2">
-							<div class=" self-center text-sm">
+							<div class=" self-center text-xs">
 								{$i18n.t('Default Prompt Suggestions')}
 								{$i18n.t('Default Prompt Suggestions')}
 							</div>
 							</div>
 
 
@@ -636,6 +636,6 @@
 	</form>
 	</form>
 {:else}
 {:else}
 	<div class=" h-full w-full flex justify-center items-center">
 	<div class=" h-full w-full flex justify-center items-center">
-		<Spinner />
+		<Spinner className="size-5" />
 	</div>
 	</div>
 {/if}
 {/if}

+ 17 - 16
src/lib/components/admin/Settings/Interface/Banners.svelte

@@ -1,7 +1,9 @@
 <script lang="ts">
 <script lang="ts">
 	import Switch from '$lib/components/common/Switch.svelte';
 	import Switch from '$lib/components/common/Switch.svelte';
+	import Textarea from '$lib/components/common/Textarea.svelte';
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
 	import EllipsisVertical from '$lib/components/icons/EllipsisVertical.svelte';
 	import EllipsisVertical from '$lib/components/icons/EllipsisVertical.svelte';
+	import XMark from '$lib/components/icons/XMark.svelte';
 	import Sortable from 'sortablejs';
 	import Sortable from 'sortablejs';
 	import { getContext } from 'svelte';
 	import { getContext } from 'svelte';
 	const i18n = getContext('i18n');
 	const i18n = getContext('i18n');
@@ -23,6 +25,13 @@
 		});
 		});
 	};
 	};
 
 
+	const classNames: Record<string, string> = {
+		info: 'bg-blue-500/20 text-blue-700 dark:text-blue-200 ',
+		success: 'bg-green-500/20 text-green-700 dark:text-green-200',
+		warning: 'bg-yellow-500/20 text-yellow-700 dark:text-yellow-200',
+		error: 'bg-red-500/20 text-red-700 dark:text-red-200'
+	};
+
 	$: if (banners) {
 	$: if (banners) {
 		init();
 		init();
 	}
 	}
@@ -44,14 +53,14 @@
 	};
 	};
 </script>
 </script>
 
 
-<div class=" flex flex-col space-y-0.5" bind:this={bannerListElement}>
+<div class=" flex flex-col gap-3 {banners?.length > 0 ? 'mt-2' : ''}" bind:this={bannerListElement}>
 	{#each banners as banner, bannerIdx (banner.id)}
 	{#each banners as banner, bannerIdx (banner.id)}
-		<div class=" flex justify-between items-center -ml-1" id="banner-item-{banner.id}">
+		<div class=" flex justify-between items-start -ml-1" id="banner-item-{banner.id}">
 			<EllipsisVertical className="size-4 cursor-move item-handle" />
 			<EllipsisVertical className="size-4 cursor-move item-handle" />
 
 
-			<div class="flex flex-row flex-1 gap-2 items-center">
+			<div class="flex flex-row flex-1 gap-2 items-start">
 				<select
 				<select
-					class="w-fit capitalize rounded-xl text-xs bg-transparent outline-hidden text-left pl-1 pr-2"
+					class="w-fit capitalize rounded-xl text-xs bg-transparent outline-hidden pl-1 pr-5"
 					bind:value={banner.type}
 					bind:value={banner.type}
 					required
 					required
 				>
 				>
@@ -64,10 +73,11 @@
 					<option value="success" class="text-gray-900">{$i18n.t('Success')}</option>
 					<option value="success" class="text-gray-900">{$i18n.t('Success')}</option>
 				</select>
 				</select>
 
 
-				<input
-					class="pr-5 py-1.5 text-xs w-full bg-transparent outline-hidden"
+				<Textarea
+					className="mr-2 text-xs w-full bg-transparent outline-hidden resize-none"
 					placeholder={$i18n.t('Content')}
 					placeholder={$i18n.t('Content')}
 					bind:value={banner.content}
 					bind:value={banner.content}
+					maxSize={100}
 				/>
 				/>
 
 
 				<div class="relative -left-2">
 				<div class="relative -left-2">
@@ -85,16 +95,7 @@
 					banners = banners;
 					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>
+				<XMark className={'size-4'} />
 			</button>
 			</button>
 		</div>
 		</div>
 	{/each}
 	{/each}

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

@@ -563,6 +563,6 @@
 	{/if}
 	{/if}
 {:else}
 {:else}
 	<div class=" h-full w-full flex justify-center items-center">
 	<div class=" h-full w-full flex justify-center items-center">
-		<Spinner />
+		<Spinner className="size-5" />
 	</div>
 	</div>
 {/if}
 {/if}

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

@@ -18,6 +18,7 @@
 	import Plus from '$lib/components/icons/Plus.svelte';
 	import Plus from '$lib/components/icons/Plus.svelte';
 	import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
 	import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
 	import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
 	import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
+	import XMark from '$lib/components/icons/XMark.svelte';
 
 
 	export let show = false;
 	export let show = false;
 	export let initHandler = () => {};
 	export let initHandler = () => {};
@@ -129,16 +130,7 @@
 					show = false;
 					show = false;
 				}}
 				}}
 			>
 			>
-				<svg
-					xmlns="http://www.w3.org/2000/svg"
-					viewBox="0 0 20 20"
-					fill="currentColor"
-					class="w-5 h-5"
-				>
-					<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>
+				<XMark className={'size-5'} />
 			</button>
 			</button>
 		</div>
 		</div>
 
 
@@ -278,29 +270,7 @@
 
 
 								{#if loading}
 								{#if loading}
 									<div class="ml-2 self-center">
 									<div class="ml-2 self-center">
-										<svg
-											class=" w-4 h-4"
-											viewBox="0 0 24 24"
-											fill="currentColor"
-											xmlns="http://www.w3.org/2000/svg"
-											><style>
-												.spinner_ajPY {
-													transform-origin: center;
-													animation: spinner_AtaB 0.75s infinite linear;
-												}
-												@keyframes spinner_AtaB {
-													100% {
-														transform: rotate(360deg);
-													}
-												}
-											</style><path
-												d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
-												opacity=".25"
-											/><path
-												d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
-												class="spinner_ajPY"
-											/></svg
-										>
+										<Spinner />
 									</div>
 									</div>
 								{/if}
 								{/if}
 							</button>
 							</button>
@@ -308,7 +278,7 @@
 					</form>
 					</form>
 				{:else}
 				{:else}
 					<div>
 					<div>
-						<Spinner />
+						<Spinner className="size-5" />
 					</div>
 					</div>
 				{/if}
 				{/if}
 			</div>
 			</div>

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

@@ -1057,6 +1057,6 @@
 	</div>
 	</div>
 {:else}
 {:else}
 	<div class="flex justify-center items-center w-full h-full py-3">
 	<div class="flex justify-center items-center w-full h-full py-3">
-		<Spinner />
+		<Spinner className="size-5" />
 	</div>
 	</div>
 {/if}
 {/if}

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

@@ -7,6 +7,7 @@
 
 
 	import { user } from '$lib/stores';
 	import { user } from '$lib/stores';
 
 
+	import XMark from '$lib/components/icons/XMark.svelte';
 	import Modal from '$lib/components/common/Modal.svelte';
 	import Modal from '$lib/components/common/Modal.svelte';
 	import ManageOllama from './Manage/ManageOllama.svelte';
 	import ManageOllama from './Manage/ManageOllama.svelte';
 	import { getOllamaConfig } from '$lib/apis/ollama';
 	import { getOllamaConfig } from '$lib/apis/ollama';
@@ -48,16 +49,7 @@
 					show = false;
 					show = false;
 				}}
 				}}
 			>
 			>
-				<svg
-					xmlns="http://www.w3.org/2000/svg"
-					viewBox="0 0 20 20"
-					fill="currentColor"
-					class="w-5 h-5"
-				>
-					<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>
+				<XMark className={'size-5'} />
 			</button>
 			</button>
 		</div>
 		</div>
 
 

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

@@ -141,6 +141,7 @@
 												placeholder={$i18n.t('Enter Searxng Query URL')}
 												placeholder={$i18n.t('Enter Searxng Query URL')}
 												bind:value={webConfig.SEARXNG_QUERY_URL}
 												bind:value={webConfig.SEARXNG_QUERY_URL}
 												autocomplete="off"
 												autocomplete="off"
+												required
 											/>
 											/>
 										</div>
 										</div>
 									</div>
 									</div>
@@ -248,7 +249,6 @@
 										bind:value={webConfig.KAGI_SEARCH_API_KEY}
 										bind:value={webConfig.KAGI_SEARCH_API_KEY}
 									/>
 									/>
 								</div>
 								</div>
-								.
 							</div>
 							</div>
 						{:else if webConfig.WEB_SEARCH_ENGINE === 'mojeek'}
 						{:else if webConfig.WEB_SEARCH_ENGINE === 'mojeek'}
 							<div class="mb-2.5 flex w-full flex-col">
 							<div class="mb-2.5 flex w-full flex-col">

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

@@ -16,6 +16,7 @@
 	import UsersSolid from '$lib/components/icons/UsersSolid.svelte';
 	import UsersSolid from '$lib/components/icons/UsersSolid.svelte';
 	import ChevronRight from '$lib/components/icons/ChevronRight.svelte';
 	import ChevronRight from '$lib/components/icons/ChevronRight.svelte';
 	import EllipsisHorizontal from '$lib/components/icons/EllipsisHorizontal.svelte';
 	import EllipsisHorizontal from '$lib/components/icons/EllipsisHorizontal.svelte';
+	import Search from '$lib/components/icons/Search.svelte';
 	import User from '$lib/components/icons/User.svelte';
 	import User from '$lib/components/icons/User.svelte';
 	import UserCircleSolid from '$lib/components/icons/UserCircleSolid.svelte';
 	import UserCircleSolid from '$lib/components/icons/UserCircleSolid.svelte';
 	import GroupModal from './Groups/EditGroupModal.svelte';
 	import GroupModal from './Groups/EditGroupModal.svelte';
@@ -159,18 +160,7 @@
 			<div class=" flex w-full space-x-2">
 			<div class=" flex w-full space-x-2">
 				<div class="flex flex-1">
 				<div class="flex flex-1">
 					<div class=" self-center ml-1 mr-3">
 					<div class=" self-center ml-1 mr-3">
-						<svg
-							xmlns="http://www.w3.org/2000/svg"
-							viewBox="0 0 20 20"
-							fill="currentColor"
-							class="w-4 h-4"
-						>
-							<path
-								fill-rule="evenodd"
-								d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z"
-								clip-rule="evenodd"
-							/>
-						</svg>
+						<Search />
 					</div>
 					</div>
 					<input
 					<input
 						class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-hidden bg-transparent"
 						class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-hidden bg-transparent"

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

@@ -3,8 +3,10 @@
 	import { getContext, onMount } from 'svelte';
 	import { getContext, onMount } from 'svelte';
 	const i18n = getContext('i18n');
 	const i18n = getContext('i18n');
 
 
+	import Spinner from '$lib/components/common/Spinner.svelte';
 	import Modal from '$lib/components/common/Modal.svelte';
 	import Modal from '$lib/components/common/Modal.svelte';
 	import Textarea from '$lib/components/common/Textarea.svelte';
 	import Textarea from '$lib/components/common/Textarea.svelte';
+	import XMark from '$lib/components/icons/XMark.svelte';
 	export let onSubmit: Function = () => {};
 	export let onSubmit: Function = () => {};
 	export let show = false;
 	export let show = false;
 
 
@@ -45,16 +47,7 @@
 					show = false;
 					show = false;
 				}}
 				}}
 			>
 			>
-				<svg
-					xmlns="http://www.w3.org/2000/svg"
-					viewBox="0 0 20 20"
-					fill="currentColor"
-					class="w-5 h-5"
-				>
-					<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>
+				<XMark className={'size-5'} />
 			</button>
 			</button>
 		</div>
 		</div>
 
 
@@ -111,29 +104,7 @@
 
 
 							{#if loading}
 							{#if loading}
 								<div class="ml-2 self-center">
 								<div class="ml-2 self-center">
-									<svg
-										class=" w-4 h-4"
-										viewBox="0 0 24 24"
-										fill="currentColor"
-										xmlns="http://www.w3.org/2000/svg"
-										><style>
-											.spinner_ajPY {
-												transform-origin: center;
-												animation: spinner_AtaB 0.75s infinite linear;
-											}
-											@keyframes spinner_AtaB {
-												100% {
-													transform: rotate(360deg);
-												}
-											}
-										</style><path
-											d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
-											opacity=".25"
-										/><path
-											d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
-											class="spinner_ajPY"
-										/></svg
-									>
+									<Spinner />
 								</div>
 								</div>
 							{/if}
 							{/if}
 						</button>
 						</button>

+ 4 - 33
src/lib/components/admin/Users/Groups/EditGroupModal.svelte

@@ -3,6 +3,7 @@
 	import { getContext, onMount } from 'svelte';
 	import { getContext, onMount } from 'svelte';
 	const i18n = getContext('i18n');
 	const i18n = getContext('i18n');
 
 
+	import Spinner from '$lib/components/common/Spinner.svelte';
 	import Modal from '$lib/components/common/Modal.svelte';
 	import Modal from '$lib/components/common/Modal.svelte';
 	import Display from './Display.svelte';
 	import Display from './Display.svelte';
 	import Permissions from './Permissions.svelte';
 	import Permissions from './Permissions.svelte';
@@ -10,6 +11,7 @@
 	import UserPlusSolid from '$lib/components/icons/UserPlusSolid.svelte';
 	import UserPlusSolid from '$lib/components/icons/UserPlusSolid.svelte';
 	import WrenchSolid from '$lib/components/icons/WrenchSolid.svelte';
 	import WrenchSolid from '$lib/components/icons/WrenchSolid.svelte';
 	import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
 	import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
+	import XMark from '$lib/components/icons/XMark.svelte';
 
 
 	export let onSubmit: Function = () => {};
 	export let onSubmit: Function = () => {};
 	export let onDelete: Function = () => {};
 	export let onDelete: Function = () => {};
@@ -124,16 +126,7 @@
 					show = false;
 					show = false;
 				}}
 				}}
 			>
 			>
-				<svg
-					xmlns="http://www.w3.org/2000/svg"
-					viewBox="0 0 20 20"
-					fill="currentColor"
-					class="w-5 h-5"
-				>
-					<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>
+				<XMark className={'size-5'} />
 			</button>
 			</button>
 		</div>
 		</div>
 
 
@@ -305,29 +298,7 @@
 
 
 							{#if loading}
 							{#if loading}
 								<div class="ml-2 self-center">
 								<div class="ml-2 self-center">
-									<svg
-										class=" w-4 h-4"
-										viewBox="0 0 24 24"
-										fill="currentColor"
-										xmlns="http://www.w3.org/2000/svg"
-										><style>
-											.spinner_ajPY {
-												transform-origin: center;
-												animation: spinner_AtaB 0.75s infinite linear;
-											}
-											@keyframes spinner_AtaB {
-												100% {
-													transform: rotate(360deg);
-												}
-											}
-										</style><path
-											d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
-											opacity=".25"
-										/><path
-											d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
-											class="spinner_ajPY"
-										/></svg
-									>
+									<Spinner />
 								</div>
 								</div>
 							{/if}
 							{/if}
 						</button>
 						</button>

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

@@ -7,6 +7,7 @@
 	import { WEBUI_BASE_URL } from '$lib/constants';
 	import { WEBUI_BASE_URL } from '$lib/constants';
 	import Checkbox from '$lib/components/common/Checkbox.svelte';
 	import Checkbox from '$lib/components/common/Checkbox.svelte';
 	import Badge from '$lib/components/common/Badge.svelte';
 	import Badge from '$lib/components/common/Badge.svelte';
+	import Search from '$lib/components/icons/Search.svelte';
 
 
 	export let users = [];
 	export let users = [];
 	export let userIds = [];
 	export let userIds = [];
@@ -50,18 +51,7 @@
 	<div class="flex w-full">
 	<div class="flex w-full">
 		<div class="flex flex-1">
 		<div class="flex flex-1">
 			<div class=" self-center mr-3">
 			<div class=" self-center mr-3">
-				<svg
-					xmlns="http://www.w3.org/2000/svg"
-					viewBox="0 0 20 20"
-					fill="currentColor"
-					class="w-4 h-4"
-				>
-					<path
-						fill-rule="evenodd"
-						d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z"
-						clip-rule="evenodd"
-					/>
-				</svg>
+				<Search />
 			</div>
 			</div>
 			<input
 			<input
 				class=" w-full text-sm pr-4 rounded-r-xl outline-hidden bg-transparent"
 				class=" w-full text-sm pr-4 rounded-r-xl outline-hidden bg-transparent"

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

@@ -151,7 +151,7 @@
 
 
 {#if users === null || total === null}
 {#if users === null || total === null}
 	<div class="my-10">
 	<div class="my-10">
-		<Spinner />
+		<Spinner className="size-5" />
 	</div>
 	</div>
 {:else}
 {:else}
 	<div class="mt-0.5 mb-2 gap-1 flex flex-col md:flex-row justify-between">
 	<div class="mt-0.5 mb-2 gap-1 flex flex-col md:flex-row justify-between">

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

@@ -6,8 +6,10 @@
 
 
 	import { WEBUI_BASE_URL } from '$lib/constants';
 	import { WEBUI_BASE_URL } from '$lib/constants';
 
 
+	import Spinner from '$lib/components/common/Spinner.svelte';
 	import Modal from '$lib/components/common/Modal.svelte';
 	import Modal from '$lib/components/common/Modal.svelte';
 	import { generateInitialsImage } from '$lib/utils';
 	import { generateInitialsImage } from '$lib/utils';
+	import XMark from '$lib/components/icons/XMark.svelte';
 
 
 	const i18n = getContext('i18n');
 	const i18n = getContext('i18n');
 	const dispatch = createEventDispatcher();
 	const dispatch = createEventDispatcher();
@@ -132,16 +134,7 @@
 					show = false;
 					show = false;
 				}}
 				}}
 			>
 			>
-				<svg
-					xmlns="http://www.w3.org/2000/svg"
-					viewBox="0 0 20 20"
-					fill="currentColor"
-					class="w-5 h-5"
-				>
-					<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>
+				<XMark className={'size-5'} />
 			</button>
 			</button>
 		</div>
 		</div>
 
 
@@ -293,29 +286,7 @@
 
 
 							{#if loading}
 							{#if loading}
 								<div class="ml-2 self-center">
 								<div class="ml-2 self-center">
-									<svg
-										class=" w-4 h-4"
-										viewBox="0 0 24 24"
-										fill="currentColor"
-										xmlns="http://www.w3.org/2000/svg"
-										><style>
-											.spinner_ajPY {
-												transform-origin: center;
-												animation: spinner_AtaB 0.75s infinite linear;
-											}
-											@keyframes spinner_AtaB {
-												100% {
-													transform: rotate(360deg);
-												}
-											}
-										</style><path
-											d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
-											opacity=".25"
-										/><path
-											d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
-											class="spinner_ajPY"
-										/></svg
-									>
+									<Spinner />
 								</div>
 								</div>
 							{/if}
 							{/if}
 						</button>
 						</button>

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

@@ -8,6 +8,7 @@
 
 
 	import Modal from '$lib/components/common/Modal.svelte';
 	import Modal from '$lib/components/common/Modal.svelte';
 	import localizedFormat from 'dayjs/plugin/localizedFormat';
 	import localizedFormat from 'dayjs/plugin/localizedFormat';
+	import XMark from '$lib/components/icons/XMark.svelte';
 
 
 	const i18n = getContext('i18n');
 	const i18n = getContext('i18n');
 	const dispatch = createEventDispatcher();
 	const dispatch = createEventDispatcher();
@@ -54,16 +55,7 @@
 					show = false;
 					show = false;
 				}}
 				}}
 			>
 			>
-				<svg
-					xmlns="http://www.w3.org/2000/svg"
-					viewBox="0 0 20 20"
-					fill="currentColor"
-					class="w-5 h-5"
-				>
-					<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>
+				<XMark className={'size-5'} />
 			</button>
 			</button>
 		</div>
 		</div>
 
 

+ 46 - 34
src/lib/components/channel/MessageInput.svelte

@@ -1,6 +1,7 @@
 <script lang="ts">
 <script lang="ts">
 	import { toast } from 'svelte-sonner';
 	import { toast } from 'svelte-sonner';
 	import { v4 as uuidv4 } from 'uuid';
 	import { v4 as uuidv4 } from 'uuid';
+	import heic2any from 'heic2any';
 
 
 	import { tick, getContext, onMount, onDestroy } from 'svelte';
 	import { tick, getContext, onMount, onDestroy } from 'svelte';
 
 
@@ -78,7 +79,7 @@
 	};
 	};
 
 
 	const inputFilesHandler = async (inputFiles) => {
 	const inputFilesHandler = async (inputFiles) => {
-		inputFiles.forEach((file) => {
+		inputFiles.forEach(async (file) => {
 			console.info('Processing file:', {
 			console.info('Processing file:', {
 				name: file.name,
 				name: file.name,
 				type: file.type,
 				type: file.type,
@@ -102,44 +103,51 @@
 				return;
 				return;
 			}
 			}
 
 
-			if (
-				['image/gif', 'image/webp', 'image/jpeg', 'image/png', 'image/avif'].includes(file['type'])
-			) {
-				let reader = new FileReader();
+			if (file['type'].startsWith('image/')) {
+				const compressImageHandler = async (imageUrl, settings = {}, config = {}) => {
+					// Quick shortcut so we don’t do unnecessary work.
+					const settingsCompression = settings?.imageCompression ?? false;
+					const configWidth = config?.file?.image_compression?.width ?? null;
+					const configHeight = config?.file?.image_compression?.height ?? null;
 
 
-				reader.onload = async (event) => {
-					let imageUrl = event.target.result;
+					// If neither settings nor config wants compression, return original URL.
+					if (!settingsCompression && !configWidth && !configHeight) {
+						return imageUrl;
+					}
 
 
-					if (
-						($settings?.imageCompression ?? false) ||
-						($config?.file?.image_compression?.width ?? null) ||
-						($config?.file?.image_compression?.height ?? null)
-					) {
-						let width = null;
-						let height = null;
-
-						if ($settings?.imageCompression ?? false) {
-							width = $settings?.imageCompressionSize?.width ?? null;
-							height = $settings?.imageCompressionSize?.height ?? null;
-						}
+					// Default to null (no compression unless set)
+					let width = null;
+					let height = null;
 
 
-						if (
-							($config?.file?.image_compression?.width ?? null) ||
-							($config?.file?.image_compression?.height ?? null)
-						) {
-							if (width > ($config?.file?.image_compression?.width ?? null)) {
-								width = $config?.file?.image_compression?.width ?? null;
-							}
-							if (height > ($config?.file?.image_compression?.height ?? null)) {
-								height = $config?.file?.image_compression?.height ?? null;
-							}
-						}
+					// If user/settings want compression, pick their preferred size.
+					if (settingsCompression) {
+						width = settings?.imageCompressionSize?.width ?? null;
+						height = settings?.imageCompressionSize?.height ?? null;
+					}
 
 
-						if (width || height) {
-							imageUrl = await compressImage(imageUrl, width, height);
-						}
+					// Apply config limits as an upper bound if any
+					if (configWidth && (width === null || width > configWidth)) {
+						width = configWidth;
+					}
+					if (configHeight && (height === null || height > configHeight)) {
+						height = configHeight;
 					}
 					}
 
 
+					// Do the compression if required
+					if (width || height) {
+						return await compressImage(imageUrl, width, height);
+					}
+					return imageUrl;
+				};
+
+				let reader = new FileReader();
+
+				reader.onload = async (event) => {
+					let imageUrl = event.target.result;
+
+					// Compress the image if settings or config require it
+					imageUrl = await compressImageHandler(imageUrl, $settings, $config);
+
 					files = [
 					files = [
 						...files,
 						...files,
 						{
 						{
@@ -149,7 +157,11 @@
 					];
 					];
 				};
 				};
 
 
-				reader.readAsDataURL(file);
+				reader.readAsDataURL(
+					file['type'] === 'image/heic'
+						? await heic2any({ blob: file, toType: 'image/jpeg' })
+						: file
+				);
 			} else {
 			} else {
 				uploadFileHandler(file);
 				uploadFileHandler(file);
 			}
 			}

+ 11 - 10
src/lib/components/chat/Chat.svelte

@@ -55,10 +55,7 @@
 
 
 	import { generateChatCompletion } from '$lib/apis/ollama';
 	import { generateChatCompletion } from '$lib/apis/ollama';
 	import {
 	import {
-		addTagById,
 		createNewChat,
 		createNewChat,
-		deleteTagById,
-		deleteTagsById,
 		getAllTags,
 		getAllTags,
 		getChatById,
 		getChatById,
 		getChatList,
 		getChatList,
@@ -153,10 +150,10 @@
 			webSearchEnabled = false;
 			webSearchEnabled = false;
 			imageGenerationEnabled = false;
 			imageGenerationEnabled = false;
 
 
-			if (localStorage.getItem(`chat-input${chatIdProp ? `-${chatIdProp}` : ''}`)) {
+			if (sessionStorage.getItem(`chat-input${chatIdProp ? `-${chatIdProp}` : ''}`)) {
 				try {
 				try {
 					const input = JSON.parse(
 					const input = JSON.parse(
-						localStorage.getItem(`chat-input${chatIdProp ? `-${chatIdProp}` : ''}`)
+						sessionStorage.getItem(`chat-input${chatIdProp ? `-${chatIdProp}` : ''}`)
 					);
 					);
 
 
 					if (!$temporaryChatEnabled) {
 					if (!$temporaryChatEnabled) {
@@ -446,7 +443,7 @@
 			}
 			}
 		});
 		});
 
 
-		if (localStorage.getItem(`chat-input${chatIdProp ? `-${chatIdProp}` : ''}`)) {
+		if (sessionStorage.getItem(`chat-input${chatIdProp ? `-${chatIdProp}` : ''}`)) {
 			prompt = '';
 			prompt = '';
 			files = [];
 			files = [];
 			selectedToolIds = [];
 			selectedToolIds = [];
@@ -457,7 +454,7 @@
 
 
 			try {
 			try {
 				const input = JSON.parse(
 				const input = JSON.parse(
-					localStorage.getItem(`chat-input${chatIdProp ? `-${chatIdProp}` : ''}`)
+					sessionStorage.getItem(`chat-input${chatIdProp ? `-${chatIdProp}` : ''}`)
 				);
 				);
 
 
 				if (!$temporaryChatEnabled) {
 				if (!$temporaryChatEnabled) {
@@ -708,6 +705,10 @@
 	//////////////////////////
 	//////////////////////////
 
 
 	const initNewChat = async () => {
 	const initNewChat = async () => {
+		if ($user?.role !== 'admin' && $user?.permissions?.chat?.temporary_enforced) {
+			await temporaryChatEnabled.set(true);
+		}
+
 		const availableModels = $models
 		const availableModels = $models
 			.filter((m) => !(m?.info?.meta?.hidden ?? false))
 			.filter((m) => !(m?.info?.meta?.hidden ?? false))
 			.map((m) => m.id);
 			.map((m) => m.id);
@@ -2120,12 +2121,12 @@
 									onChange={(input) => {
 									onChange={(input) => {
 										if (!$temporaryChatEnabled) {
 										if (!$temporaryChatEnabled) {
 											if (input.prompt !== null) {
 											if (input.prompt !== null) {
-												localStorage.setItem(
+												sessionStorage.setItem(
 													`chat-input${$chatId ? `-${$chatId}` : ''}`,
 													`chat-input${$chatId ? `-${$chatId}` : ''}`,
 													JSON.stringify(input)
 													JSON.stringify(input)
 												);
 												);
 											} else {
 											} else {
-												localStorage.removeItem(`chat-input${$chatId ? `-${$chatId}` : ''}`);
+												sessionStorage.removeItem(`chat-input${$chatId ? `-${$chatId}` : ''}`);
 											}
 											}
 										}
 										}
 									}}
 									}}
@@ -2227,7 +2228,7 @@
 	{:else if loading}
 	{:else if loading}
 		<div class=" flex items-center justify-center h-full w-full">
 		<div class=" flex items-center justify-center h-full w-full">
 			<div class="m-auto">
 			<div class="m-auto">
-				<Spinner />
+				<Spinner className="size-5" />
 			</div>
 			</div>
 		</div>
 		</div>
 	{/if}
 	{/if}

+ 7 - 3
src/lib/components/chat/ChatPlaceholder.svelte

@@ -46,7 +46,9 @@
 					>
 					>
 						<Tooltip
 						<Tooltip
 							content={marked.parse(
 							content={marked.parse(
-								sanitizeResponseContent(models[selectedModelIdx]?.info?.meta?.description ?? '')
+								sanitizeResponseContent(
+									models[selectedModelIdx]?.info?.meta?.description ?? ''
+								).replaceAll('\n', '<br>')
 							)}
 							)}
 							placement="right"
 							placement="right"
 						>
 						>
@@ -68,7 +70,7 @@
 
 
 		{#if $temporaryChatEnabled}
 		{#if $temporaryChatEnabled}
 			<Tooltip
 			<Tooltip
-				content={$i18n.t('This chat won’t appear in history and your messages will not be saved.')}
+				content={$i18n.t("This chat won't appear in history and your messages will not be saved.")}
 				className="w-full flex justify-start mb-0.5"
 				className="w-full flex justify-start mb-0.5"
 				placement="top"
 				placement="top"
 			>
 			>
@@ -96,7 +98,9 @@
 							class="mt-0.5 text-base font-normal text-gray-500 dark:text-gray-400 line-clamp-3 markdown"
 							class="mt-0.5 text-base font-normal text-gray-500 dark:text-gray-400 line-clamp-3 markdown"
 						>
 						>
 							{@html marked.parse(
 							{@html marked.parse(
-								sanitizeResponseContent(models[selectedModelIdx]?.info?.meta?.description)
+								sanitizeResponseContent(
+									models[selectedModelIdx]?.info?.meta?.description
+								).replaceAll('\n', '<br>')
 							)}
 							)}
 						</div>
 						</div>
 						{#if models[selectedModelIdx]?.info?.meta?.user}
 						{#if models[selectedModelIdx]?.info?.meta?.user}

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

@@ -9,7 +9,7 @@
 	import FileItem from '$lib/components/common/FileItem.svelte';
 	import FileItem from '$lib/components/common/FileItem.svelte';
 	import Collapsible from '$lib/components/common/Collapsible.svelte';
 	import Collapsible from '$lib/components/common/Collapsible.svelte';
 
 
-	import { user } from '$lib/stores';
+	import { user, settings } from '$lib/stores';
 	export let models = [];
 	export let models = [];
 	export let chatFiles = [];
 	export let chatFiles = [];
 	export let params = {};
 	export let params = {};
@@ -74,7 +74,9 @@
 				<div class="" slot="content">
 				<div class="" slot="content">
 					<textarea
 					<textarea
 						bind:value={params.system}
 						bind:value={params.system}
-						class="w-full text-xs py-1.5 bg-transparent outline-hidden resize-none"
+						class="w-full text-xs outline-hidden resize-vertical {$settings.highContrastMode
+							? 'border-2 border-gray-300 dark:border-gray-700 rounded-lg bg-gray-50 dark:bg-gray-800 p-2.5'
+							: 'py-1.5 bg-transparent'}"
 						rows="4"
 						rows="4"
 						placeholder={$i18n.t('Enter system prompt')}
 						placeholder={$i18n.t('Enter system prompt')}
 					/>
 					/>

+ 61 - 38
src/lib/components/chat/MessageInput.svelte

@@ -1,6 +1,7 @@
 <script lang="ts">
 <script lang="ts">
 	import DOMPurify from 'dompurify';
 	import DOMPurify from 'dompurify';
 	import { marked } from 'marked';
 	import { marked } from 'marked';
+	import heic2any from 'heic2any';
 
 
 	import { toast } from 'svelte-sonner';
 	import { toast } from 'svelte-sonner';
 
 
@@ -320,7 +321,7 @@
 			return;
 			return;
 		}
 		}
 
 
-		inputFiles.forEach((file) => {
+		inputFiles.forEach(async (file) => {
 			console.log('Processing file:', {
 			console.log('Processing file:', {
 				name: file.name,
 				name: file.name,
 				type: file.type,
 				type: file.type,
@@ -344,47 +345,54 @@
 				return;
 				return;
 			}
 			}
 
 
-			if (
-				['image/gif', 'image/webp', 'image/jpeg', 'image/png', 'image/avif'].includes(file['type'])
-			) {
+			if (file['type'].startsWith('image/')) {
 				if (visionCapableModels.length === 0) {
 				if (visionCapableModels.length === 0) {
 					toast.error($i18n.t('Selected model(s) do not support image inputs'));
 					toast.error($i18n.t('Selected model(s) do not support image inputs'));
 					return;
 					return;
 				}
 				}
-				let reader = new FileReader();
-				reader.onload = async (event) => {
-					let imageUrl = event.target.result;
 
 
-					if (
-						($settings?.imageCompression ?? false) ||
-						($config?.file?.image_compression?.width ?? null) ||
-						($config?.file?.image_compression?.height ?? null)
-					) {
-						let width = null;
-						let height = null;
-
-						if ($settings?.imageCompression ?? false) {
-							width = $settings?.imageCompressionSize?.width ?? null;
-							height = $settings?.imageCompressionSize?.height ?? null;
-						}
+				const compressImageHandler = async (imageUrl, settings = {}, config = {}) => {
+					// Quick shortcut so we don’t do unnecessary work.
+					const settingsCompression = settings?.imageCompression ?? false;
+					const configWidth = config?.file?.image_compression?.width ?? null;
+					const configHeight = config?.file?.image_compression?.height ?? null;
 
 
-						if (
-							($config?.file?.image_compression?.width ?? null) ||
-							($config?.file?.image_compression?.height ?? null)
-						) {
-							if (width > ($config?.file?.image_compression?.width ?? null)) {
-								width = $config?.file?.image_compression?.width ?? null;
-							}
-							if (height > ($config?.file?.image_compression?.height ?? null)) {
-								height = $config?.file?.image_compression?.height ?? null;
-							}
-						}
+					// If neither settings nor config wants compression, return original URL.
+					if (!settingsCompression && !configWidth && !configHeight) {
+						return imageUrl;
+					}
 
 
-						if (width || height) {
-							imageUrl = await compressImage(imageUrl, width, height);
-						}
+					// Default to null (no compression unless set)
+					let width = null;
+					let height = null;
+
+					// If user/settings want compression, pick their preferred size.
+					if (settingsCompression) {
+						width = settings?.imageCompressionSize?.width ?? null;
+						height = settings?.imageCompressionSize?.height ?? null;
 					}
 					}
 
 
+					// Apply config limits as an upper bound if any
+					if (configWidth && (width === null || width > configWidth)) {
+						width = configWidth;
+					}
+					if (configHeight && (height === null || height > configHeight)) {
+						height = configHeight;
+					}
+
+					// Do the compression if required
+					if (width || height) {
+						return await compressImage(imageUrl, width, height);
+					}
+					return imageUrl;
+				};
+
+				let reader = new FileReader();
+				reader.onload = async (event) => {
+					let imageUrl = event.target.result;
+
+					imageUrl = await compressImageHandler(imageUrl, $settings, $config);
+
 					files = [
 					files = [
 						...files,
 						...files,
 						{
 						{
@@ -393,7 +401,11 @@
 						}
 						}
 					];
 					];
 				};
 				};
-				reader.readAsDataURL(file);
+				reader.readAsDataURL(
+					file['type'] === 'image/heic'
+						? await heic2any({ blob: file, toType: 'image/jpeg' })
+						: file
+				);
 			} else {
 			} else {
 				uploadFileHandler(file);
 				uploadFileHandler(file);
 			}
 			}
@@ -659,7 +671,7 @@
 													<div class="relative flex items-center">
 													<div class="relative flex items-center">
 														<Image
 														<Image
 															src={file.url}
 															src={file.url}
-															alt="input"
+															alt=""
 															imageClassName=" size-14 rounded-xl object-cover"
 															imageClassName=" size-14 rounded-xl object-cover"
 														/>
 														/>
 														{#if atSelectedModel ? visionCapableModels.length === 0 : selectedModels.length !== visionCapableModels.length}
 														{#if atSelectedModel ? visionCapableModels.length === 0 : selectedModels.length !== visionCapableModels.length}
@@ -677,6 +689,7 @@
 																	xmlns="http://www.w3.org/2000/svg"
 																	xmlns="http://www.w3.org/2000/svg"
 																	viewBox="0 0 24 24"
 																	viewBox="0 0 24 24"
 																	fill="currentColor"
 																	fill="currentColor"
+																	aria-hidden="true"
 																	class="size-4 fill-yellow-300"
 																	class="size-4 fill-yellow-300"
 																>
 																>
 																	<path
 																	<path
@@ -690,8 +703,12 @@
 													</div>
 													</div>
 													<div class=" absolute -top-1 -right-1">
 													<div class=" absolute -top-1 -right-1">
 														<button
 														<button
-															class=" bg-white text-black border border-white rounded-full group-hover:visible invisible transition"
+															class=" bg-white text-black border border-white rounded-full {($settings?.highContrastMode ??
+															false)
+																? ''
+																: 'outline-hidden focus:outline-hidden group-hover:visible invisible transition'}"
 															type="button"
 															type="button"
+															aria-label={$i18n.t('Remove file')}
 															on:click={() => {
 															on:click={() => {
 																files.splice(fileIdx, 1);
 																files.splice(fileIdx, 1);
 																files = files;
 																files = files;
@@ -701,6 +718,7 @@
 																xmlns="http://www.w3.org/2000/svg"
 																xmlns="http://www.w3.org/2000/svg"
 																viewBox="0 0 20 20"
 																viewBox="0 0 20 20"
 																fill="currentColor"
 																fill="currentColor"
+																aria-hidden="true"
 																class="size-4"
 																class="size-4"
 															>
 															>
 																<path
 																<path
@@ -1253,11 +1271,12 @@
 											<button
 											<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-hidden focus:outline-hidden"
 												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"
 												type="button"
-												aria-label="More"
+												aria-label={$i18n.t('More Available Tools')}
 											>
 											>
 												<svg
 												<svg
 													xmlns="http://www.w3.org/2000/svg"
 													xmlns="http://www.w3.org/2000/svg"
 													viewBox="0 0 20 20"
 													viewBox="0 0 20 20"
+													aria-hidden="true"
 													fill="currentColor"
 													fill="currentColor"
 													class="size-5"
 													class="size-5"
 												>
 												>
@@ -1379,6 +1398,10 @@
 												{#if showCodeInterpreterButton}
 												{#if showCodeInterpreterButton}
 													<Tooltip content={$i18n.t('Execute code for analysis')} placement="top">
 													<Tooltip content={$i18n.t('Execute code for analysis')} placement="top">
 														<button
 														<button
+															aria-label={codeInterpreterEnabled
+																? $i18n.t('Disable Code Interpreter')
+																: $i18n.t('Enable Code Interpreter')}
+															aria-pressed={codeInterpreterEnabled}
 															on:click|preventDefault={() =>
 															on:click|preventDefault={() =>
 																(codeInterpreterEnabled = !codeInterpreterEnabled)}
 																(codeInterpreterEnabled = !codeInterpreterEnabled)}
 															type="button"
 															type="button"
@@ -1530,7 +1553,7 @@
 																);
 																);
 															}
 															}
 														}}
 														}}
-														aria-label="Call"
+														aria-label={$i18n.t('Voice mode')}
 													>
 													>
 														<Headphone className="size-5" />
 														<Headphone className="size-5" />
 													</button>
 													</button>

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

@@ -854,6 +854,7 @@
 				</button>
 				</button>
 			{:else}
 			{:else}
 				<div class="relative flex video-container w-full max-h-full pt-2 pb-4 md:py-6 px-2 h-full">
 				<div class="relative flex video-container w-full max-h-full pt-2 pb-4 md:py-6 px-2 h-full">
+					<!-- svelte-ignore a11y-media-has-caption -->
 					<video
 					<video
 						id="camera-feed"
 						id="camera-feed"
 						autoplay
 						autoplay

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

@@ -6,7 +6,7 @@
 	import relativeTime from 'dayjs/plugin/relativeTime';
 	import relativeTime from 'dayjs/plugin/relativeTime';
 	dayjs.extend(relativeTime);
 	dayjs.extend(relativeTime);
 
 
-	import { createEventDispatcher, tick, getContext, onMount } from 'svelte';
+	import { createEventDispatcher, tick, getContext, onMount, onDestroy } from 'svelte';
 	import { removeLastWordFromString, isValidHttpUrl } from '$lib/utils';
 	import { removeLastWordFromString, isValidHttpUrl } from '$lib/utils';
 	import { knowledge } from '$lib/stores';
 	import { knowledge } from '$lib/stores';
 
 
@@ -42,6 +42,24 @@
 		selectedIdx = Math.min(selectedIdx + 1, filteredItems.length - 1);
 		selectedIdx = Math.min(selectedIdx + 1, filteredItems.length - 1);
 	};
 	};
 
 
+	let container;
+	let adjustHeightDebounce;
+
+	const adjustHeight = () => {
+		if (container) {
+			if (adjustHeightDebounce) {
+				clearTimeout(adjustHeightDebounce);
+			}
+
+			adjustHeightDebounce = setTimeout(() => {
+				if (!container) return;
+
+				// Ensure the container is visible before adjusting height
+				const rect = container.getBoundingClientRect();
+				container.style.maxHeight = Math.max(Math.min(240, rect.bottom - 100), 100) + 'px';
+			}, 100);
+		}
+	};
 	const confirmSelect = async (item) => {
 	const confirmSelect = async (item) => {
 		dispatch('select', item);
 		dispatch('select', item);
 
 
@@ -75,7 +93,18 @@
 		await tick();
 		await tick();
 	};
 	};
 
 
+	const decodeString = (str: string) => {
+		try {
+			return decodeURIComponent(str);
+		} catch (e) {
+			return str;
+		}
+	};
+
 	onMount(() => {
 	onMount(() => {
+		window.addEventListener('resize', adjustHeight);
+		adjustHeight();
+
 		let legacy_documents = $knowledge
 		let legacy_documents = $knowledge
 			.filter((item) => item?.meta?.document)
 			.filter((item) => item?.meta?.document)
 			.map((item) => ({
 			.map((item) => ({
@@ -155,13 +184,9 @@
 		});
 		});
 	});
 	});
 
 
-	const decodeString = (str: string) => {
-		try {
-			return decodeURIComponent(str);
-		} catch (e) {
-			return str;
-		}
-	};
+	onDestroy(() => {
+		window.removeEventListener('resize', adjustHeight);
+	});
 </script>
 </script>
 
 
 {#if filteredItems.length > 0 || prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
 {#if filteredItems.length > 0 || prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
@@ -174,6 +199,7 @@
 				<div
 				<div
 					class="m-1 overflow-y-auto p-1 rounded-r-xl space-y-0.5 scrollbar-hidden max-h-60"
 					class="m-1 overflow-y-auto p-1 rounded-r-xl space-y-0.5 scrollbar-hidden max-h-60"
 					id="command-options-container"
 					id="command-options-container"
+					bind:this={container}
 				>
 				>
 					{#each filteredItems as item, idx}
 					{#each filteredItems as item, idx}
 						<button
 						<button

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

@@ -1,7 +1,7 @@
 <script lang="ts">
 <script lang="ts">
 	import Fuse from 'fuse.js';
 	import Fuse from 'fuse.js';
 
 
-	import { createEventDispatcher, onMount } from 'svelte';
+	import { createEventDispatcher, onDestroy, onMount } from 'svelte';
 	import { tick, getContext } from 'svelte';
 	import { tick, getContext } from 'svelte';
 
 
 	import { models } from '$lib/stores';
 	import { models } from '$lib/stores';
@@ -51,18 +51,44 @@
 		selectedIdx = Math.min(selectedIdx + 1, filteredItems.length - 1);
 		selectedIdx = Math.min(selectedIdx + 1, filteredItems.length - 1);
 	};
 	};
 
 
+	let container;
+	let adjustHeightDebounce;
+
+	const adjustHeight = () => {
+		if (container) {
+			if (adjustHeightDebounce) {
+				clearTimeout(adjustHeightDebounce);
+			}
+
+			adjustHeightDebounce = setTimeout(() => {
+				if (!container) return;
+
+				// Ensure the container is visible before adjusting height
+				const rect = container.getBoundingClientRect();
+				container.style.maxHeight = Math.max(Math.min(240, rect.bottom - 100), 100) + 'px';
+			}, 100);
+		}
+	};
+
 	const confirmSelect = async (model) => {
 	const confirmSelect = async (model) => {
 		command = '';
 		command = '';
 		dispatch('select', model);
 		dispatch('select', model);
 	};
 	};
 
 
 	onMount(async () => {
 	onMount(async () => {
+		window.addEventListener('resize', adjustHeight);
+		adjustHeight();
+
 		await tick();
 		await tick();
 		const chatInputElement = document.getElementById('chat-input');
 		const chatInputElement = document.getElementById('chat-input');
 		await tick();
 		await tick();
 		chatInputElement?.focus();
 		chatInputElement?.focus();
 		await tick();
 		await tick();
 	});
 	});
+
+	onDestroy(() => {
+		window.removeEventListener('resize', adjustHeight);
+	});
 </script>
 </script>
 
 
 {#if filteredItems.length > 0}
 {#if filteredItems.length > 0}
@@ -75,6 +101,7 @@
 				<div
 				<div
 					class="m-1 overflow-y-auto p-1 rounded-r-lg space-y-0.5 scrollbar-hidden max-h-60"
 					class="m-1 overflow-y-auto p-1 rounded-r-lg space-y-0.5 scrollbar-hidden max-h-60"
 					id="command-options-container"
 					id="command-options-container"
+					bind:this={container}
 				>
 				>
 					{#each filteredItems as model, modelIdx}
 					{#each filteredItems as model, modelIdx}
 						<button
 						<button

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

@@ -9,7 +9,7 @@
 		getUserTimezone,
 		getUserTimezone,
 		getWeekday
 		getWeekday
 	} from '$lib/utils';
 	} from '$lib/utils';
-	import { tick, getContext } from 'svelte';
+	import { tick, getContext, onMount, onDestroy } from 'svelte';
 	import { toast } from 'svelte-sonner';
 	import { toast } from 'svelte-sonner';
 
 
 	const i18n = getContext('i18n');
 	const i18n = getContext('i18n');
@@ -38,6 +38,25 @@
 		selectedPromptIdx = Math.min(selectedPromptIdx + 1, filteredPrompts.length - 1);
 		selectedPromptIdx = Math.min(selectedPromptIdx + 1, filteredPrompts.length - 1);
 	};
 	};
 
 
+	let container;
+	let adjustHeightDebounce;
+
+	const adjustHeight = () => {
+		if (container) {
+			if (adjustHeightDebounce) {
+				clearTimeout(adjustHeightDebounce);
+			}
+
+			adjustHeightDebounce = setTimeout(() => {
+				if (!container) return;
+
+				// Ensure the container is visible before adjusting height
+				const rect = container.getBoundingClientRect();
+				container.style.maxHeight = Math.max(Math.min(240, rect.bottom - 100), 100) + 'px';
+			}, 100);
+		}
+	};
+
 	const confirmPrompt = async (command) => {
 	const confirmPrompt = async (command) => {
 		let text = command.content;
 		let text = command.content;
 
 
@@ -172,6 +191,15 @@
 			}
 			}
 		}
 		}
 	};
 	};
+
+	onMount(() => {
+		window.addEventListener('resize', adjustHeight);
+		adjustHeight();
+	});
+
+	onDestroy(() => {
+		window.removeEventListener('resize', adjustHeight);
+	});
 </script>
 </script>
 
 
 {#if filteredPrompts.length > 0}
 {#if filteredPrompts.length > 0}
@@ -184,6 +212,7 @@
 				<div
 				<div
 					class="m-1 overflow-y-auto p-1 space-y-0.5 scrollbar-hidden max-h-60"
 					class="m-1 overflow-y-auto p-1 space-y-0.5 scrollbar-hidden max-h-60"
 					id="command-options-container"
 					id="command-options-container"
+					bind:this={container}
 				>
 				>
 					{#each filteredPrompts as prompt, promptIdx}
 					{#each filteredPrompts as prompt, promptIdx}
 						<button
 						<button

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

@@ -5,6 +5,7 @@
 	import { blobToFile, calculateSHA256, extractCurlyBraceWords } from '$lib/utils';
 	import { blobToFile, calculateSHA256, extractCurlyBraceWords } from '$lib/utils';
 
 
 	import { transcribeAudio } from '$lib/apis/audio';
 	import { transcribeAudio } from '$lib/apis/audio';
+	import XMark from '$lib/components/icons/XMark.svelte';
 
 
 	import dayjs from 'dayjs';
 	import dayjs from 'dayjs';
 	import LocalizedFormat from 'dayjs/plugin/localizedFormat';
 	import LocalizedFormat from 'dayjs/plugin/localizedFormat';
@@ -406,16 +407,7 @@
 				onCancel();
 				onCancel();
 			}}
 			}}
 		>
 		>
-			<svg
-				xmlns="http://www.w3.org/2000/svg"
-				fill="none"
-				viewBox="0 0 24 24"
-				stroke-width="3"
-				stroke="currentColor"
-				class="size-4"
-			>
-				<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
-			</svg>
+			<XMark className={'size-4'} />
 		</button>
 		</button>
 	</div>
 	</div>
 
 

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

@@ -438,6 +438,7 @@
 						<Message
 						<Message
 							{chatId}
 							{chatId}
 							bind:history
 							bind:history
+							{selectedModels}
 							messageId={message.id}
 							messageId={message.id}
 							idx={messageIdx}
 							idx={messageIdx}
 							{user}
 							{user}

+ 0 - 1
src/lib/components/chat/Messages/Citations.svelte

@@ -43,7 +43,6 @@
 	}
 	}
 
 
 	$: {
 	$: {
-		console.log('sources', sources);
 		citations = sources.reduce((acc, source) => {
 		citations = sources.reduce((acc, source) => {
 			if (Object.keys(source).length === 0) {
 			if (Object.keys(source).length === 0) {
 				return acc;
 				return acc;

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

@@ -4,6 +4,8 @@
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
 	import { WEBUI_API_BASE_URL } from '$lib/constants';
 	import { WEBUI_API_BASE_URL } from '$lib/constants';
 
 
+	import XMark from '$lib/components/icons/XMark.svelte';
+
 	const i18n = getContext('i18n');
 	const i18n = getContext('i18n');
 
 
 	export let show = false;
 	export let show = false;
@@ -67,16 +69,7 @@
 					show = false;
 					show = false;
 				}}
 				}}
 			>
 			>
-				<svg
-					xmlns="http://www.w3.org/2000/svg"
-					viewBox="0 0 20 20"
-					fill="currentColor"
-					class="w-5 h-5"
-				>
-					<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>
+				<XMark className={'size-5'} />
 			</button>
 			</button>
 		</div>
 		</div>
 
 

+ 2 - 10
src/lib/components/chat/Messages/CodeExecutionModal.svelte

@@ -4,6 +4,7 @@
 	import Modal from '$lib/components/common/Modal.svelte';
 	import Modal from '$lib/components/common/Modal.svelte';
 	import Spinner from '$lib/components/common/Spinner.svelte';
 	import Spinner from '$lib/components/common/Spinner.svelte';
 	import Badge from '$lib/components/common/Badge.svelte';
 	import Badge from '$lib/components/common/Badge.svelte';
+	import XMark from '$lib/components/icons/XMark.svelte';
 	const i18n = getContext('i18n');
 	const i18n = getContext('i18n');
 
 
 	export let show = false;
 	export let show = false;
@@ -49,16 +50,7 @@
 					codeExecution = null;
 					codeExecution = null;
 				}}
 				}}
 			>
 			>
-				<svg
-					xmlns="http://www.w3.org/2000/svg"
-					viewBox="0 0 20 20"
-					fill="currentColor"
-					class="w-5 h-5"
-				>
-					<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>
+				<XMark className={'size-5'} />
 			</button>
 			</button>
 		</div>
 		</div>
 
 

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

@@ -18,6 +18,8 @@
 	export let id;
 	export let id;
 	export let content;
 	export let content;
 	export let history;
 	export let history;
+	export let selectedModels = [];
+
 	export let model = null;
 	export let model = null;
 	export let sources = null;
 	export let sources = null;
 
 
@@ -25,11 +27,10 @@
 	export let preview = false;
 	export let preview = false;
 	export let floatingButtons = true;
 	export let floatingButtons = true;
 
 
-	export let onSave = () => {};
-	export let onSourceClick = () => {};
-	export let onTaskClick = () => {};
-
-	export let onAddMessages = () => {};
+	export let onSave = (e) => {};
+	export let onSourceClick = (e) => {};
+	export let onTaskClick = (e) => {};
+	export let onAddMessages = (e) => {};
 
 
 	let contentContainerElement;
 	let contentContainerElement;
 
 
@@ -192,7 +193,11 @@
 	<FloatingButtons
 	<FloatingButtons
 		bind:this={floatingButtonsElement}
 		bind:this={floatingButtonsElement}
 		{id}
 		{id}
-		model={model?.id}
+		model={(selectedModels ?? []).includes(model?.id)
+			? model?.id
+			: (selectedModels ?? []).length > 0
+				? selectedModels.at(0)
+				: model?.id}
 		messages={createMessagesList(history, id)}
 		messages={createMessagesList(history, id)}
 		onAdd={({ modelId, parentId, messages }) => {
 		onAdd={({ modelId, parentId, messages }) => {
 			console.log(modelId, parentId, messages);
 			console.log(modelId, parentId, messages);

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

@@ -27,7 +27,8 @@
 	let tokens = [];
 	let tokens = [];
 
 
 	const options = {
 	const options = {
-		throwOnError: false
+		throwOnError: false,
+		breaks: true
 	};
 	};
 
 
 	marked.use(markedKatexExtension(options));
 	marked.use(markedKatexExtension(options));

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

@@ -13,6 +13,7 @@
 	import UserMessage from './UserMessage.svelte';
 	import UserMessage from './UserMessage.svelte';
 
 
 	export let chatId;
 	export let chatId;
+	export let selectedModels = [];
 	export let idx = 0;
 	export let idx = 0;
 
 
 	export let history;
 	export let history;
@@ -70,6 +71,7 @@
 				{chatId}
 				{chatId}
 				{history}
 				{history}
 				{messageId}
 				{messageId}
+				{selectedModels}
 				isLastMessage={messageId === history.currentId}
 				isLastMessage={messageId === history.currentId}
 				siblings={history.messages[history.messages[messageId].parentId]?.childrenIds ?? []}
 				siblings={history.messages[history.messages[messageId].parentId]?.childrenIds ?? []}
 				{gotoMessage}
 				{gotoMessage}
@@ -92,6 +94,7 @@
 				bind:history
 				bind:history
 				{chatId}
 				{chatId}
 				{messageId}
 				{messageId}
+				{selectedModels}
 				isLastMessage={messageId === history?.currentId}
 				isLastMessage={messageId === history?.currentId}
 				{updateChat}
 				{updateChat}
 				{editMessage}
 				{editMessage}

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

@@ -23,6 +23,7 @@
 	export let chatId;
 	export let chatId;
 	export let history;
 	export let history;
 	export let messageId;
 	export let messageId;
+	export let selectedModels = [];
 
 
 	export let isLastMessage;
 	export let isLastMessage;
 	export let readOnly = false;
 	export let readOnly = false;
@@ -252,6 +253,7 @@
 									{chatId}
 									{chatId}
 									{history}
 									{history}
 									messageId={_messageId}
 									messageId={_messageId}
+									{selectedModels}
 									isLastMessage={true}
 									isLastMessage={true}
 									siblings={groupedMessageIds[modelIdx].messageIds}
 									siblings={groupedMessageIds[modelIdx].messageIds}
 									gotoMessage={(message, messageIdx) => gotoMessage(modelIdx, messageIdx)}
 									gotoMessage={(message, messageIdx) => gotoMessage(modelIdx, messageIdx)}

+ 2 - 10
src/lib/components/chat/Messages/RateComment.svelte

@@ -4,6 +4,7 @@
 	import { createEventDispatcher, onMount, getContext } from 'svelte';
 	import { createEventDispatcher, onMount, getContext } from 'svelte';
 	import { config, models } from '$lib/stores';
 	import { config, models } from '$lib/stores';
 	import Tags from '$lib/components/common/Tags.svelte';
 	import Tags from '$lib/components/common/Tags.svelte';
+	import XMark from '$lib/components/icons/XMark.svelte';
 
 
 	const i18n = getContext('i18n');
 	const i18n = getContext('i18n');
 
 
@@ -123,16 +124,7 @@
 				show = false;
 				show = false;
 			}}
 			}}
 		>
 		>
-			<svg
-				xmlns="http://www.w3.org/2000/svg"
-				fill="none"
-				viewBox="0 0 24 24"
-				stroke-width="1.5"
-				stroke="currentColor"
-				class="size-4"
-			>
-				<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
-			</svg>
+			<XMark className={'size-4'} />
 		</button>
 		</button>
 	</div>
 	</div>
 
 

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

@@ -106,6 +106,7 @@
 	export let chatId = '';
 	export let chatId = '';
 	export let history;
 	export let history;
 	export let messageId;
 	export let messageId;
+	export let selectedModels = [];
 
 
 	let message: MessageType = JSON.parse(JSON.stringify(history.messages[messageId]));
 	let message: MessageType = JSON.parse(JSON.stringify(history.messages[messageId]));
 	$: if (history.messages) {
 	$: if (history.messages) {
@@ -601,7 +602,7 @@
 		id="message-{message.id}"
 		id="message-{message.id}"
 		dir={$settings.chatDirection}
 		dir={$settings.chatDirection}
 	>
 	>
-		<div class={`shrink-0 ltr:mr-3 rtl:ml-3 hidden @lg:flex `}>
+		<div class={`shrink-0 ltr:mr-3 rtl:ml-3 hidden @lg:flex mt-1 `}>
 			<ProfileImage
 			<ProfileImage
 				src={model?.info?.meta?.profile_image_url ??
 				src={model?.info?.meta?.profile_image_url ??
 					($i18n.language === 'dg-DG' ? `/doge.png` : `${WEBUI_BASE_URL}/static/favicon.png`)}
 					($i18n.language === 'dg-DG' ? `/doge.png` : `${WEBUI_BASE_URL}/static/favicon.png`)}
@@ -609,7 +610,7 @@
 			/>
 			/>
 		</div>
 		</div>
 
 
-		<div class="flex-auto w-0 pl-1 relative -translate-y-0.5">
+		<div class="flex-auto w-0 pl-1 relative">
 			<Name>
 			<Name>
 				<Tooltip content={model?.name ?? message.model} placement="top-start">
 				<Tooltip content={model?.name ?? message.model} placement="top-start">
 					<span class="line-clamp-1 text-black dark:text-white">
 					<span class="line-clamp-1 text-black dark:text-white">
@@ -795,6 +796,7 @@
 									<ContentRenderer
 									<ContentRenderer
 										id={message.id}
 										id={message.id}
 										{history}
 										{history}
+										{selectedModels}
 										content={message.content}
 										content={message.content}
 										sources={message.sources}
 										sources={message.sources}
 										floatingButtons={message?.done && !readOnly}
 										floatingButtons={message?.done && !readOnly}

+ 2 - 2
src/lib/components/chat/Messages/ResponseMessage/WebSearchResults.svelte

@@ -1,7 +1,7 @@
 <script lang="ts">
 <script lang="ts">
 	import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
 	import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
 	import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
 	import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
-	import MagnifyingGlass from '$lib/components/icons/MagnifyingGlass.svelte';
+	import Search from '$lib/components/icons/Search.svelte';
 	import Collapsible from '$lib/components/common/Collapsible.svelte';
 	import Collapsible from '$lib/components/common/Collapsible.svelte';
 
 
 	export let status = { urls: [], query: '' };
 	export let status = { urls: [], query: '' };
@@ -31,7 +31,7 @@
 				class="flex w-full items-center p-3 px-4 border-b border-gray-300/30 dark:border-gray-700/50 group/item justify-between font-normal text-gray-800 dark:text-gray-300 no-underline"
 				class="flex w-full items-center p-3 px-4 border-b border-gray-300/30 dark:border-gray-700/50 group/item justify-between font-normal text-gray-800 dark:text-gray-300 no-underline"
 			>
 			>
 				<div class="flex gap-2 items-center">
 				<div class="flex gap-2 items-center">
-					<MagnifyingGlass />
+					<Search />
 
 
 					<div class=" line-clamp-1">
 					<div class=" line-clamp-1">
 						{status.query}
 						{status.query}

+ 24 - 39
src/lib/components/chat/Messages/Skeleton.svelte

@@ -2,44 +2,29 @@
 	export let size = 'md';
 	export let size = 'md';
 </script>
 </script>
 
 
-<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-sm mr-14"
-			/>
+<span class="relative flex {size === 'md' ? 'size-3 my-2' : 'size-2 my-1'} mx-1">
+	<span
+		class="absolute inline-flex h-full w-full animate-pulse rounded-full bg-gray-700 dark:bg-gray-200 opacity-75"
+	></span>
+	<span
+		class="relative inline-flex {size === 'md'
+			? 'size-3'
+			: 'size-2'} rounded-full bg-black dark:bg-white animate-size"
+	></span>
+</span>
 
 
-			<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-sm col-span-2"
-				/>
-				<div
-					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-sm col-span-1"
-				/>
-				<div
-					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-sm col-span-1 mr-4"
-				/>
-			</div>
+<style>
+	@keyframes size {
+		0%,
+		100% {
+			transform: scale(1);
+		}
+		50% {
+			transform: scale(1.25);
+		}
+	}
 
 
-			<div class="{size === 'md' ? 'h-2' : 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded-sm" />
-		</div>
-	</div>
-</div>
+	.animate-size {
+		animation: size 1.5s ease-in-out infinite;
+	}
+</style>

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

@@ -113,7 +113,7 @@
 	id="message-{message.id}"
 	id="message-{message.id}"
 >
 >
 	{#if !($settings?.chatBubble ?? true)}
 	{#if !($settings?.chatBubble ?? true)}
-		<div class={`shrink-0 ltr:mr-3 rtl:ml-3`}>
+		<div class={`shrink-0 ltr:mr-3 rtl:ml-3 mt-1`}>
 			<ProfileImage
 			<ProfileImage
 				src={message.user
 				src={message.user
 					? ($models.find((m) => m.id === message.user)?.info?.meta?.profile_image_url ??
 					? ($models.find((m) => m.id === message.user)?.info?.meta?.profile_image_url ??

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

@@ -49,7 +49,7 @@
 	{#each selectedModels as selectedModel, selectedModelIdx}
 	{#each selectedModels as selectedModel, selectedModelIdx}
 		<div class="flex w-full max-w-fit">
 		<div class="flex w-full max-w-fit">
 			<div class="overflow-hidden w-full">
 			<div class="overflow-hidden w-full">
-				<div class="mr-1 max-w-full">
+				<div class="max-w-full {($settings?.highContrastMode ?? false) ? 'm-1' : 'mr-1'}">
 					<Selector
 					<Selector
 						id={`${selectedModelIdx}`}
 						id={`${selectedModelIdx}`}
 						placeholder={$i18n.t('Select a model')}
 						placeholder={$i18n.t('Select a model')}

+ 11 - 28
src/lib/components/chat/ModelSelector/Selector.svelte

@@ -7,6 +7,7 @@
 	import relativeTime from 'dayjs/plugin/relativeTime';
 	import relativeTime from 'dayjs/plugin/relativeTime';
 	dayjs.extend(relativeTime);
 	dayjs.extend(relativeTime);
 
 
+	import Spinner from '$lib/components/common/Spinner.svelte';
 	import { flyAndScale } from '$lib/utils/transitions';
 	import { flyAndScale } from '$lib/utils/transitions';
 	import { createEventDispatcher, onMount, getContext, tick } from 'svelte';
 	import { createEventDispatcher, onMount, getContext, tick } from 'svelte';
 	import { goto } from '$app/navigation';
 	import { goto } from '$app/navigation';
@@ -345,12 +346,17 @@
 	closeFocus={false}
 	closeFocus={false}
 >
 >
 	<DropdownMenu.Trigger
 	<DropdownMenu.Trigger
-		class="relative w-full font-primary"
+		class="relative w-full font-primary {($settings?.highContrastMode ?? false)
+			? ''
+			: 'outline-hidden focus:outline-hidden'}"
 		aria-label={placeholder}
 		aria-label={placeholder}
 		id="model-selector-{id}-button"
 		id="model-selector-{id}-button"
 	>
 	>
-		<button
-			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"
+		<div
+			class="flex w-full text-left px-0.5 bg-transparent truncate {triggerClassName} justify-between {($settings?.highContrastMode ??
+			false)
+				? 'dark:placeholder-gray-100 placeholder-gray-800'
+				: 'placeholder-gray-400'}"
 			on:mouseenter={async () => {
 			on:mouseenter={async () => {
 				models.set(
 				models.set(
 					await getModels(
 					await getModels(
@@ -359,7 +365,6 @@
 					)
 					)
 				);
 				);
 			}}
 			}}
-			type="button"
 		>
 		>
 			{#if selectedModel}
 			{#if selectedModel}
 				{selectedModel.label}
 				{selectedModel.label}
@@ -367,7 +372,7 @@
 				{placeholder}
 				{placeholder}
 			{/if}
 			{/if}
 			<ChevronDown className=" self-center ml-2 size-3" strokeWidth="2.5" />
 			<ChevronDown className=" self-center ml-2 size-3" strokeWidth="2.5" />
-		</button>
+		</div>
 	</DropdownMenu.Trigger>
 	</DropdownMenu.Trigger>
 
 
 	<DropdownMenu.Content
 	<DropdownMenu.Content
@@ -550,29 +555,7 @@
 					>
 					>
 						<div class="flex">
 						<div class="flex">
 							<div class="-ml-2 mr-2.5 translate-y-0.5">
 							<div class="-ml-2 mr-2.5 translate-y-0.5">
-								<svg
-									class="size-4"
-									viewBox="0 0 24 24"
-									fill="currentColor"
-									xmlns="http://www.w3.org/2000/svg"
-									><style>
-										.spinner_ajPY {
-											transform-origin: center;
-											animation: spinner_AtaB 0.75s infinite linear;
-										}
-										@keyframes spinner_AtaB {
-											100% {
-												transform: rotate(360deg);
-											}
-										}
-									</style><path
-										d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
-										opacity=".25"
-									/><path
-										d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
-										class="spinner_ajPY"
-									/></svg
-								>
+								<Spinner />
 							</div>
 							</div>
 
 
 							<div class="flex flex-col self-start">
 							<div class="flex flex-col self-start">

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

@@ -173,19 +173,19 @@
 								}
 								}
 							}}
 							}}
 						>
 						>
-							<button
+							<div
 								class="select-none flex rounded-xl p-1.5 w-full hover:bg-gray-50 dark:hover:bg-gray-850 transition"
 								class="select-none flex rounded-xl p-1.5 w-full hover:bg-gray-50 dark:hover:bg-gray-850 transition"
-								aria-label="User Menu"
 							>
 							>
 								<div class=" self-center">
 								<div class=" self-center">
+									<span class="sr-only">{$i18n.t('User menu')}</span>
 									<img
 									<img
 										src={$user?.profile_image_url}
 										src={$user?.profile_image_url}
 										class="size-6 object-cover rounded-full"
 										class="size-6 object-cover rounded-full"
-										alt="User profile"
+										alt=""
 										draggable="false"
 										draggable="false"
 									/>
 									/>
 								</div>
 								</div>
-							</button>
+							</div>
 						</UserMenu>
 						</UserMenu>
 					{/if}
 					{/if}
 				</div>
 				</div>

+ 11 - 5
src/lib/components/chat/Placeholder.svelte

@@ -118,6 +118,8 @@
 								placement="top"
 								placement="top"
 							>
 							>
 								<button
 								<button
+									aria-hidden={models.length <= 1}
+									aria-label={$i18n.t('Get information on {{name}} in the UI', { name: models[modelIdx]?.name})}
 									on:click={() => {
 									on:click={() => {
 										selectedModelIdx = modelIdx;
 										selectedModelIdx = modelIdx;
 									}}
 									}}
@@ -129,7 +131,7 @@
 												? `/doge.png`
 												? `/doge.png`
 												: `${WEBUI_BASE_URL}/static/favicon.png`)}
 												: `${WEBUI_BASE_URL}/static/favicon.png`)}
 										class=" size-9 @sm:size-10 rounded-full border-[1px] border-gray-100 dark:border-none"
 										class=" size-9 @sm:size-10 rounded-full border-[1px] border-gray-100 dark:border-none"
-										alt="logo"
+										aria-hidden="true"
 										draggable="false"
 										draggable="false"
 									/>
 									/>
 								</button>
 								</button>
@@ -164,7 +166,9 @@
 						<Tooltip
 						<Tooltip
 							className=" w-fit"
 							className=" w-fit"
 							content={marked.parse(
 							content={marked.parse(
-								sanitizeResponseContent(models[selectedModelIdx]?.info?.meta?.description ?? '')
+								sanitizeResponseContent(
+									models[selectedModelIdx]?.info?.meta?.description ?? ''
+								).replaceAll('\n', '<br>')
 							)}
 							)}
 							placement="top"
 							placement="top"
 						>
 						>
@@ -172,7 +176,9 @@
 								class="mt-0.5 px-2 text-sm font-normal text-gray-500 dark:text-gray-400 line-clamp-2 max-w-xl markdown"
 								class="mt-0.5 px-2 text-sm font-normal text-gray-500 dark:text-gray-400 line-clamp-2 max-w-xl markdown"
 							>
 							>
 								{@html marked.parse(
 								{@html marked.parse(
-									sanitizeResponseContent(models[selectedModelIdx]?.info?.meta?.description)
+									sanitizeResponseContent(
+										models[selectedModelIdx]?.info?.meta?.description ?? ''
+									).replaceAll('\n', '<br>')
 								)}
 								)}
 							</div>
 							</div>
 						</Tooltip>
 						</Tooltip>
@@ -218,9 +224,9 @@
 					onChange={(input) => {
 					onChange={(input) => {
 						if (!$temporaryChatEnabled) {
 						if (!$temporaryChatEnabled) {
 							if (input.prompt !== null) {
 							if (input.prompt !== null) {
-								localStorage.setItem(`chat-input`, JSON.stringify(input));
+								sessionStorage.setItem(`chat-input`, JSON.stringify(input));
 							} else {
 							} else {
-								localStorage.removeItem(`chat-input`);
+								sessionStorage.removeItem(`chat-input`);
 							}
 							}
 						}
 						}
 					}}
 					}}

+ 13 - 9
src/lib/components/chat/Settings/About.svelte

@@ -38,7 +38,9 @@
 			return '';
 			return '';
 		});
 		});
 
 
-		checkForVersionUpdates();
+		if (!$config?.offline_mode) {
+			checkForVersionUpdates();
+		}
 	});
 	});
 </script>
 </script>
 
 
@@ -80,14 +82,16 @@
 					</button>
 					</button>
 				</div>
 				</div>
 
 
-				<button
-					class=" text-xs px-3 py-1.5 bg-gray-100 hover:bg-gray-200 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-lg font-medium"
-					on:click={() => {
-						checkForVersionUpdates();
-					}}
-				>
-					{$i18n.t('Check for updates')}
-				</button>
+				{#if $config?.offline_mode}
+					<button
+						class=" text-xs px-3 py-1.5 bg-gray-100 hover:bg-gray-200 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-lg font-medium"
+						on:click={() => {
+							checkForVersionUpdates();
+						}}
+					>
+						{$i18n.t('Check for updates')}
+					</button>
+				{/if}
 			</div>
 			</div>
 		</div>
 		</div>
 
 

+ 14 - 5
src/lib/components/chat/Settings/Chats.svelte

@@ -6,11 +6,10 @@
 
 
 	import {
 	import {
 		archiveAllChats,
 		archiveAllChats,
-		createNewChat,
 		deleteAllChats,
 		deleteAllChats,
 		getAllChats,
 		getAllChats,
-		getAllUserChats,
-		getChatList
+		getChatList,
+		importChat
 	} from '$lib/apis/chats';
 	} from '$lib/apis/chats';
 	import { getImportOrigin, convertOpenAIChats } from '$lib/utils';
 	import { getImportOrigin, convertOpenAIChats } from '$lib/utils';
 	import { onMount, getContext } from 'svelte';
 	import { onMount, getContext } from 'svelte';
@@ -58,9 +57,18 @@
 			console.log(chat);
 			console.log(chat);
 
 
 			if (chat.chat) {
 			if (chat.chat) {
-				await createNewChat(localStorage.token, chat.chat);
+				await importChat(
+					localStorage.token,
+					chat.chat,
+					chat.meta ?? {},
+					false,
+					null,
+					chat?.created_at ?? null,
+					chat?.updated_at ?? null
+				);
 			} else {
 			} else {
-				await createNewChat(localStorage.token, chat);
+				// Legacy format
+				await importChat(localStorage.token, chat, {}, false, null);
 			}
 			}
 		}
 		}
 
 
@@ -101,6 +109,7 @@
 	const handleArchivedChatsChange = async () => {
 	const handleArchivedChatsChange = async () => {
 		currentChatPage.set(1);
 		currentChatPage.set(1);
 		await chats.set(await getChatList(localStorage.token, $currentChatPage));
 		await chats.set(await getChatList(localStorage.token, $currentChatPage));
+
 		scrollPaginationEnabled.set(true);
 		scrollPaginationEnabled.set(true);
 	};
 	};
 </script>
 </script>

+ 4 - 1
src/lib/components/chat/Settings/General.svelte

@@ -274,7 +274,10 @@
 				<div class=" my-2.5 text-sm font-medium">{$i18n.t('System Prompt')}</div>
 				<div class=" my-2.5 text-sm font-medium">{$i18n.t('System Prompt')}</div>
 				<Textarea
 				<Textarea
 					bind:value={system}
 					bind:value={system}
-					className="w-full text-sm bg-white dark:text-gray-300 dark:bg-gray-900 outline-hidden resize-none"
+					className={'w-full text-sm outline-hidden resize-vertical' +
+						($settings.highContrastMode
+							? ' p-2.5 border-2 border-gray-300 dark:border-gray-700 rounded-lg bg-gray-50 dark:bg-gray-850 text-gray-900 dark:text-gray-100 focus:ring-1 focus:ring-blue-500 focus:border-blue-500 overflow-y-hidden'
+							: ' bg-white dark:text-gray-300 dark:bg-gray-900')}
 					rows="4"
 					rows="4"
 					placeholder={$i18n.t('Enter system prompt here')}
 					placeholder={$i18n.t('Enter system prompt here')}
 				/>
 				/>

+ 4 - 33
src/lib/components/chat/Settings/Personalization/AddMemoryModal.svelte

@@ -4,6 +4,8 @@
 	import Modal from '$lib/components/common/Modal.svelte';
 	import Modal from '$lib/components/common/Modal.svelte';
 	import { addNewMemory, updateMemoryById } from '$lib/apis/memories';
 	import { addNewMemory, updateMemoryById } from '$lib/apis/memories';
 	import { toast } from 'svelte-sonner';
 	import { toast } from 'svelte-sonner';
+	import Spinner from '$lib/components/common/Spinner.svelte';
+	import XMark from '$lib/components/icons/XMark.svelte';
 
 
 	const dispatch = createEventDispatcher();
 	const dispatch = createEventDispatcher();
 
 
@@ -46,16 +48,7 @@
 					show = false;
 					show = false;
 				}}
 				}}
 			>
 			>
-				<svg
-					xmlns="http://www.w3.org/2000/svg"
-					viewBox="0 0 20 20"
-					fill="currentColor"
-					class="w-5 h-5"
-				>
-					<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>
+				<XMark className={'size-5'} />
 			</button>
 			</button>
 		</div>
 		</div>
 
 
@@ -93,29 +86,7 @@
 
 
 							{#if loading}
 							{#if loading}
 								<div class="ml-2 self-center">
 								<div class="ml-2 self-center">
-									<svg
-										class=" w-4 h-4"
-										viewBox="0 0 24 24"
-										fill="currentColor"
-										xmlns="http://www.w3.org/2000/svg"
-										><style>
-											.spinner_ajPY {
-												transform-origin: center;
-												animation: spinner_AtaB 0.75s infinite linear;
-											}
-											@keyframes spinner_AtaB {
-												100% {
-													transform: rotate(360deg);
-												}
-											}
-										</style><path
-											d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
-											opacity=".25"
-										/><path
-											d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
-											class="spinner_ajPY"
-										/></svg
-									>
+									<Spinner />
 								</div>
 								</div>
 							{/if}
 							{/if}
 						</button>
 						</button>

Деякі файли не було показано, через те що забагато файлів було змінено