Michael Poluektov 10 months ago
parent
commit
f9e3c47d4a
100 changed files with 3553 additions and 1307 deletions
  1. 32 6
      .github/workflows/integration-test.yml
  2. 114 0
      backend/alembic.ini
  3. 2 1
      backend/apps/rag/main.py
  4. 80 24
      backend/apps/webui/internal/db.py
  5. 0 4
      backend/apps/webui/internal/migrations/017_add_user_oauth_sub.py
  6. 0 21
      backend/apps/webui/internal/migrations/README.md
  7. 82 2
      backend/apps/webui/main.py
  8. 63 56
      backend/apps/webui/models/auths.py
  9. 206 187
      backend/apps/webui/models/chats.py
  10. 69 62
      backend/apps/webui/models/documents.py
  11. 64 50
      backend/apps/webui/models/files.py
  12. 125 106
      backend/apps/webui/models/functions.py
  13. 92 76
      backend/apps/webui/models/memories.py
  14. 53 42
      backend/apps/webui/models/models.py
  15. 47 46
      backend/apps/webui/models/prompts.py
  16. 141 106
      backend/apps/webui/models/tags.py
  17. 74 64
      backend/apps/webui/models/tools.py
  18. 118 94
      backend/apps/webui/models/users.py
  19. 5 3
      backend/apps/webui/routers/chats.py
  20. 3 1
      backend/apps/webui/routers/documents.py
  21. 1 4
      backend/apps/webui/routers/files.py
  22. 4 1
      backend/apps/webui/routers/functions.py
  23. 3 1
      backend/apps/webui/routers/memories.py
  24. 8 2
      backend/apps/webui/routers/models.py
  25. 3 1
      backend/apps/webui/routers/prompts.py
  26. 7 3
      backend/apps/webui/routers/tools.py
  27. 4 2
      backend/apps/webui/routers/users.py
  28. 4 4
      backend/apps/webui/routers/utils.py
  29. 43 2
      backend/config.py
  30. 11 0
      backend/constants.py
  31. 100 19
      backend/main.py
  32. 4 0
      backend/migrations/README
  33. 96 0
      backend/migrations/env.py
  34. 27 0
      backend/migrations/script.py.mako
  35. 9 0
      backend/migrations/util.py
  36. 202 0
      backend/migrations/versions/7e5b5dc7342b_init.py
  37. 11 4
      backend/requirements.txt
  38. BIN
      backend/static/splash.png
  39. 0 0
      backend/test/__init__.py
  40. 202 0
      backend/test/apps/webui/routers/test_auths.py
  41. 238 0
      backend/test/apps/webui/routers/test_chats.py
  42. 106 0
      backend/test/apps/webui/routers/test_documents.py
  43. 62 0
      backend/test/apps/webui/routers/test_models.py
  44. 92 0
      backend/test/apps/webui/routers/test_prompts.py
  45. 168 0
      backend/test/apps/webui/routers/test_users.py
  46. 161 0
      backend/test/util/abstract_integration_test.py
  47. 45 0
      backend/test/util/mock_user.py
  48. 4 1
      backend/utils/tools.py
  49. 1 0
      backend/utils/utils.py
  50. 16 2
      src/app.css
  51. 7 5
      src/app.html
  52. 3 3
      src/lib/components/ChangelogModal.svelte
  53. 37 23
      src/lib/components/admin/Settings.svelte
  54. 6 6
      src/lib/components/admin/Settings/Audio.svelte
  55. 2 2
      src/lib/components/admin/Settings/Connections.svelte
  56. 14 14
      src/lib/components/admin/Settings/Documents.svelte
  57. 2 2
      src/lib/components/admin/Settings/General.svelte
  58. 9 9
      src/lib/components/admin/Settings/Images.svelte
  59. 12 10
      src/lib/components/admin/Settings/Interface.svelte
  60. 23 21
      src/lib/components/admin/Settings/Models.svelte
  61. 5 5
      src/lib/components/admin/Settings/Pipelines.svelte
  62. 2 2
      src/lib/components/admin/Settings/Users.svelte
  63. 5 5
      src/lib/components/admin/Settings/WebSearch.svelte
  64. 102 60
      src/lib/components/chat/Chat.svelte
  65. 63 0
      src/lib/components/chat/ChatControls.svelte
  66. 49 0
      src/lib/components/chat/Controls/Controls.svelte
  67. 1 1
      src/lib/components/chat/MessageInput.svelte
  68. 6 4
      src/lib/components/chat/MessageInput/CallOverlay.svelte
  69. 2 1
      src/lib/components/chat/Messages/CodeBlock.svelte
  70. 1 1
      src/lib/components/chat/Messages/Name.svelte
  71. 3 3
      src/lib/components/chat/Messages/Placeholder.svelte
  72. 4 1
      src/lib/components/chat/Messages/ResponseMessage.svelte
  73. 15 22
      src/lib/components/chat/Messages/ResponseMessage/WebSearchResults.svelte
  74. 2 2
      src/lib/components/chat/ModelSelector.svelte
  75. 4 4
      src/lib/components/chat/ModelSelector/Selector.svelte
  76. 19 19
      src/lib/components/chat/Settings/Advanced/AdvancedParams.svelte
  77. 4 0
      src/lib/components/chat/Settings/General.svelte
  78. 35 4
      src/lib/components/chat/SettingsModal.svelte
  79. 5 4
      src/lib/components/common/Banner.svelte
  80. 18 0
      src/lib/components/common/Collapsible.svelte
  81. 15 4
      src/lib/components/common/ConfirmDialog.svelte
  82. 1 1
      src/lib/components/common/Modal.svelte
  83. 1 1
      src/lib/components/common/Selector.svelte
  84. 2 2
      src/lib/components/common/SensitiveInput.svelte
  85. 1 1
      src/lib/components/common/Tags/TagInput.svelte
  86. 6 1
      src/lib/components/common/Tooltip.svelte
  87. 1 1
      src/lib/components/documents/EditDocModal.svelte
  88. 17 0
      src/lib/components/icons/AdjustmentsHorizontal.svelte
  89. 21 4
      src/lib/components/layout/Navbar.svelte
  90. 5 41
      src/lib/components/layout/Sidebar.svelte
  91. 1 1
      src/lib/components/layout/Sidebar/UserMenu.svelte
  92. 1 1
      src/lib/components/workspace/Documents.svelte
  93. 3 3
      src/lib/components/workspace/Functions.svelte
  94. 4 4
      src/lib/components/workspace/Models.svelte
  95. 1 1
      src/lib/components/workspace/Models/Knowledge/Selector.svelte
  96. 2 2
      src/lib/components/workspace/Prompts.svelte
  97. 3 3
      src/lib/components/workspace/Tools.svelte
  98. 2 2
      src/lib/i18n/locales/ar-BH/translation.json
  99. 2 2
      src/lib/i18n/locales/bg-BG/translation.json
  100. 2 2
      src/lib/i18n/locales/bn-BD/translation.json

+ 32 - 6
.github/workflows/integration-test.yml

@@ -35,6 +35,10 @@ jobs:
           done
           done
           echo "Service is up!"
           echo "Service is up!"
 
 
+      - name: Delete Docker build cache
+        run: |
+          docker builder prune --all --force
+
       - name: Preload Ollama model
       - name: Preload Ollama model
         run: |
         run: |
           docker exec ollama ollama pull qwen:0.5b-chat-v1.5-q2_K
           docker exec ollama ollama pull qwen:0.5b-chat-v1.5-q2_K
@@ -43,7 +47,7 @@ jobs:
         uses: cypress-io/github-action@v6
         uses: cypress-io/github-action@v6
         with:
         with:
           browser: chrome
           browser: chrome
-          wait-on: "http://localhost:3000"
+          wait-on: 'http://localhost:3000'
           config: baseUrl=http://localhost:3000
           config: baseUrl=http://localhost:3000
 
 
       - uses: actions/upload-artifact@v4
       - uses: actions/upload-artifact@v4
@@ -67,6 +71,28 @@ jobs:
           path: compose-logs.txt
           path: compose-logs.txt
           if-no-files-found: ignore
           if-no-files-found: ignore
 
 
+  # pytest:
+  #   name: Run Backend Tests
+  #   runs-on: ubuntu-latest
+  #   steps:
+  #     - uses: actions/checkout@v4
+
+  #     - name: Set up Python
+  #       uses: actions/setup-python@v4
+  #       with:
+  #         python-version: ${{ matrix.python-version }}
+
+  #     - name: Install dependencies
+  #       run: |
+  #         python -m pip install --upgrade pip
+  #         pip install -r backend/requirements.txt
+
+  #     - name: pytest run
+  #       run: |
+  #         ls -al
+  #         cd backend
+  #         PYTHONPATH=. pytest . -o log_cli=true -o log_cli_level=INFO
+
   migration_test:
   migration_test:
     name: Run Migration Tests
     name: Run Migration Tests
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
@@ -126,11 +152,11 @@ jobs:
           cd backend
           cd backend
           uvicorn main:app --port "8080" --forwarded-allow-ips '*' &
           uvicorn main:app --port "8080" --forwarded-allow-ips '*' &
           UVICORN_PID=$!
           UVICORN_PID=$!
-          # Wait up to 20 seconds for the server to start
-          for i in {1..20}; do
+          # Wait up to 40 seconds for the server to start
+          for i in {1..40}; do
               curl -s http://localhost:8080/api/config > /dev/null && break
               curl -s http://localhost:8080/api/config > /dev/null && break
               sleep 1
               sleep 1
-              if [ $i -eq 20 ]; then
+              if [ $i -eq 40 ]; then
                   echo "Server failed to start"
                   echo "Server failed to start"
                   kill -9 $UVICORN_PID
                   kill -9 $UVICORN_PID
                   exit 1
                   exit 1
@@ -171,7 +197,7 @@ jobs:
           fi
           fi
 
 
           # Check that service will reconnect to postgres when connection will be closed
           # Check that service will reconnect to postgres when connection will be closed
-          status_code=$(curl --write-out %{http_code} -s --output /dev/null http://localhost:8081/health)
+          status_code=$(curl --write-out %{http_code} -s --output /dev/null http://localhost:8081/health/db)
           if [[ "$status_code" -ne 200 ]] ; then
           if [[ "$status_code" -ne 200 ]] ; then
             echo "Server has failed before postgres reconnect check"
             echo "Server has failed before postgres reconnect check"
             exit 1
             exit 1
@@ -183,7 +209,7 @@ jobs:
             cur = conn.cursor(); \
             cur = conn.cursor(); \
             cur.execute('SELECT pg_terminate_backend(psa.pid) FROM pg_stat_activity psa WHERE datname = current_database() AND pid <> pg_backend_pid();')"
             cur.execute('SELECT pg_terminate_backend(psa.pid) FROM pg_stat_activity psa WHERE datname = current_database() AND pid <> pg_backend_pid();')"
 
 
-          status_code=$(curl --write-out %{http_code} -s --output /dev/null http://localhost:8081/health)
+          status_code=$(curl --write-out %{http_code} -s --output /dev/null http://localhost:8081/health/db)
           if [[ "$status_code" -ne 200 ]] ; then
           if [[ "$status_code" -ne 200 ]] ; then
             echo "Server has not reconnected to postgres after connection was closed: returned status $status_code"
             echo "Server has not reconnected to postgres after connection was closed: returned status $status_code"
             exit 1
             exit 1

+ 114 - 0
backend/alembic.ini

@@ -0,0 +1,114 @@
+# A generic, single database configuration.
+
+[alembic]
+# path to migration scripts
+script_location = migrations
+
+# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
+# Uncomment the line below if you want the files to be prepended with date and time
+# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
+
+# sys.path path, will be prepended to sys.path if present.
+# defaults to the current working directory.
+prepend_sys_path = .
+
+# timezone to use when rendering the date within the migration file
+# as well as the filename.
+# If specified, requires the python>=3.9 or backports.zoneinfo library.
+# Any required deps can installed by adding `alembic[tz]` to the pip requirements
+# string value is passed to ZoneInfo()
+# leave blank for localtime
+# timezone =
+
+# max length of characters to apply to the
+# "slug" field
+# truncate_slug_length = 40
+
+# set to 'true' to run the environment during
+# the 'revision' command, regardless of autogenerate
+# revision_environment = false
+
+# set to 'true' to allow .pyc and .pyo files without
+# a source .py file to be detected as revisions in the
+# versions/ directory
+# sourceless = false
+
+# version location specification; This defaults
+# to migrations/versions.  When using multiple version
+# directories, initial revisions must be specified with --version-path.
+# The path separator used here should be the separator specified by "version_path_separator" below.
+# version_locations = %(here)s/bar:%(here)s/bat:migrations/versions
+
+# version path separator; As mentioned above, this is the character used to split
+# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
+# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
+# Valid values for version_path_separator are:
+#
+# version_path_separator = :
+# version_path_separator = ;
+# version_path_separator = space
+version_path_separator = os  # Use os.pathsep. Default configuration used for new projects.
+
+# set to 'true' to search source files recursively
+# in each "version_locations" directory
+# new in Alembic version 1.10
+# recursive_version_locations = false
+
+# the output encoding used when revision files
+# are written from script.py.mako
+# output_encoding = utf-8
+
+# sqlalchemy.url = REPLACE_WITH_DATABASE_URL
+
+
+[post_write_hooks]
+# post_write_hooks defines scripts or Python functions that are run
+# on newly generated revision scripts.  See the documentation for further
+# detail and examples
+
+# format using "black" - use the console_scripts runner, against the "black" entrypoint
+# hooks = black
+# black.type = console_scripts
+# black.entrypoint = black
+# black.options = -l 79 REVISION_SCRIPT_FILENAME
+
+# lint with attempts to fix using "ruff" - use the exec runner, execute a binary
+# hooks = ruff
+# ruff.type = exec
+# ruff.executable = %(here)s/.venv/bin/ruff
+# ruff.options = --fix REVISION_SCRIPT_FILENAME
+
+# Logging configuration
+[loggers]
+keys = root,sqlalchemy,alembic
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARN
+handlers = console
+qualname =
+
+[logger_sqlalchemy]
+level = WARN
+handlers =
+qualname = sqlalchemy.engine
+
+[logger_alembic]
+level = INFO
+handlers =
+qualname = alembic
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(levelname)-5.5s [%(name)s] %(message)s
+datefmt = %H:%M:%S

+ 2 - 1
backend/apps/rag/main.py

@@ -1004,10 +1004,11 @@ def store_docs_in_vector_db(docs, collection_name, overwrite: bool = False) -> b
 
 
         return True
         return True
     except Exception as e:
     except Exception as e:
-        log.exception(e)
         if e.__class__.__name__ == "UniqueConstraintError":
         if e.__class__.__name__ == "UniqueConstraintError":
             return True
             return True
 
 
+        log.exception(e)
+
         return False
         return False
 
 
 
 

+ 80 - 24
backend/apps/webui/internal/db.py

@@ -1,18 +1,39 @@
 import os
 import os
 import logging
 import logging
 import json
 import json
+from contextlib import contextmanager
 
 
-from peewee import *
 from peewee_migrate import Router
 from peewee_migrate import Router
-
 from apps.webui.internal.wrappers import register_connection
 from apps.webui.internal.wrappers import register_connection
+
+from typing import Optional, Any
+from typing_extensions import Self
+
+from sqlalchemy import create_engine, types, Dialect
+from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.orm import sessionmaker, scoped_session
+from sqlalchemy.sql.type_api import _T
+
 from config import SRC_LOG_LEVELS, DATA_DIR, DATABASE_URL, BACKEND_DIR
 from config import SRC_LOG_LEVELS, DATA_DIR, DATABASE_URL, BACKEND_DIR
 
 
 log = logging.getLogger(__name__)
 log = logging.getLogger(__name__)
 log.setLevel(SRC_LOG_LEVELS["DB"])
 log.setLevel(SRC_LOG_LEVELS["DB"])
 
 
 
 
-class JSONField(TextField):
+class JSONField(types.TypeDecorator):
+    impl = types.Text
+    cache_ok = True
+
+    def process_bind_param(self, value: Optional[_T], dialect: Dialect) -> Any:
+        return json.dumps(value)
+
+    def process_result_value(self, value: Optional[_T], dialect: Dialect) -> Any:
+        if value is not None:
+            return json.loads(value)
+
+    def copy(self, **kw: Any) -> Self:
+        return JSONField(self.impl.length)
+
     def db_value(self, value):
     def db_value(self, value):
         return json.dumps(value)
         return json.dumps(value)
 
 
@@ -30,25 +51,60 @@ else:
     pass
     pass
 
 
 
 
-# The `register_connection` function encapsulates the logic for setting up
-# the database connection based on the connection string, while `connect`
-# is a Peewee-specific method to manage the connection state and avoid errors
-# when a connection is already open.
-try:
-    DB = register_connection(DATABASE_URL)
-    log.info(f"Connected to a {DB.__class__.__name__} database.")
-except Exception as e:
-    log.error(f"Failed to initialize the database connection: {e}")
-    raise
-
-router = Router(
-    DB,
-    migrate_dir=BACKEND_DIR / "apps" / "webui" / "internal" / "migrations",
-    logger=log,
+# Workaround to handle the peewee migration
+# This is required to ensure the peewee migration is handled before the alembic migration
+def handle_peewee_migration(DATABASE_URL):
+    try:
+        # Replace the postgresql:// with postgres:// and %40 with @ in the DATABASE_URL
+        db = register_connection(
+            DATABASE_URL.replace("postgresql://", "postgres://").replace("%40", "@")
+        )
+        migrate_dir = BACKEND_DIR / "apps" / "webui" / "internal" / "migrations"
+        router = Router(db, logger=log, migrate_dir=migrate_dir)
+        router.run()
+        db.close()
+
+        # check if db connection has been closed
+
+    except Exception as e:
+        log.error(f"Failed to initialize the database connection: {e}")
+        raise
+
+    finally:
+        # Properly closing the database connection
+        if db and not db.is_closed():
+            db.close()
+
+        # Assert if db connection has been closed
+        assert db.is_closed(), "Database connection is still open."
+
+
+handle_peewee_migration(DATABASE_URL)
+
+
+SQLALCHEMY_DATABASE_URL = DATABASE_URL
+if "sqlite" in SQLALCHEMY_DATABASE_URL:
+    engine = create_engine(
+        SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
+    )
+else:
+    engine = create_engine(SQLALCHEMY_DATABASE_URL, pool_pre_ping=True)
+
+
+SessionLocal = sessionmaker(
+    autocommit=False, autoflush=False, bind=engine, expire_on_commit=False
 )
 )
-router.run()
-try:
-    DB.connect(reuse_if_open=True)
-except OperationalError as e:
-    log.info(f"Failed to connect to database again due to: {e}")
-    pass
+Base = declarative_base()
+Session = scoped_session(SessionLocal)
+
+
+# Dependency
+def get_session():
+    db = SessionLocal()
+    try:
+        yield db
+    finally:
+        db.close()
+
+
+get_db = contextmanager(get_session)

+ 0 - 4
backend/apps/webui/internal/migrations/017_add_user_oauth_sub.py

@@ -1,10 +1,7 @@
 """Peewee migrations -- 017_add_user_oauth_sub.py.
 """Peewee migrations -- 017_add_user_oauth_sub.py.
-
 Some examples (model - class or model name)::
 Some examples (model - class or model name)::
-
     > Model = migrator.orm['table_name']            # Return model in current state by name
     > Model = migrator.orm['table_name']            # Return model in current state by name
     > Model = migrator.ModelClass                   # Return model in current state by name
     > Model = migrator.ModelClass                   # Return model in current state by name
-
     > migrator.sql(sql)                             # Run custom SQL
     > migrator.sql(sql)                             # Run custom SQL
     > migrator.run(func, *args, **kwargs)           # Run python function with the given args
     > migrator.run(func, *args, **kwargs)           # Run python function with the given args
     > migrator.create_model(Model)                  # Create a model (could be used as decorator)
     > migrator.create_model(Model)                  # Create a model (could be used as decorator)
@@ -21,7 +18,6 @@ Some examples (model - class or model name)::
     > migrator.drop_index(model, *col_names)
     > migrator.drop_index(model, *col_names)
     > migrator.drop_not_null(model, *field_names)
     > migrator.drop_not_null(model, *field_names)
     > migrator.drop_constraints(model, *constraints)
     > migrator.drop_constraints(model, *constraints)
-
 """
 """
 
 
 from contextlib import suppress
 from contextlib import suppress

+ 0 - 21
backend/apps/webui/internal/migrations/README.md

@@ -1,21 +0,0 @@
-# Database Migrations
-
-This directory contains all the database migrations for the web app.
-Migrations are done using the [`peewee-migrate`](https://github.com/klen/peewee_migrate) library.
-
-Migrations are automatically ran at app startup.
-
-## Creating a migration
-
-Have you made a change to the schema of an existing model?
-You will need to create a migration file to ensure that existing databases are updated for backwards compatibility.
-
-1. Have a database file (`webui.db`) that has the old schema prior to any of your changes.
-2. Make your changes to the models.
-3. From the `backend` directory, run the following command:
-   ```bash
-   pw_migrate create --auto --auto-source apps.webui.models --database sqlite:///${SQLITE_DB} --directory apps/web/internal/migrations ${MIGRATION_NAME}
-   ```
-   - `$SQLITE_DB` should be the path to the database file.
-   - `$MIGRATION_NAME` should be a descriptive name for the migration.
-4. The migration file will be created in the `apps/web/internal/migrations` directory.

+ 82 - 2
backend/apps/webui/main.py

@@ -3,7 +3,7 @@ from fastapi.routing import APIRoute
 from fastapi.responses import StreamingResponse
 from fastapi.responses import StreamingResponse
 from fastapi.middleware.cors import CORSMiddleware
 from fastapi.middleware.cors import CORSMiddleware
 from starlette.middleware.sessions import SessionMiddleware
 from starlette.middleware.sessions import SessionMiddleware
-
+from sqlalchemy.orm import Session
 from apps.webui.routers import (
 from apps.webui.routers import (
     auths,
     auths,
     users,
     users,
@@ -19,8 +19,13 @@ from apps.webui.routers import (
     functions,
     functions,
 )
 )
 from apps.webui.models.functions import Functions
 from apps.webui.models.functions import Functions
+from apps.webui.models.models import Models
+
 from apps.webui.utils import load_function_module_by_id
 from apps.webui.utils import load_function_module_by_id
+
 from utils.misc import stream_message_template
 from utils.misc import stream_message_template
+from utils.task import prompt_template
+
 
 
 from config import (
 from config import (
     WEBUI_BUILD_HASH,
     WEBUI_BUILD_HASH,
@@ -39,6 +44,8 @@ from config import (
     WEBUI_BANNERS,
     WEBUI_BANNERS,
     ENABLE_COMMUNITY_SHARING,
     ENABLE_COMMUNITY_SHARING,
     AppConfig,
     AppConfig,
+    OAUTH_USERNAME_CLAIM,
+    OAUTH_PICTURE_CLAIM,
 )
 )
 
 
 import inspect
 import inspect
@@ -74,6 +81,9 @@ app.state.config.BANNERS = WEBUI_BANNERS
 
 
 app.state.config.ENABLE_COMMUNITY_SHARING = ENABLE_COMMUNITY_SHARING
 app.state.config.ENABLE_COMMUNITY_SHARING = ENABLE_COMMUNITY_SHARING
 
 
+app.state.config.OAUTH_USERNAME_CLAIM = OAUTH_USERNAME_CLAIM
+app.state.config.OAUTH_PICTURE_CLAIM = OAUTH_PICTURE_CLAIM
+
 app.state.MODELS = {}
 app.state.MODELS = {}
 app.state.TOOLS = {}
 app.state.TOOLS = {}
 app.state.FUNCTIONS = {}
 app.state.FUNCTIONS = {}
@@ -129,7 +139,6 @@ async def get_pipe_models():
             function_module = app.state.FUNCTIONS[pipe.id]
             function_module = app.state.FUNCTIONS[pipe.id]
 
 
         if hasattr(function_module, "valves") and hasattr(function_module, "Valves"):
         if hasattr(function_module, "valves") and hasattr(function_module, "Valves"):
-            print(f"Getting valves for {pipe.id}")
             valves = Functions.get_function_valves_by_id(pipe.id)
             valves = Functions.get_function_valves_by_id(pipe.id)
             function_module.valves = function_module.Valves(
             function_module.valves = function_module.Valves(
                 **(valves if valves else {})
                 **(valves if valves else {})
@@ -181,6 +190,77 @@ async def get_pipe_models():
 
 
 
 
 async def generate_function_chat_completion(form_data, user):
 async def generate_function_chat_completion(form_data, user):
+    model_id = form_data.get("model")
+    model_info = Models.get_model_by_id(model_id)
+
+    if model_info:
+        if model_info.base_model_id:
+            form_data["model"] = model_info.base_model_id
+
+        model_info.params = model_info.params.model_dump()
+
+        if model_info.params:
+            if model_info.params.get("temperature", None) is not None:
+                form_data["temperature"] = float(model_info.params.get("temperature"))
+
+            if model_info.params.get("top_p", None):
+                form_data["top_p"] = int(model_info.params.get("top_p", None))
+
+            if model_info.params.get("max_tokens", None):
+                form_data["max_tokens"] = int(model_info.params.get("max_tokens", None))
+
+            if model_info.params.get("frequency_penalty", None):
+                form_data["frequency_penalty"] = int(
+                    model_info.params.get("frequency_penalty", None)
+                )
+
+            if model_info.params.get("seed", None):
+                form_data["seed"] = model_info.params.get("seed", None)
+
+            if model_info.params.get("stop", None):
+                form_data["stop"] = (
+                    [
+                        bytes(stop, "utf-8").decode("unicode_escape")
+                        for stop in model_info.params["stop"]
+                    ]
+                    if model_info.params.get("stop", None)
+                    else None
+                )
+
+        system = model_info.params.get("system", None)
+        if system:
+            system = prompt_template(
+                system,
+                **(
+                    {
+                        "user_name": user.name,
+                        "user_location": (
+                            user.info.get("location") if user.info else None
+                        ),
+                    }
+                    if user
+                    else {}
+                ),
+            )
+            # Check if the payload already has a system message
+            # If not, add a system message to the payload
+            if form_data.get("messages"):
+                for message in form_data["messages"]:
+                    if message.get("role") == "system":
+                        message["content"] = system + message["content"]
+                        break
+                else:
+                    form_data["messages"].insert(
+                        0,
+                        {
+                            "role": "system",
+                            "content": system,
+                        },
+                    )
+
+    else:
+        pass
+
     async def job():
     async def job():
         pipe_id = form_data["model"]
         pipe_id = form_data["model"]
         if "." in pipe_id:
         if "." in pipe_id:

+ 63 - 56
backend/apps/webui/models/auths.py

@@ -1,14 +1,13 @@
 from pydantic import BaseModel
 from pydantic import BaseModel
-from typing import List, Union, Optional
-import time
+from typing import Optional
 import uuid
 import uuid
 import logging
 import logging
-from peewee import *
+from sqlalchemy import String, Column, Boolean, Text
 
 
 from apps.webui.models.users import UserModel, Users
 from apps.webui.models.users import UserModel, Users
 from utils.utils import verify_password
 from utils.utils import verify_password
 
 
-from apps.webui.internal.db import DB
+from apps.webui.internal.db import Base, get_db
 
 
 from config import SRC_LOG_LEVELS
 from config import SRC_LOG_LEVELS
 
 
@@ -20,14 +19,13 @@ log.setLevel(SRC_LOG_LEVELS["MODELS"])
 ####################
 ####################
 
 
 
 
-class Auth(Model):
-    id = CharField(unique=True)
-    email = CharField()
-    password = TextField()
-    active = BooleanField()
+class Auth(Base):
+    __tablename__ = "auth"
 
 
-    class Meta:
-        database = DB
+    id = Column(String, primary_key=True)
+    email = Column(String)
+    password = Column(Text)
+    active = Column(Boolean)
 
 
 
 
 class AuthModel(BaseModel):
 class AuthModel(BaseModel):
@@ -94,9 +92,6 @@ class AddUserForm(SignupForm):
 
 
 
 
 class AuthsTable:
 class AuthsTable:
-    def __init__(self, db):
-        self.db = db
-        self.db.create_tables([Auth])
 
 
     def insert_new_auth(
     def insert_new_auth(
         self,
         self,
@@ -107,36 +102,44 @@ class AuthsTable:
         role: str = "pending",
         role: str = "pending",
         oauth_sub: Optional[str] = None,
         oauth_sub: Optional[str] = None,
     ) -> Optional[UserModel]:
     ) -> Optional[UserModel]:
-        log.info("insert_new_auth")
+        with get_db() as db:
 
 
-        id = str(uuid.uuid4())
+            log.info("insert_new_auth")
 
 
-        auth = AuthModel(
-            **{"id": id, "email": email, "password": password, "active": True}
-        )
-        result = Auth.create(**auth.model_dump())
+            id = str(uuid.uuid4())
 
 
-        user = Users.insert_new_user(
-            id, name, email, profile_image_url, role, oauth_sub
-        )
+            auth = AuthModel(
+                **{"id": id, "email": email, "password": password, "active": True}
+            )
+            result = Auth(**auth.model_dump())
+            db.add(result)
 
 
-        if result and user:
-            return user
-        else:
-            return None
+            user = Users.insert_new_user(
+                id, name, email, profile_image_url, role, oauth_sub
+            )
+
+            db.commit()
+            db.refresh(result)
+
+            if result and user:
+                return user
+            else:
+                return None
 
 
     def authenticate_user(self, email: str, password: str) -> Optional[UserModel]:
     def authenticate_user(self, email: str, password: str) -> Optional[UserModel]:
         log.info(f"authenticate_user: {email}")
         log.info(f"authenticate_user: {email}")
         try:
         try:
-            auth = Auth.get(Auth.email == email, Auth.active == True)
-            if auth:
-                if verify_password(password, auth.password):
-                    user = Users.get_user_by_id(auth.id)
-                    return user
+            with get_db() as db:
+
+                auth = db.query(Auth).filter_by(email=email, active=True).first()
+                if auth:
+                    if verify_password(password, auth.password):
+                        user = Users.get_user_by_id(auth.id)
+                        return user
+                    else:
+                        return None
                 else:
                 else:
                     return None
                     return None
-            else:
-                return None
         except:
         except:
             return None
             return None
 
 
@@ -155,46 +158,50 @@ class AuthsTable:
     def authenticate_user_by_trusted_header(self, email: str) -> Optional[UserModel]:
     def authenticate_user_by_trusted_header(self, email: str) -> Optional[UserModel]:
         log.info(f"authenticate_user_by_trusted_header: {email}")
         log.info(f"authenticate_user_by_trusted_header: {email}")
         try:
         try:
-            auth = Auth.get(Auth.email == email, Auth.active == True)
-            if auth:
-                user = Users.get_user_by_id(auth.id)
-                return user
+            with get_db() as db:
+                auth = db.query(Auth).filter(email=email, active=True).first()
+                if auth:
+                    user = Users.get_user_by_id(auth.id)
+                    return user
         except:
         except:
             return None
             return None
 
 
     def update_user_password_by_id(self, id: str, new_password: str) -> bool:
     def update_user_password_by_id(self, id: str, new_password: str) -> bool:
         try:
         try:
-            query = Auth.update(password=new_password).where(Auth.id == id)
-            result = query.execute()
-
-            return True if result == 1 else False
+            with get_db() as db:
+                result = (
+                    db.query(Auth).filter_by(id=id).update({"password": new_password})
+                )
+                db.commit()
+                return True if result == 1 else False
         except:
         except:
             return False
             return False
 
 
     def update_email_by_id(self, id: str, email: str) -> bool:
     def update_email_by_id(self, id: str, email: str) -> bool:
         try:
         try:
-            query = Auth.update(email=email).where(Auth.id == id)
-            result = query.execute()
-
-            return True if result == 1 else False
+            with get_db() as db:
+                result = db.query(Auth).filter_by(id=id).update({"email": email})
+                db.commit()
+                return True if result == 1 else False
         except:
         except:
             return False
             return False
 
 
     def delete_auth_by_id(self, id: str) -> bool:
     def delete_auth_by_id(self, id: str) -> bool:
         try:
         try:
-            # Delete User
-            result = Users.delete_user_by_id(id)
+            with get_db() as db:
 
 
-            if result:
-                # Delete Auth
-                query = Auth.delete().where(Auth.id == id)
-                query.execute()  # Remove the rows, return number of rows removed.
+                # Delete User
+                result = Users.delete_user_by_id(id)
 
 
-                return True
-            else:
-                return False
+                if result:
+                    db.query(Auth).filter_by(id=id).delete()
+                    db.commit()
+
+                    return True
+                else:
+                    return False
         except:
         except:
             return False
             return False
 
 
 
 
-Auths = AuthsTable(DB)
+Auths = AuthsTable()

+ 206 - 187
backend/apps/webui/models/chats.py

@@ -1,36 +1,38 @@
-from pydantic import BaseModel
+from pydantic import BaseModel, ConfigDict
 from typing import List, Union, Optional
 from typing import List, Union, Optional
-from peewee import *
-from playhouse.shortcuts import model_to_dict
 
 
 import json
 import json
 import uuid
 import uuid
 import time
 import time
 
 
-from apps.webui.internal.db import DB
+from sqlalchemy import Column, String, BigInteger, Boolean, Text
+
+from apps.webui.internal.db import Base, get_db
+
 
 
 ####################
 ####################
 # Chat DB Schema
 # Chat DB Schema
 ####################
 ####################
 
 
 
 
-class Chat(Model):
-    id = CharField(unique=True)
-    user_id = CharField()
-    title = TextField()
-    chat = TextField()  # Save Chat JSON as Text
+class Chat(Base):
+    __tablename__ = "chat"
 
 
-    created_at = BigIntegerField()
-    updated_at = BigIntegerField()
+    id = Column(String, primary_key=True)
+    user_id = Column(String)
+    title = Column(Text)
+    chat = Column(Text)  # Save Chat JSON as Text
 
 
-    share_id = CharField(null=True, unique=True)
-    archived = BooleanField(default=False)
+    created_at = Column(BigInteger)
+    updated_at = Column(BigInteger)
 
 
-    class Meta:
-        database = DB
+    share_id = Column(Text, unique=True, nullable=True)
+    archived = Column(Boolean, default=False)
 
 
 
 
 class ChatModel(BaseModel):
 class ChatModel(BaseModel):
+    model_config = ConfigDict(from_attributes=True)
+
     id: str
     id: str
     user_id: str
     user_id: str
     title: str
     title: str
@@ -75,91 +77,104 @@ class ChatTitleIdResponse(BaseModel):
 
 
 
 
 class ChatTable:
 class ChatTable:
-    def __init__(self, db):
-        self.db = db
-        db.create_tables([Chat])
 
 
     def insert_new_chat(self, user_id: str, form_data: ChatForm) -> Optional[ChatModel]:
     def insert_new_chat(self, user_id: str, form_data: ChatForm) -> Optional[ChatModel]:
-        id = str(uuid.uuid4())
-        chat = ChatModel(
-            **{
-                "id": id,
-                "user_id": user_id,
-                "title": (
-                    form_data.chat["title"] if "title" in form_data.chat else "New Chat"
-                ),
-                "chat": json.dumps(form_data.chat),
-                "created_at": int(time.time()),
-                "updated_at": int(time.time()),
-            }
-        )
-
-        result = Chat.create(**chat.model_dump())
-        return chat if result else None
+        with get_db() as db:
+
+            id = str(uuid.uuid4())
+            chat = ChatModel(
+                **{
+                    "id": id,
+                    "user_id": user_id,
+                    "title": (
+                        form_data.chat["title"]
+                        if "title" in form_data.chat
+                        else "New Chat"
+                    ),
+                    "chat": json.dumps(form_data.chat),
+                    "created_at": int(time.time()),
+                    "updated_at": int(time.time()),
+                }
+            )
+
+            result = Chat(**chat.model_dump())
+            db.add(result)
+            db.commit()
+            db.refresh(result)
+            return ChatModel.model_validate(result) if result else None
 
 
     def update_chat_by_id(self, id: str, chat: dict) -> Optional[ChatModel]:
     def update_chat_by_id(self, id: str, chat: dict) -> Optional[ChatModel]:
         try:
         try:
-            query = Chat.update(
-                chat=json.dumps(chat),
-                title=chat["title"] if "title" in chat else "New Chat",
-                updated_at=int(time.time()),
-            ).where(Chat.id == id)
-            query.execute()
-
-            chat = Chat.get(Chat.id == id)
-            return ChatModel(**model_to_dict(chat))
-        except:
+            with get_db() as db:
+
+                chat_obj = db.get(Chat, id)
+                chat_obj.chat = json.dumps(chat)
+                chat_obj.title = chat["title"] if "title" in chat else "New Chat"
+                chat_obj.updated_at = int(time.time())
+                db.commit()
+                db.refresh(chat_obj)
+
+                return ChatModel.model_validate(chat_obj)
+        except Exception as e:
             return None
             return None
 
 
     def insert_shared_chat_by_chat_id(self, chat_id: str) -> Optional[ChatModel]:
     def insert_shared_chat_by_chat_id(self, chat_id: str) -> Optional[ChatModel]:
-        # Get the existing chat to share
-        chat = Chat.get(Chat.id == chat_id)
-        # Check if the chat is already shared
-        if chat.share_id:
-            return self.get_chat_by_id_and_user_id(chat.share_id, "shared")
-        # Create a new chat with the same data, but with a new ID
-        shared_chat = ChatModel(
-            **{
-                "id": str(uuid.uuid4()),
-                "user_id": f"shared-{chat_id}",
-                "title": chat.title,
-                "chat": chat.chat,
-                "created_at": chat.created_at,
-                "updated_at": int(time.time()),
-            }
-        )
-        shared_result = Chat.create(**shared_chat.model_dump())
-        # Update the original chat with the share_id
-        result = (
-            Chat.update(share_id=shared_chat.id).where(Chat.id == chat_id).execute()
-        )
-
-        return shared_chat if (shared_result and result) else None
+        with get_db() as db:
+
+            # Get the existing chat to share
+            chat = db.get(Chat, chat_id)
+            # Check if the chat is already shared
+            if chat.share_id:
+                return self.get_chat_by_id_and_user_id(chat.share_id, "shared")
+            # Create a new chat with the same data, but with a new ID
+            shared_chat = ChatModel(
+                **{
+                    "id": str(uuid.uuid4()),
+                    "user_id": f"shared-{chat_id}",
+                    "title": chat.title,
+                    "chat": chat.chat,
+                    "created_at": chat.created_at,
+                    "updated_at": int(time.time()),
+                }
+            )
+            shared_result = Chat(**shared_chat.model_dump())
+            db.add(shared_result)
+            db.commit()
+            db.refresh(shared_result)
+
+            # Update the original chat with the share_id
+            result = (
+                db.query(Chat)
+                .filter_by(id=chat_id)
+                .update({"share_id": shared_chat.id})
+            )
+            db.commit()
+            return shared_chat if (shared_result and result) else None
 
 
     def update_shared_chat_by_chat_id(self, chat_id: str) -> Optional[ChatModel]:
     def update_shared_chat_by_chat_id(self, chat_id: str) -> Optional[ChatModel]:
         try:
         try:
-            print("update_shared_chat_by_id")
-            chat = Chat.get(Chat.id == chat_id)
-            print(chat)
+            with get_db() as db:
 
 
-            query = Chat.update(
-                title=chat.title,
-                chat=chat.chat,
-            ).where(Chat.id == chat.share_id)
+                print("update_shared_chat_by_id")
+                chat = db.get(Chat, chat_id)
+                print(chat)
+                chat.title = chat.title
+                chat.chat = chat.chat
+                db.commit()
+                db.refresh(chat)
 
 
-            query.execute()
-
-            chat = Chat.get(Chat.id == chat.share_id)
-            return ChatModel(**model_to_dict(chat))
+                return self.get_chat_by_id(chat.share_id)
         except:
         except:
             return None
             return None
 
 
     def delete_shared_chat_by_chat_id(self, chat_id: str) -> bool:
     def delete_shared_chat_by_chat_id(self, chat_id: str) -> bool:
         try:
         try:
-            query = Chat.delete().where(Chat.user_id == f"shared-{chat_id}")
-            query.execute()  # Remove the rows, return number of rows removed.
+            with get_db() as db:
+
+                db.query(Chat).filter_by(user_id=f"shared-{chat_id}").delete()
+                db.commit()
 
 
-            return True
+                return True
         except:
         except:
             return False
             return False
 
 
@@ -167,56 +182,50 @@ class ChatTable:
         self, id: str, share_id: Optional[str]
         self, id: str, share_id: Optional[str]
     ) -> Optional[ChatModel]:
     ) -> Optional[ChatModel]:
         try:
         try:
-            query = Chat.update(
-                share_id=share_id,
-            ).where(Chat.id == id)
-            query.execute()
+            with get_db() as db:
 
 
-            chat = Chat.get(Chat.id == id)
-            return ChatModel(**model_to_dict(chat))
+                chat = db.get(Chat, id)
+                chat.share_id = share_id
+                db.commit()
+                db.refresh(chat)
+                return ChatModel.model_validate(chat)
         except:
         except:
             return None
             return None
 
 
     def toggle_chat_archive_by_id(self, id: str) -> Optional[ChatModel]:
     def toggle_chat_archive_by_id(self, id: str) -> Optional[ChatModel]:
         try:
         try:
-            chat = self.get_chat_by_id(id)
-            query = Chat.update(
-                archived=(not chat.archived),
-            ).where(Chat.id == id)
+            with get_db() as db:
 
 
-            query.execute()
-
-            chat = Chat.get(Chat.id == id)
-            return ChatModel(**model_to_dict(chat))
+                chat = db.get(Chat, id)
+                chat.archived = not chat.archived
+                db.commit()
+                db.refresh(chat)
+                return ChatModel.model_validate(chat)
         except:
         except:
             return None
             return None
 
 
     def archive_all_chats_by_user_id(self, user_id: str) -> bool:
     def archive_all_chats_by_user_id(self, user_id: str) -> bool:
         try:
         try:
-            chats = self.get_chats_by_user_id(user_id)
-            for chat in chats:
-                query = Chat.update(
-                    archived=True,
-                ).where(Chat.id == chat.id)
-
-                query.execute()
-
-            return True
+            with get_db() as db:
+                db.query(Chat).filter_by(user_id=user_id).update({"archived": True})
+                db.commit()
+                return True
         except:
         except:
             return False
             return False
 
 
     def get_archived_chat_list_by_user_id(
     def get_archived_chat_list_by_user_id(
         self, user_id: str, skip: int = 0, limit: int = 50
         self, user_id: str, skip: int = 0, limit: int = 50
     ) -> List[ChatModel]:
     ) -> List[ChatModel]:
-        return [
-            ChatModel(**model_to_dict(chat))
-            for chat in Chat.select()
-            .where(Chat.archived == True)
-            .where(Chat.user_id == user_id)
-            .order_by(Chat.updated_at.desc())
-            # .limit(limit)
-            # .offset(skip)
-        ]
+        with get_db() as db:
+
+            all_chats = (
+                db.query(Chat)
+                .filter_by(user_id=user_id, archived=True)
+                .order_by(Chat.updated_at.desc())
+                # .limit(limit).offset(skip)
+                .all()
+            )
+            return [ChatModel.model_validate(chat) for chat in all_chats]
 
 
     def get_chat_list_by_user_id(
     def get_chat_list_by_user_id(
         self,
         self,
@@ -225,131 +234,141 @@ class ChatTable:
         skip: int = 0,
         skip: int = 0,
         limit: int = 50,
         limit: int = 50,
     ) -> List[ChatModel]:
     ) -> List[ChatModel]:
-        if include_archived:
-            return [
-                ChatModel(**model_to_dict(chat))
-                for chat in Chat.select()
-                .where(Chat.user_id == user_id)
-                .order_by(Chat.updated_at.desc())
-                # .limit(limit)
-                # .offset(skip)
-            ]
-        else:
-            return [
-                ChatModel(**model_to_dict(chat))
-                for chat in Chat.select()
-                .where(Chat.archived == False)
-                .where(Chat.user_id == user_id)
-                .order_by(Chat.updated_at.desc())
-                # .limit(limit)
-                # .offset(skip)
-            ]
+        with get_db() as db:
+            query = db.query(Chat).filter_by(user_id=user_id)
+            if not include_archived:
+                query = query.filter_by(archived=False)
+            all_chats = (
+                query.order_by(Chat.updated_at.desc())
+                # .limit(limit).offset(skip)
+                .all()
+            )
+            return [ChatModel.model_validate(chat) for chat in all_chats]
 
 
     def get_chat_list_by_chat_ids(
     def get_chat_list_by_chat_ids(
         self, chat_ids: List[str], skip: int = 0, limit: int = 50
         self, chat_ids: List[str], skip: int = 0, limit: int = 50
     ) -> List[ChatModel]:
     ) -> List[ChatModel]:
-        return [
-            ChatModel(**model_to_dict(chat))
-            for chat in Chat.select()
-            .where(Chat.archived == False)
-            .where(Chat.id.in_(chat_ids))
-            .order_by(Chat.updated_at.desc())
-        ]
+        with get_db() as db:
+            all_chats = (
+                db.query(Chat)
+                .filter(Chat.id.in_(chat_ids))
+                .filter_by(archived=False)
+                .order_by(Chat.updated_at.desc())
+                .all()
+            )
+            return [ChatModel.model_validate(chat) for chat in all_chats]
 
 
     def get_chat_by_id(self, id: str) -> Optional[ChatModel]:
     def get_chat_by_id(self, id: str) -> Optional[ChatModel]:
         try:
         try:
-            chat = Chat.get(Chat.id == id)
-            return ChatModel(**model_to_dict(chat))
+            with get_db() as db:
+
+                chat = db.get(Chat, id)
+                return ChatModel.model_validate(chat)
         except:
         except:
             return None
             return None
 
 
     def get_chat_by_share_id(self, id: str) -> Optional[ChatModel]:
     def get_chat_by_share_id(self, id: str) -> Optional[ChatModel]:
         try:
         try:
-            chat = Chat.get(Chat.share_id == id)
+            with get_db() as db:
 
 
-            if chat:
-                chat = Chat.get(Chat.id == id)
-                return ChatModel(**model_to_dict(chat))
-            else:
-                return None
-        except:
+                chat = db.query(Chat).filter_by(share_id=id).first()
+
+                if chat:
+                    return self.get_chat_by_id(id)
+                else:
+                    return None
+        except Exception as e:
             return None
             return None
 
 
     def get_chat_by_id_and_user_id(self, id: str, user_id: str) -> Optional[ChatModel]:
     def get_chat_by_id_and_user_id(self, id: str, user_id: str) -> Optional[ChatModel]:
         try:
         try:
-            chat = Chat.get(Chat.id == id, Chat.user_id == user_id)
-            return ChatModel(**model_to_dict(chat))
+            with get_db() as db:
+
+                chat = db.query(Chat).filter_by(id=id, user_id=user_id).first()
+                return ChatModel.model_validate(chat)
         except:
         except:
             return None
             return None
 
 
     def get_chats(self, skip: int = 0, limit: int = 50) -> List[ChatModel]:
     def get_chats(self, skip: int = 0, limit: int = 50) -> List[ChatModel]:
-        return [
-            ChatModel(**model_to_dict(chat))
-            for chat in Chat.select().order_by(Chat.updated_at.desc())
-            # .limit(limit).offset(skip)
-        ]
+        with get_db() as db:
+
+            all_chats = (
+                db.query(Chat)
+                # .limit(limit).offset(skip)
+                .order_by(Chat.updated_at.desc())
+            )
+            return [ChatModel.model_validate(chat) for chat in all_chats]
 
 
     def get_chats_by_user_id(self, user_id: str) -> List[ChatModel]:
     def get_chats_by_user_id(self, user_id: str) -> List[ChatModel]:
-        return [
-            ChatModel(**model_to_dict(chat))
-            for chat in Chat.select()
-            .where(Chat.user_id == user_id)
-            .order_by(Chat.updated_at.desc())
-            # .limit(limit).offset(skip)
-        ]
+        with get_db() as db:
+
+            all_chats = (
+                db.query(Chat)
+                .filter_by(user_id=user_id)
+                .order_by(Chat.updated_at.desc())
+            )
+            return [ChatModel.model_validate(chat) for chat in all_chats]
 
 
     def get_archived_chats_by_user_id(self, user_id: str) -> List[ChatModel]:
     def get_archived_chats_by_user_id(self, user_id: str) -> List[ChatModel]:
-        return [
-            ChatModel(**model_to_dict(chat))
-            for chat in Chat.select()
-            .where(Chat.archived == True)
-            .where(Chat.user_id == user_id)
-            .order_by(Chat.updated_at.desc())
-        ]
+        with get_db() as db:
+
+            all_chats = (
+                db.query(Chat)
+                .filter_by(user_id=user_id, archived=True)
+                .order_by(Chat.updated_at.desc())
+            )
+            return [ChatModel.model_validate(chat) for chat in all_chats]
 
 
     def delete_chat_by_id(self, id: str) -> bool:
     def delete_chat_by_id(self, id: str) -> bool:
         try:
         try:
-            query = Chat.delete().where((Chat.id == id))
-            query.execute()  # Remove the rows, return number of rows removed.
+            with get_db() as db:
 
 
-            return True and self.delete_shared_chat_by_chat_id(id)
+                db.query(Chat).filter_by(id=id).delete()
+                db.commit()
+
+                return True and self.delete_shared_chat_by_chat_id(id)
         except:
         except:
             return False
             return False
 
 
     def delete_chat_by_id_and_user_id(self, id: str, user_id: str) -> bool:
     def delete_chat_by_id_and_user_id(self, id: str, user_id: str) -> bool:
         try:
         try:
-            query = Chat.delete().where((Chat.id == id) & (Chat.user_id == user_id))
-            query.execute()  # Remove the rows, return number of rows removed.
+            with get_db() as db:
+
+                db.query(Chat).filter_by(id=id, user_id=user_id).delete()
+                db.commit()
 
 
-            return True and self.delete_shared_chat_by_chat_id(id)
+                return True and self.delete_shared_chat_by_chat_id(id)
         except:
         except:
             return False
             return False
 
 
     def delete_chats_by_user_id(self, user_id: str) -> bool:
     def delete_chats_by_user_id(self, user_id: str) -> bool:
         try:
         try:
 
 
-            self.delete_shared_chats_by_user_id(user_id)
+            with get_db() as db:
 
 
-            query = Chat.delete().where(Chat.user_id == user_id)
-            query.execute()  # Remove the rows, return number of rows removed.
+                self.delete_shared_chats_by_user_id(user_id)
 
 
-            return True
+                db.query(Chat).filter_by(user_id=user_id).delete()
+                db.commit()
+
+                return True
         except:
         except:
             return False
             return False
 
 
     def delete_shared_chats_by_user_id(self, user_id: str) -> bool:
     def delete_shared_chats_by_user_id(self, user_id: str) -> bool:
         try:
         try:
-            shared_chat_ids = [
-                f"shared-{chat.id}"
-                for chat in Chat.select().where(Chat.user_id == user_id)
-            ]
 
 
-            query = Chat.delete().where(Chat.user_id << shared_chat_ids)
-            query.execute()  # Remove the rows, return number of rows removed.
+            with get_db() as db:
+
+                chats_by_user = db.query(Chat).filter_by(user_id=user_id).all()
+                shared_chat_ids = [f"shared-{chat.id}" for chat in chats_by_user]
+
+                db.query(Chat).filter(Chat.user_id.in_(shared_chat_ids)).delete()
+                db.commit()
 
 
-            return True
+                return True
         except:
         except:
             return False
             return False
 
 
 
 
-Chats = ChatTable(DB)
+Chats = ChatTable()

+ 69 - 62
backend/apps/webui/models/documents.py

@@ -1,14 +1,11 @@
-from pydantic import BaseModel
-from peewee import *
-from playhouse.shortcuts import model_to_dict
-from typing import List, Union, Optional
+from pydantic import BaseModel, ConfigDict
+from typing import List, Optional
 import time
 import time
 import logging
 import logging
 
 
-from utils.utils import decode_token
-from utils.misc import get_gravatar_url
+from sqlalchemy import String, Column, BigInteger, Text
 
 
-from apps.webui.internal.db import DB
+from apps.webui.internal.db import Base, get_db
 
 
 import json
 import json
 
 
@@ -22,20 +19,21 @@ log.setLevel(SRC_LOG_LEVELS["MODELS"])
 ####################
 ####################
 
 
 
 
-class Document(Model):
-    collection_name = CharField(unique=True)
-    name = CharField(unique=True)
-    title = TextField()
-    filename = TextField()
-    content = TextField(null=True)
-    user_id = CharField()
-    timestamp = BigIntegerField()
+class Document(Base):
+    __tablename__ = "document"
 
 
-    class Meta:
-        database = DB
+    collection_name = Column(String, primary_key=True)
+    name = Column(String, unique=True)
+    title = Column(Text)
+    filename = Column(Text)
+    content = Column(Text, nullable=True)
+    user_id = Column(String)
+    timestamp = Column(BigInteger)
 
 
 
 
 class DocumentModel(BaseModel):
 class DocumentModel(BaseModel):
+    model_config = ConfigDict(from_attributes=True)
+
     collection_name: str
     collection_name: str
     name: str
     name: str
     title: str
     title: str
@@ -72,57 +70,63 @@ class DocumentForm(DocumentUpdateForm):
 
 
 
 
 class DocumentsTable:
 class DocumentsTable:
-    def __init__(self, db):
-        self.db = db
-        self.db.create_tables([Document])
 
 
     def insert_new_doc(
     def insert_new_doc(
         self, user_id: str, form_data: DocumentForm
         self, user_id: str, form_data: DocumentForm
     ) -> Optional[DocumentModel]:
     ) -> Optional[DocumentModel]:
-        document = DocumentModel(
-            **{
-                **form_data.model_dump(),
-                "user_id": user_id,
-                "timestamp": int(time.time()),
-            }
-        )
-
-        try:
-            result = Document.create(**document.model_dump())
-            if result:
-                return document
-            else:
+        with get_db() as db:
+
+            document = DocumentModel(
+                **{
+                    **form_data.model_dump(),
+                    "user_id": user_id,
+                    "timestamp": int(time.time()),
+                }
+            )
+
+            try:
+                result = Document(**document.model_dump())
+                db.add(result)
+                db.commit()
+                db.refresh(result)
+                if result:
+                    return DocumentModel.model_validate(result)
+                else:
+                    return None
+            except:
                 return None
                 return None
-        except:
-            return None
 
 
     def get_doc_by_name(self, name: str) -> Optional[DocumentModel]:
     def get_doc_by_name(self, name: str) -> Optional[DocumentModel]:
         try:
         try:
-            document = Document.get(Document.name == name)
-            return DocumentModel(**model_to_dict(document))
+            with get_db() as db:
+
+                document = db.query(Document).filter_by(name=name).first()
+                return DocumentModel.model_validate(document) if document else None
         except:
         except:
             return None
             return None
 
 
     def get_docs(self) -> List[DocumentModel]:
     def get_docs(self) -> List[DocumentModel]:
-        return [
-            DocumentModel(**model_to_dict(doc))
-            for doc in Document.select()
-            # .limit(limit).offset(skip)
-        ]
+        with get_db() as db:
+
+            return [
+                DocumentModel.model_validate(doc) for doc in db.query(Document).all()
+            ]
 
 
     def update_doc_by_name(
     def update_doc_by_name(
         self, name: str, form_data: DocumentUpdateForm
         self, name: str, form_data: DocumentUpdateForm
     ) -> Optional[DocumentModel]:
     ) -> Optional[DocumentModel]:
         try:
         try:
-            query = Document.update(
-                title=form_data.title,
-                name=form_data.name,
-                timestamp=int(time.time()),
-            ).where(Document.name == name)
-            query.execute()
-
-            doc = Document.get(Document.name == form_data.name)
-            return DocumentModel(**model_to_dict(doc))
+            with get_db() as db:
+
+                db.query(Document).filter_by(name=name).update(
+                    {
+                        "title": form_data.title,
+                        "name": form_data.name,
+                        "timestamp": int(time.time()),
+                    }
+                )
+                db.commit()
+                return self.get_doc_by_name(form_data.name)
         except Exception as e:
         except Exception as e:
             log.exception(e)
             log.exception(e)
             return None
             return None
@@ -135,26 +139,29 @@ class DocumentsTable:
             doc_content = json.loads(doc.content if doc.content else "{}")
             doc_content = json.loads(doc.content if doc.content else "{}")
             doc_content = {**doc_content, **updated}
             doc_content = {**doc_content, **updated}
 
 
-            query = Document.update(
-                content=json.dumps(doc_content),
-                timestamp=int(time.time()),
-            ).where(Document.name == name)
-            query.execute()
+            with get_db() as db:
 
 
-            doc = Document.get(Document.name == name)
-            return DocumentModel(**model_to_dict(doc))
+                db.query(Document).filter_by(name=name).update(
+                    {
+                        "content": json.dumps(doc_content),
+                        "timestamp": int(time.time()),
+                    }
+                )
+                db.commit()
+                return self.get_doc_by_name(name)
         except Exception as e:
         except Exception as e:
             log.exception(e)
             log.exception(e)
             return None
             return None
 
 
     def delete_doc_by_name(self, name: str) -> bool:
     def delete_doc_by_name(self, name: str) -> bool:
         try:
         try:
-            query = Document.delete().where((Document.name == name))
-            query.execute()  # Remove the rows, return number of rows removed.
+            with get_db() as db:
 
 
-            return True
+                db.query(Document).filter_by(name=name).delete()
+                db.commit()
+                return True
         except:
         except:
             return False
             return False
 
 
 
 
-Documents = DocumentsTable(DB)
+Documents = DocumentsTable()

+ 64 - 50
backend/apps/webui/models/files.py

@@ -1,10 +1,11 @@
-from pydantic import BaseModel
-from peewee import *
-from playhouse.shortcuts import model_to_dict
+from pydantic import BaseModel, ConfigDict
 from typing import List, Union, Optional
 from typing import List, Union, Optional
 import time
 import time
 import logging
 import logging
-from apps.webui.internal.db import DB, JSONField
+
+from sqlalchemy import Column, String, BigInteger, Text
+
+from apps.webui.internal.db import JSONField, Base, get_db
 
 
 import json
 import json
 
 
@@ -18,15 +19,14 @@ log.setLevel(SRC_LOG_LEVELS["MODELS"])
 ####################
 ####################
 
 
 
 
-class File(Model):
-    id = CharField(unique=True)
-    user_id = CharField()
-    filename = TextField()
-    meta = JSONField()
-    created_at = BigIntegerField()
+class File(Base):
+    __tablename__ = "file"
 
 
-    class Meta:
-        database = DB
+    id = Column(String, primary_key=True)
+    user_id = Column(String)
+    filename = Column(Text)
+    meta = Column(JSONField)
+    created_at = Column(BigInteger)
 
 
 
 
 class FileModel(BaseModel):
 class FileModel(BaseModel):
@@ -36,6 +36,8 @@ class FileModel(BaseModel):
     meta: dict
     meta: dict
     created_at: int  # timestamp in epoch
     created_at: int  # timestamp in epoch
 
 
+    model_config = ConfigDict(from_attributes=True)
+
 
 
 ####################
 ####################
 # Forms
 # Forms
@@ -57,56 +59,68 @@ class FileForm(BaseModel):
 
 
 
 
 class FilesTable:
 class FilesTable:
-    def __init__(self, db):
-        self.db = db
-        self.db.create_tables([File])
 
 
     def insert_new_file(self, user_id: str, form_data: FileForm) -> Optional[FileModel]:
     def insert_new_file(self, user_id: str, form_data: FileForm) -> Optional[FileModel]:
-        file = FileModel(
-            **{
-                **form_data.model_dump(),
-                "user_id": user_id,
-                "created_at": int(time.time()),
-            }
-        )
-
-        try:
-            result = File.create(**file.model_dump())
-            if result:
-                return file
-            else:
+        with get_db() as db:
+
+            file = FileModel(
+                **{
+                    **form_data.model_dump(),
+                    "user_id": user_id,
+                    "created_at": int(time.time()),
+                }
+            )
+
+            try:
+                result = File(**file.model_dump())
+                db.add(result)
+                db.commit()
+                db.refresh(result)
+                if result:
+                    return FileModel.model_validate(result)
+                else:
+                    return None
+            except Exception as e:
+                print(f"Error creating tool: {e}")
                 return None
                 return None
-        except Exception as e:
-            print(f"Error creating tool: {e}")
-            return None
 
 
     def get_file_by_id(self, id: str) -> Optional[FileModel]:
     def get_file_by_id(self, id: str) -> Optional[FileModel]:
-        try:
-            file = File.get(File.id == id)
-            return FileModel(**model_to_dict(file))
-        except:
-            return None
+        with get_db() as db:
+
+            try:
+                file = db.get(File, id)
+                return FileModel.model_validate(file)
+            except:
+                return None
 
 
     def get_files(self) -> List[FileModel]:
     def get_files(self) -> List[FileModel]:
-        return [FileModel(**model_to_dict(file)) for file in File.select()]
+        with get_db() as db:
+
+            return [FileModel.model_validate(file) for file in db.query(File).all()]
 
 
     def delete_file_by_id(self, id: str) -> bool:
     def delete_file_by_id(self, id: str) -> bool:
-        try:
-            query = File.delete().where((File.id == id))
-            query.execute()  # Remove the rows, return number of rows removed.
 
 
-            return True
-        except:
-            return False
+        with get_db() as db:
+
+            try:
+                db.query(File).filter_by(id=id).delete()
+                db.commit()
+
+                return True
+            except:
+                return False
 
 
     def delete_all_files(self) -> bool:
     def delete_all_files(self) -> bool:
-        try:
-            query = File.delete()
-            query.execute()  # Remove the rows, return number of rows removed.
 
 
-            return True
-        except:
-            return False
+        with get_db() as db:
+
+            try:
+                db.query(File).delete()
+                db.commit()
+
+                return True
+            except:
+                return False
 
 
 
 
-Files = FilesTable(DB)
+Files = FilesTable()

+ 125 - 106
backend/apps/webui/models/functions.py

@@ -1,10 +1,11 @@
-from pydantic import BaseModel
-from peewee import *
-from playhouse.shortcuts import model_to_dict
+from pydantic import BaseModel, ConfigDict
 from typing import List, Union, Optional
 from typing import List, Union, Optional
 import time
 import time
 import logging
 import logging
-from apps.webui.internal.db import DB, JSONField
+
+from sqlalchemy import Column, String, Text, BigInteger, Boolean
+
+from apps.webui.internal.db import JSONField, Base, get_db
 from apps.webui.models.users import Users
 from apps.webui.models.users import Users
 
 
 import json
 import json
@@ -21,21 +22,20 @@ log.setLevel(SRC_LOG_LEVELS["MODELS"])
 ####################
 ####################
 
 
 
 
-class Function(Model):
-    id = CharField(unique=True)
-    user_id = CharField()
-    name = TextField()
-    type = TextField()
-    content = TextField()
-    meta = JSONField()
-    valves = JSONField()
-    is_active = BooleanField(default=False)
-    is_global = BooleanField(default=False)
-    updated_at = BigIntegerField()
-    created_at = BigIntegerField()
+class Function(Base):
+    __tablename__ = "function"
 
 
-    class Meta:
-        database = DB
+    id = Column(String, primary_key=True)
+    user_id = Column(String)
+    name = Column(Text)
+    type = Column(Text)
+    content = Column(Text)
+    meta = Column(JSONField)
+    valves = Column(JSONField)
+    is_active = Column(Boolean)
+    is_global = Column(Boolean)
+    updated_at = Column(BigInteger)
+    created_at = Column(BigInteger)
 
 
 
 
 class FunctionMeta(BaseModel):
 class FunctionMeta(BaseModel):
@@ -55,6 +55,8 @@ class FunctionModel(BaseModel):
     updated_at: int  # timestamp in epoch
     updated_at: int  # timestamp in epoch
     created_at: int  # timestamp in epoch
     created_at: int  # timestamp in epoch
 
 
+    model_config = ConfigDict(from_attributes=True)
+
 
 
 ####################
 ####################
 # Forms
 # Forms
@@ -85,13 +87,11 @@ class FunctionValves(BaseModel):
 
 
 
 
 class FunctionsTable:
 class FunctionsTable:
-    def __init__(self, db):
-        self.db = db
-        self.db.create_tables([Function])
 
 
     def insert_new_function(
     def insert_new_function(
         self, user_id: str, type: str, form_data: FunctionForm
         self, user_id: str, type: str, form_data: FunctionForm
     ) -> Optional[FunctionModel]:
     ) -> Optional[FunctionModel]:
+
         function = FunctionModel(
         function = FunctionModel(
             **{
             **{
                 **form_data.model_dump(),
                 **form_data.model_dump(),
@@ -103,89 +103,102 @@ class FunctionsTable:
         )
         )
 
 
         try:
         try:
-            result = Function.create(**function.model_dump())
-            if result:
-                return function
-            else:
-                return None
+            with get_db() as db:
+                result = Function(**function.model_dump())
+                db.add(result)
+                db.commit()
+                db.refresh(result)
+                if result:
+                    return FunctionModel.model_validate(result)
+                else:
+                    return None
         except Exception as e:
         except Exception as e:
             print(f"Error creating tool: {e}")
             print(f"Error creating tool: {e}")
             return None
             return None
 
 
     def get_function_by_id(self, id: str) -> Optional[FunctionModel]:
     def get_function_by_id(self, id: str) -> Optional[FunctionModel]:
         try:
         try:
-            function = Function.get(Function.id == id)
-            return FunctionModel(**model_to_dict(function))
+            with get_db() as db:
+
+                function = db.get(Function, id)
+                return FunctionModel.model_validate(function)
         except:
         except:
             return None
             return None
 
 
     def get_functions(self, active_only=False) -> List[FunctionModel]:
     def get_functions(self, active_only=False) -> List[FunctionModel]:
-        if active_only:
-            return [
-                FunctionModel(**model_to_dict(function))
-                for function in Function.select().where(Function.is_active == True)
-            ]
-        else:
-            return [
-                FunctionModel(**model_to_dict(function))
-                for function in Function.select()
-            ]
+        with get_db() as db:
+
+            if active_only:
+                return [
+                    FunctionModel.model_validate(function)
+                    for function in db.query(Function).filter_by(is_active=True).all()
+                ]
+            else:
+                return [
+                    FunctionModel.model_validate(function)
+                    for function in db.query(Function).all()
+                ]
 
 
     def get_functions_by_type(
     def get_functions_by_type(
         self, type: str, active_only=False
         self, type: str, active_only=False
     ) -> List[FunctionModel]:
     ) -> List[FunctionModel]:
-        if active_only:
-            return [
-                FunctionModel(**model_to_dict(function))
-                for function in Function.select().where(
-                    Function.type == type, Function.is_active == True
-                )
-            ]
-        else:
-            return [
-                FunctionModel(**model_to_dict(function))
-                for function in Function.select().where(Function.type == type)
-            ]
+        with get_db() as db:
+
+            if active_only:
+                return [
+                    FunctionModel.model_validate(function)
+                    for function in db.query(Function)
+                    .filter_by(type=type, is_active=True)
+                    .all()
+                ]
+            else:
+                return [
+                    FunctionModel.model_validate(function)
+                    for function in db.query(Function).filter_by(type=type).all()
+                ]
 
 
     def get_global_filter_functions(self) -> List[FunctionModel]:
     def get_global_filter_functions(self) -> List[FunctionModel]:
-        return [
-            FunctionModel(**model_to_dict(function))
-            for function in Function.select().where(
-                Function.type == "filter",
-                Function.is_active == True,
-                Function.is_global == True,
-            )
-        ]
+        with get_db() as db:
+
+            return [
+                FunctionModel.model_validate(function)
+                for function in db.query(Function)
+                .filter_by(type="filter", is_active=True, is_global=True)
+                .all()
+            ]
 
 
     def get_function_valves_by_id(self, id: str) -> Optional[dict]:
     def get_function_valves_by_id(self, id: str) -> Optional[dict]:
-        try:
-            function = Function.get(Function.id == id)
-            return function.valves if function.valves else {}
-        except Exception as e:
-            print(f"An error occurred: {e}")
-            return None
+        with get_db() as db:
+
+            try:
+                function = db.get(Function, id)
+                return function.valves if function.valves else {}
+            except Exception as e:
+                print(f"An error occurred: {e}")
+                return None
 
 
     def update_function_valves_by_id(
     def update_function_valves_by_id(
         self, id: str, valves: dict
         self, id: str, valves: dict
     ) -> Optional[FunctionValves]:
     ) -> Optional[FunctionValves]:
-        try:
-            query = Function.update(
-                **{"valves": valves},
-                updated_at=int(time.time()),
-            ).where(Function.id == id)
-            query.execute()
-
-            function = Function.get(Function.id == id)
-            return FunctionValves(**model_to_dict(function))
-        except:
-            return None
+        with get_db() as db:
+
+            try:
+                function = db.get(Function, id)
+                function.valves = valves
+                function.updated_at = int(time.time())
+                db.commit()
+                db.refresh(function)
+                return self.get_function_by_id(id)
+            except:
+                return None
 
 
     def get_user_valves_by_id_and_user_id(
     def get_user_valves_by_id_and_user_id(
         self, id: str, user_id: str
         self, id: str, user_id: str
     ) -> Optional[dict]:
     ) -> Optional[dict]:
+
         try:
         try:
             user = Users.get_user_by_id(user_id)
             user = Users.get_user_by_id(user_id)
-            user_settings = user.settings.model_dump()
+            user_settings = user.settings.model_dump() if user.settings else {}
 
 
             # Check if user has "functions" and "valves" settings
             # Check if user has "functions" and "valves" settings
             if "functions" not in user_settings:
             if "functions" not in user_settings:
@@ -201,9 +214,10 @@ class FunctionsTable:
     def update_user_valves_by_id_and_user_id(
     def update_user_valves_by_id_and_user_id(
         self, id: str, user_id: str, valves: dict
         self, id: str, user_id: str, valves: dict
     ) -> Optional[dict]:
     ) -> Optional[dict]:
+
         try:
         try:
             user = Users.get_user_by_id(user_id)
             user = Users.get_user_by_id(user_id)
-            user_settings = user.settings.model_dump()
+            user_settings = user.settings.model_dump() if user.settings else {}
 
 
             # Check if user has "functions" and "valves" settings
             # Check if user has "functions" and "valves" settings
             if "functions" not in user_settings:
             if "functions" not in user_settings:
@@ -222,39 +236,44 @@ class FunctionsTable:
             return None
             return None
 
 
     def update_function_by_id(self, id: str, updated: dict) -> Optional[FunctionModel]:
     def update_function_by_id(self, id: str, updated: dict) -> Optional[FunctionModel]:
-        try:
-            query = Function.update(
-                **updated,
-                updated_at=int(time.time()),
-            ).where(Function.id == id)
-            query.execute()
-
-            function = Function.get(Function.id == id)
-            return FunctionModel(**model_to_dict(function))
-        except:
-            return None
+        with get_db() as db:
+
+            try:
+                db.query(Function).filter_by(id=id).update(
+                    {
+                        **updated,
+                        "updated_at": int(time.time()),
+                    }
+                )
+                db.commit()
+                return self.get_function_by_id(id)
+            except:
+                return None
 
 
     def deactivate_all_functions(self) -> Optional[bool]:
     def deactivate_all_functions(self) -> Optional[bool]:
-        try:
-            query = Function.update(
-                **{"is_active": False},
-                updated_at=int(time.time()),
-            )
-
-            query.execute()
-
-            return True
-        except:
-            return None
+        with get_db() as db:
+
+            try:
+                db.query(Function).update(
+                    {
+                        "is_active": False,
+                        "updated_at": int(time.time()),
+                    }
+                )
+                db.commit()
+                return True
+            except:
+                return None
 
 
     def delete_function_by_id(self, id: str) -> bool:
     def delete_function_by_id(self, id: str) -> bool:
-        try:
-            query = Function.delete().where((Function.id == id))
-            query.execute()  # Remove the rows, return number of rows removed.
+        with get_db() as db:
+            try:
+                db.query(Function).filter_by(id=id).delete()
+                db.commit()
 
 
-            return True
-        except:
-            return False
+                return True
+            except:
+                return False
 
 
 
 
-Functions = FunctionsTable(DB)
+Functions = FunctionsTable()

+ 92 - 76
backend/apps/webui/models/memories.py

@@ -1,10 +1,9 @@
-from pydantic import BaseModel
-from peewee import *
-from playhouse.shortcuts import model_to_dict
+from pydantic import BaseModel, ConfigDict
 from typing import List, Union, Optional
 from typing import List, Union, Optional
 
 
-from apps.webui.internal.db import DB
-from apps.webui.models.chats import Chats
+from sqlalchemy import Column, String, BigInteger, Text
+
+from apps.webui.internal.db import Base, get_db
 
 
 import time
 import time
 import uuid
 import uuid
@@ -14,15 +13,14 @@ import uuid
 ####################
 ####################
 
 
 
 
-class Memory(Model):
-    id = CharField(unique=True)
-    user_id = CharField()
-    content = TextField()
-    updated_at = BigIntegerField()
-    created_at = BigIntegerField()
+class Memory(Base):
+    __tablename__ = "memory"
 
 
-    class Meta:
-        database = DB
+    id = Column(String, primary_key=True)
+    user_id = Column(String)
+    content = Column(Text)
+    updated_at = Column(BigInteger)
+    created_at = Column(BigInteger)
 
 
 
 
 class MemoryModel(BaseModel):
 class MemoryModel(BaseModel):
@@ -32,6 +30,8 @@ class MemoryModel(BaseModel):
     updated_at: int  # timestamp in epoch
     updated_at: int  # timestamp in epoch
     created_at: int  # timestamp in epoch
     created_at: int  # timestamp in epoch
 
 
+    model_config = ConfigDict(from_attributes=True)
+
 
 
 ####################
 ####################
 # Forms
 # Forms
@@ -39,94 +39,110 @@ class MemoryModel(BaseModel):
 
 
 
 
 class MemoriesTable:
 class MemoriesTable:
-    def __init__(self, db):
-        self.db = db
-        self.db.create_tables([Memory])
 
 
     def insert_new_memory(
     def insert_new_memory(
         self,
         self,
         user_id: str,
         user_id: str,
         content: str,
         content: str,
     ) -> Optional[MemoryModel]:
     ) -> Optional[MemoryModel]:
-        id = str(uuid.uuid4())
-
-        memory = MemoryModel(
-            **{
-                "id": id,
-                "user_id": user_id,
-                "content": content,
-                "created_at": int(time.time()),
-                "updated_at": int(time.time()),
-            }
-        )
-        result = Memory.create(**memory.model_dump())
-        if result:
-            return memory
-        else:
-            return None
+
+        with get_db() as db:
+            id = str(uuid.uuid4())
+
+            memory = MemoryModel(
+                **{
+                    "id": id,
+                    "user_id": user_id,
+                    "content": content,
+                    "created_at": int(time.time()),
+                    "updated_at": int(time.time()),
+                }
+            )
+            result = Memory(**memory.model_dump())
+            db.add(result)
+            db.commit()
+            db.refresh(result)
+            if result:
+                return MemoryModel.model_validate(result)
+            else:
+                return None
 
 
     def update_memory_by_id(
     def update_memory_by_id(
         self,
         self,
         id: str,
         id: str,
         content: str,
         content: str,
     ) -> Optional[MemoryModel]:
     ) -> Optional[MemoryModel]:
-        try:
-            memory = Memory.get(Memory.id == id)
-            memory.content = content
-            memory.updated_at = int(time.time())
-            memory.save()
-            return MemoryModel(**model_to_dict(memory))
-        except:
-            return None
+        with get_db() as db:
+
+            try:
+                db.query(Memory).filter_by(id=id).update(
+                    {"content": content, "updated_at": int(time.time())}
+                )
+                db.commit()
+                return self.get_memory_by_id(id)
+            except:
+                return None
 
 
     def get_memories(self) -> List[MemoryModel]:
     def get_memories(self) -> List[MemoryModel]:
-        try:
-            memories = Memory.select()
-            return [MemoryModel(**model_to_dict(memory)) for memory in memories]
-        except:
-            return None
+        with get_db() as db:
+
+            try:
+                memories = db.query(Memory).all()
+                return [MemoryModel.model_validate(memory) for memory in memories]
+            except:
+                return None
 
 
     def get_memories_by_user_id(self, user_id: str) -> List[MemoryModel]:
     def get_memories_by_user_id(self, user_id: str) -> List[MemoryModel]:
-        try:
-            memories = Memory.select().where(Memory.user_id == user_id)
-            return [MemoryModel(**model_to_dict(memory)) for memory in memories]
-        except:
-            return None
-
-    def get_memory_by_id(self, id) -> Optional[MemoryModel]:
-        try:
-            memory = Memory.get(Memory.id == id)
-            return MemoryModel(**model_to_dict(memory))
-        except:
-            return None
+        with get_db() as db:
+
+            try:
+                memories = db.query(Memory).filter_by(user_id=user_id).all()
+                return [MemoryModel.model_validate(memory) for memory in memories]
+            except:
+                return None
+
+    def get_memory_by_id(self, id: str) -> Optional[MemoryModel]:
+        with get_db() as db:
+
+            try:
+                memory = db.get(Memory, id)
+                return MemoryModel.model_validate(memory)
+            except:
+                return None
 
 
     def delete_memory_by_id(self, id: str) -> bool:
     def delete_memory_by_id(self, id: str) -> bool:
-        try:
-            query = Memory.delete().where(Memory.id == id)
-            query.execute()  # Remove the rows, return number of rows removed.
+        with get_db() as db:
+
+            try:
+                db.query(Memory).filter_by(id=id).delete()
+                db.commit()
 
 
-            return True
+                return True
 
 
-        except:
-            return False
+            except:
+                return False
 
 
     def delete_memories_by_user_id(self, user_id: str) -> bool:
     def delete_memories_by_user_id(self, user_id: str) -> bool:
-        try:
-            query = Memory.delete().where(Memory.user_id == user_id)
-            query.execute()
+        with get_db() as db:
 
 
-            return True
-        except:
-            return False
+            try:
+                db.query(Memory).filter_by(user_id=user_id).delete()
+                db.commit()
+
+                return True
+            except:
+                return False
 
 
     def delete_memory_by_id_and_user_id(self, id: str, user_id: str) -> bool:
     def delete_memory_by_id_and_user_id(self, id: str, user_id: str) -> bool:
-        try:
-            query = Memory.delete().where(Memory.id == id, Memory.user_id == user_id)
-            query.execute()
+        with get_db() as db:
+
+            try:
+                db.query(Memory).filter_by(id=id, user_id=user_id).delete()
+                db.commit()
 
 
-            return True
-        except:
-            return False
+                return True
+            except:
+                return False
 
 
 
 
-Memories = MemoriesTable(DB)
+Memories = MemoriesTable()

+ 53 - 42
backend/apps/webui/models/models.py

@@ -2,13 +2,10 @@ import json
 import logging
 import logging
 from typing import Optional
 from typing import Optional
 
 
-import peewee as pw
-from peewee import *
-
-from playhouse.shortcuts import model_to_dict
 from pydantic import BaseModel, ConfigDict
 from pydantic import BaseModel, ConfigDict
+from sqlalchemy import String, Column, BigInteger, Text
 
 
-from apps.webui.internal.db import DB, JSONField
+from apps.webui.internal.db import Base, JSONField, get_db
 
 
 from typing import List, Union, Optional
 from typing import List, Union, Optional
 from config import SRC_LOG_LEVELS
 from config import SRC_LOG_LEVELS
@@ -32,7 +29,7 @@ class ModelParams(BaseModel):
 
 
 # ModelMeta is a model for the data stored in the meta field of the Model table
 # ModelMeta is a model for the data stored in the meta field of the Model table
 class ModelMeta(BaseModel):
 class ModelMeta(BaseModel):
-    profile_image_url: Optional[str] = "/favicon.png"
+    profile_image_url: Optional[str] = "/static/favicon.png"
 
 
     description: Optional[str] = None
     description: Optional[str] = None
     """
     """
@@ -46,38 +43,37 @@ class ModelMeta(BaseModel):
     pass
     pass
 
 
 
 
-class Model(pw.Model):
-    id = pw.TextField(unique=True)
+class Model(Base):
+    __tablename__ = "model"
+
+    id = Column(Text, primary_key=True)
     """
     """
         The model's id as used in the API. If set to an existing model, it will override the model.
         The model's id as used in the API. If set to an existing model, it will override the model.
     """
     """
-    user_id = pw.TextField()
+    user_id = Column(Text)
 
 
-    base_model_id = pw.TextField(null=True)
+    base_model_id = Column(Text, nullable=True)
     """
     """
         An optional pointer to the actual model that should be used when proxying requests.
         An optional pointer to the actual model that should be used when proxying requests.
     """
     """
 
 
-    name = pw.TextField()
+    name = Column(Text)
     """
     """
         The human-readable display name of the model.
         The human-readable display name of the model.
     """
     """
 
 
-    params = JSONField()
+    params = Column(JSONField)
     """
     """
         Holds a JSON encoded blob of parameters, see `ModelParams`.
         Holds a JSON encoded blob of parameters, see `ModelParams`.
     """
     """
 
 
-    meta = JSONField()
+    meta = Column(JSONField)
     """
     """
         Holds a JSON encoded blob of metadata, see `ModelMeta`.
         Holds a JSON encoded blob of metadata, see `ModelMeta`.
     """
     """
 
 
-    updated_at = BigIntegerField()
-    created_at = BigIntegerField()
-
-    class Meta:
-        database = DB
+    updated_at = Column(BigInteger)
+    created_at = Column(BigInteger)
 
 
 
 
 class ModelModel(BaseModel):
 class ModelModel(BaseModel):
@@ -92,6 +88,8 @@ class ModelModel(BaseModel):
     updated_at: int  # timestamp in epoch
     updated_at: int  # timestamp in epoch
     created_at: int  # timestamp in epoch
     created_at: int  # timestamp in epoch
 
 
+    model_config = ConfigDict(from_attributes=True)
+
 
 
 ####################
 ####################
 # Forms
 # Forms
@@ -115,12 +113,6 @@ class ModelForm(BaseModel):
 
 
 
 
 class ModelsTable:
 class ModelsTable:
-    def __init__(
-        self,
-        db: pw.SqliteDatabase | pw.PostgresqlDatabase,
-    ):
-        self.db = db
-        self.db.create_tables([Model])
 
 
     def insert_new_model(
     def insert_new_model(
         self, form_data: ModelForm, user_id: str
         self, form_data: ModelForm, user_id: str
@@ -134,34 +126,50 @@ class ModelsTable:
             }
             }
         )
         )
         try:
         try:
-            result = Model.create(**model.model_dump())
 
 
-            if result:
-                return model
-            else:
-                return None
+            with get_db() as db:
+
+                result = Model(**model.model_dump())
+                db.add(result)
+                db.commit()
+                db.refresh(result)
+
+                if result:
+                    return ModelModel.model_validate(result)
+                else:
+                    return None
         except Exception as e:
         except Exception as e:
             print(e)
             print(e)
             return None
             return None
 
 
     def get_all_models(self) -> List[ModelModel]:
     def get_all_models(self) -> List[ModelModel]:
-        return [ModelModel(**model_to_dict(model)) for model in Model.select()]
+        with get_db() as db:
+
+            return [ModelModel.model_validate(model) for model in db.query(Model).all()]
 
 
     def get_model_by_id(self, id: str) -> Optional[ModelModel]:
     def get_model_by_id(self, id: str) -> Optional[ModelModel]:
         try:
         try:
-            model = Model.get(Model.id == id)
-            return ModelModel(**model_to_dict(model))
+            with get_db() as db:
+
+                model = db.get(Model, id)
+                return ModelModel.model_validate(model)
         except:
         except:
             return None
             return None
 
 
     def update_model_by_id(self, id: str, model: ModelForm) -> Optional[ModelModel]:
     def update_model_by_id(self, id: str, model: ModelForm) -> Optional[ModelModel]:
         try:
         try:
-            # update only the fields that are present in the model
-            query = Model.update(**model.model_dump()).where(Model.id == id)
-            query.execute()
-
-            model = Model.get(Model.id == id)
-            return ModelModel(**model_to_dict(model))
+            with get_db() as db:
+                # update only the fields that are present in the model
+                result = (
+                    db.query(Model)
+                    .filter_by(id=id)
+                    .update(model.model_dump(exclude={"id"}, exclude_none=True))
+                )
+                db.commit()
+
+                model = db.get(Model, id)
+                db.refresh(model)
+                return ModelModel.model_validate(model)
         except Exception as e:
         except Exception as e:
             print(e)
             print(e)
 
 
@@ -169,11 +177,14 @@ class ModelsTable:
 
 
     def delete_model_by_id(self, id: str) -> bool:
     def delete_model_by_id(self, id: str) -> bool:
         try:
         try:
-            query = Model.delete().where(Model.id == id)
-            query.execute()
-            return True
+            with get_db() as db:
+
+                db.query(Model).filter_by(id=id).delete()
+                db.commit()
+
+                return True
         except:
         except:
             return False
             return False
 
 
 
 
-Models = ModelsTable(DB)
+Models = ModelsTable()

+ 47 - 46
backend/apps/webui/models/prompts.py

@@ -1,13 +1,10 @@
-from pydantic import BaseModel
-from peewee import *
-from playhouse.shortcuts import model_to_dict
-from typing import List, Union, Optional
+from pydantic import BaseModel, ConfigDict
+from typing import List, Optional
 import time
 import time
 
 
-from utils.utils import decode_token
-from utils.misc import get_gravatar_url
+from sqlalchemy import String, Column, BigInteger, Text
 
 
-from apps.webui.internal.db import DB
+from apps.webui.internal.db import Base, get_db
 
 
 import json
 import json
 
 
@@ -16,15 +13,14 @@ import json
 ####################
 ####################
 
 
 
 
-class Prompt(Model):
-    command = CharField(unique=True)
-    user_id = CharField()
-    title = TextField()
-    content = TextField()
-    timestamp = BigIntegerField()
+class Prompt(Base):
+    __tablename__ = "prompt"
 
 
-    class Meta:
-        database = DB
+    command = Column(String, primary_key=True)
+    user_id = Column(String)
+    title = Column(Text)
+    content = Column(Text)
+    timestamp = Column(BigInteger)
 
 
 
 
 class PromptModel(BaseModel):
 class PromptModel(BaseModel):
@@ -34,6 +30,8 @@ class PromptModel(BaseModel):
     content: str
     content: str
     timestamp: int  # timestamp in epoch
     timestamp: int  # timestamp in epoch
 
 
+    model_config = ConfigDict(from_attributes=True)
+
 
 
 ####################
 ####################
 # Forms
 # Forms
@@ -48,10 +46,6 @@ class PromptForm(BaseModel):
 
 
 class PromptsTable:
 class PromptsTable:
 
 
-    def __init__(self, db):
-        self.db = db
-        self.db.create_tables([Prompt])
-
     def insert_new_prompt(
     def insert_new_prompt(
         self, user_id: str, form_data: PromptForm
         self, user_id: str, form_data: PromptForm
     ) -> Optional[PromptModel]:
     ) -> Optional[PromptModel]:
@@ -66,53 +60,60 @@ class PromptsTable:
         )
         )
 
 
         try:
         try:
-            result = Prompt.create(**prompt.model_dump())
-            if result:
-                return prompt
-            else:
-                return None
-        except:
+            with get_db() as db:
+
+                result = Prompt(**prompt.dict())
+                db.add(result)
+                db.commit()
+                db.refresh(result)
+                if result:
+                    return PromptModel.model_validate(result)
+                else:
+                    return None
+        except Exception as e:
             return None
             return None
 
 
     def get_prompt_by_command(self, command: str) -> Optional[PromptModel]:
     def get_prompt_by_command(self, command: str) -> Optional[PromptModel]:
         try:
         try:
-            prompt = Prompt.get(Prompt.command == command)
-            return PromptModel(**model_to_dict(prompt))
+            with get_db() as db:
+
+                prompt = db.query(Prompt).filter_by(command=command).first()
+                return PromptModel.model_validate(prompt)
         except:
         except:
             return None
             return None
 
 
     def get_prompts(self) -> List[PromptModel]:
     def get_prompts(self) -> List[PromptModel]:
-        return [
-            PromptModel(**model_to_dict(prompt))
-            for prompt in Prompt.select()
-            # .limit(limit).offset(skip)
-        ]
+        with get_db() as db:
+
+            return [
+                PromptModel.model_validate(prompt) for prompt in db.query(Prompt).all()
+            ]
 
 
     def update_prompt_by_command(
     def update_prompt_by_command(
         self, command: str, form_data: PromptForm
         self, command: str, form_data: PromptForm
     ) -> Optional[PromptModel]:
     ) -> Optional[PromptModel]:
         try:
         try:
-            query = Prompt.update(
-                title=form_data.title,
-                content=form_data.content,
-                timestamp=int(time.time()),
-            ).where(Prompt.command == command)
-
-            query.execute()
-
-            prompt = Prompt.get(Prompt.command == command)
-            return PromptModel(**model_to_dict(prompt))
+            with get_db() as db:
+
+                prompt = db.query(Prompt).filter_by(command=command).first()
+                prompt.title = form_data.title
+                prompt.content = form_data.content
+                prompt.timestamp = int(time.time())
+                db.commit()
+                return PromptModel.model_validate(prompt)
         except:
         except:
             return None
             return None
 
 
     def delete_prompt_by_command(self, command: str) -> bool:
     def delete_prompt_by_command(self, command: str) -> bool:
         try:
         try:
-            query = Prompt.delete().where((Prompt.command == command))
-            query.execute()  # Remove the rows, return number of rows removed.
+            with get_db() as db:
+
+                db.query(Prompt).filter_by(command=command).delete()
+                db.commit()
 
 
-            return True
+                return True
         except:
         except:
             return False
             return False
 
 
 
 
-Prompts = PromptsTable(DB)
+Prompts = PromptsTable()

+ 141 - 106
backend/apps/webui/models/tags.py

@@ -1,14 +1,14 @@
-from pydantic import BaseModel
-from typing import List, Union, Optional
-from peewee import *
-from playhouse.shortcuts import model_to_dict
+from pydantic import BaseModel, ConfigDict
+from typing import List, Optional
 
 
 import json
 import json
 import uuid
 import uuid
 import time
 import time
 import logging
 import logging
 
 
-from apps.webui.internal.db import DB
+from sqlalchemy import String, Column, BigInteger, Text
+
+from apps.webui.internal.db import Base, get_db
 
 
 from config import SRC_LOG_LEVELS
 from config import SRC_LOG_LEVELS
 
 
@@ -20,25 +20,23 @@ log.setLevel(SRC_LOG_LEVELS["MODELS"])
 ####################
 ####################
 
 
 
 
-class Tag(Model):
-    id = CharField(unique=True)
-    name = CharField()
-    user_id = CharField()
-    data = TextField(null=True)
+class Tag(Base):
+    __tablename__ = "tag"
 
 
-    class Meta:
-        database = DB
+    id = Column(String, primary_key=True)
+    name = Column(String)
+    user_id = Column(String)
+    data = Column(Text, nullable=True)
 
 
 
 
-class ChatIdTag(Model):
-    id = CharField(unique=True)
-    tag_name = CharField()
-    chat_id = CharField()
-    user_id = CharField()
-    timestamp = BigIntegerField()
+class ChatIdTag(Base):
+    __tablename__ = "chatidtag"
 
 
-    class Meta:
-        database = DB
+    id = Column(String, primary_key=True)
+    tag_name = Column(String)
+    chat_id = Column(String)
+    user_id = Column(String)
+    timestamp = Column(BigInteger)
 
 
 
 
 class TagModel(BaseModel):
 class TagModel(BaseModel):
@@ -47,6 +45,8 @@ class TagModel(BaseModel):
     user_id: str
     user_id: str
     data: Optional[str] = None
     data: Optional[str] = None
 
 
+    model_config = ConfigDict(from_attributes=True)
+
 
 
 class ChatIdTagModel(BaseModel):
 class ChatIdTagModel(BaseModel):
     id: str
     id: str
@@ -55,6 +55,8 @@ class ChatIdTagModel(BaseModel):
     user_id: str
     user_id: str
     timestamp: int
     timestamp: int
 
 
+    model_config = ConfigDict(from_attributes=True)
+
 
 
 ####################
 ####################
 # Forms
 # Forms
@@ -75,28 +77,31 @@ class ChatTagsResponse(BaseModel):
 
 
 
 
 class TagTable:
 class TagTable:
-    def __init__(self, db):
-        self.db = db
-        db.create_tables([Tag, ChatIdTag])
 
 
     def insert_new_tag(self, name: str, user_id: str) -> Optional[TagModel]:
     def insert_new_tag(self, name: str, user_id: str) -> Optional[TagModel]:
-        id = str(uuid.uuid4())
-        tag = TagModel(**{"id": id, "user_id": user_id, "name": name})
-        try:
-            result = Tag.create(**tag.model_dump())
-            if result:
-                return tag
-            else:
+        with get_db() as db:
+
+            id = str(uuid.uuid4())
+            tag = TagModel(**{"id": id, "user_id": user_id, "name": name})
+            try:
+                result = Tag(**tag.model_dump())
+                db.add(result)
+                db.commit()
+                db.refresh(result)
+                if result:
+                    return TagModel.model_validate(result)
+                else:
+                    return None
+            except Exception as e:
                 return None
                 return None
-        except Exception as e:
-            return None
 
 
     def get_tag_by_name_and_user_id(
     def get_tag_by_name_and_user_id(
         self, name: str, user_id: str
         self, name: str, user_id: str
     ) -> Optional[TagModel]:
     ) -> Optional[TagModel]:
         try:
         try:
-            tag = Tag.get(Tag.name == name, Tag.user_id == user_id)
-            return TagModel(**model_to_dict(tag))
+            with get_db() as db:
+                tag = db.query(Tag).filter(name=name, user_id=user_id).first()
+                return TagModel.model_validate(tag)
         except Exception as e:
         except Exception as e:
             return None
             return None
 
 
@@ -118,82 +123,110 @@ class TagTable:
             }
             }
         )
         )
         try:
         try:
-            result = ChatIdTag.create(**chatIdTag.model_dump())
-            if result:
-                return chatIdTag
-            else:
-                return None
+            with get_db() as db:
+                result = ChatIdTag(**chatIdTag.model_dump())
+                db.add(result)
+                db.commit()
+                db.refresh(result)
+                if result:
+                    return ChatIdTagModel.model_validate(result)
+                else:
+                    return None
         except:
         except:
             return None
             return None
 
 
     def get_tags_by_user_id(self, user_id: str) -> List[TagModel]:
     def get_tags_by_user_id(self, user_id: str) -> List[TagModel]:
-        tag_names = [
-            ChatIdTagModel(**model_to_dict(chat_id_tag)).tag_name
-            for chat_id_tag in ChatIdTag.select()
-            .where(ChatIdTag.user_id == user_id)
-            .order_by(ChatIdTag.timestamp.desc())
-        ]
-
-        return [
-            TagModel(**model_to_dict(tag))
-            for tag in Tag.select()
-            .where(Tag.user_id == user_id)
-            .where(Tag.name.in_(tag_names))
-        ]
+        with get_db() as db:
+            tag_names = [
+                chat_id_tag.tag_name
+                for chat_id_tag in (
+                    db.query(ChatIdTag)
+                    .filter_by(user_id=user_id)
+                    .order_by(ChatIdTag.timestamp.desc())
+                    .all()
+                )
+            ]
+
+            return [
+                TagModel.model_validate(tag)
+                for tag in (
+                    db.query(Tag)
+                    .filter_by(user_id=user_id)
+                    .filter(Tag.name.in_(tag_names))
+                    .all()
+                )
+            ]
 
 
     def get_tags_by_chat_id_and_user_id(
     def get_tags_by_chat_id_and_user_id(
         self, chat_id: str, user_id: str
         self, chat_id: str, user_id: str
     ) -> List[TagModel]:
     ) -> List[TagModel]:
-        tag_names = [
-            ChatIdTagModel(**model_to_dict(chat_id_tag)).tag_name
-            for chat_id_tag in ChatIdTag.select()
-            .where((ChatIdTag.user_id == user_id) & (ChatIdTag.chat_id == chat_id))
-            .order_by(ChatIdTag.timestamp.desc())
-        ]
-
-        return [
-            TagModel(**model_to_dict(tag))
-            for tag in Tag.select()
-            .where(Tag.user_id == user_id)
-            .where(Tag.name.in_(tag_names))
-        ]
+        with get_db() as db:
+
+            tag_names = [
+                chat_id_tag.tag_name
+                for chat_id_tag in (
+                    db.query(ChatIdTag)
+                    .filter_by(user_id=user_id, chat_id=chat_id)
+                    .order_by(ChatIdTag.timestamp.desc())
+                    .all()
+                )
+            ]
+
+            return [
+                TagModel.model_validate(tag)
+                for tag in (
+                    db.query(Tag)
+                    .filter_by(user_id=user_id)
+                    .filter(Tag.name.in_(tag_names))
+                    .all()
+                )
+            ]
 
 
     def get_chat_ids_by_tag_name_and_user_id(
     def get_chat_ids_by_tag_name_and_user_id(
         self, tag_name: str, user_id: str
         self, tag_name: str, user_id: str
-    ) -> Optional[ChatIdTagModel]:
-        return [
-            ChatIdTagModel(**model_to_dict(chat_id_tag))
-            for chat_id_tag in ChatIdTag.select()
-            .where((ChatIdTag.user_id == user_id) & (ChatIdTag.tag_name == tag_name))
-            .order_by(ChatIdTag.timestamp.desc())
-        ]
+    ) -> List[ChatIdTagModel]:
+        with get_db() as db:
+
+            return [
+                ChatIdTagModel.model_validate(chat_id_tag)
+                for chat_id_tag in (
+                    db.query(ChatIdTag)
+                    .filter_by(user_id=user_id, tag_name=tag_name)
+                    .order_by(ChatIdTag.timestamp.desc())
+                    .all()
+                )
+            ]
 
 
     def count_chat_ids_by_tag_name_and_user_id(
     def count_chat_ids_by_tag_name_and_user_id(
         self, tag_name: str, user_id: str
         self, tag_name: str, user_id: str
     ) -> int:
     ) -> int:
-        return (
-            ChatIdTag.select()
-            .where((ChatIdTag.tag_name == tag_name) & (ChatIdTag.user_id == user_id))
-            .count()
-        )
+        with get_db() as db:
+
+            return (
+                db.query(ChatIdTag)
+                .filter_by(tag_name=tag_name, user_id=user_id)
+                .count()
+            )
 
 
     def delete_tag_by_tag_name_and_user_id(self, tag_name: str, user_id: str) -> bool:
     def delete_tag_by_tag_name_and_user_id(self, tag_name: str, user_id: str) -> bool:
         try:
         try:
-            query = ChatIdTag.delete().where(
-                (ChatIdTag.tag_name == tag_name) & (ChatIdTag.user_id == user_id)
-            )
-            res = query.execute()  # Remove the rows, return number of rows removed.
-            log.debug(f"res: {res}")
-
-            tag_count = self.count_chat_ids_by_tag_name_and_user_id(tag_name, user_id)
-            if tag_count == 0:
-                # Remove tag item from Tag col as well
-                query = Tag.delete().where(
-                    (Tag.name == tag_name) & (Tag.user_id == user_id)
+            with get_db() as db:
+                res = (
+                    db.query(ChatIdTag)
+                    .filter_by(tag_name=tag_name, user_id=user_id)
+                    .delete()
                 )
                 )
-                query.execute()  # Remove the rows, return number of rows removed.
+                log.debug(f"res: {res}")
+                db.commit()
 
 
-            return True
+                tag_count = self.count_chat_ids_by_tag_name_and_user_id(
+                    tag_name, user_id
+                )
+                if tag_count == 0:
+                    # Remove tag item from Tag col as well
+                    db.query(Tag).filter_by(name=tag_name, user_id=user_id).delete()
+                    db.commit()
+                return True
         except Exception as e:
         except Exception as e:
             log.error(f"delete_tag: {e}")
             log.error(f"delete_tag: {e}")
             return False
             return False
@@ -202,23 +235,25 @@ class TagTable:
         self, tag_name: str, chat_id: str, user_id: str
         self, tag_name: str, chat_id: str, user_id: str
     ) -> bool:
     ) -> bool:
         try:
         try:
-            query = ChatIdTag.delete().where(
-                (ChatIdTag.tag_name == tag_name)
-                & (ChatIdTag.chat_id == chat_id)
-                & (ChatIdTag.user_id == user_id)
-            )
-            res = query.execute()  # Remove the rows, return number of rows removed.
-            log.debug(f"res: {res}")
-
-            tag_count = self.count_chat_ids_by_tag_name_and_user_id(tag_name, user_id)
-            if tag_count == 0:
-                # Remove tag item from Tag col as well
-                query = Tag.delete().where(
-                    (Tag.name == tag_name) & (Tag.user_id == user_id)
+            with get_db() as db:
+
+                res = (
+                    db.query(ChatIdTag)
+                    .filter_by(tag_name=tag_name, chat_id=chat_id, user_id=user_id)
+                    .delete()
+                )
+                log.debug(f"res: {res}")
+                db.commit()
+
+                tag_count = self.count_chat_ids_by_tag_name_and_user_id(
+                    tag_name, user_id
                 )
                 )
-                query.execute()  # Remove the rows, return number of rows removed.
+                if tag_count == 0:
+                    # Remove tag item from Tag col as well
+                    db.query(Tag).filter_by(name=tag_name, user_id=user_id).delete()
+                    db.commit()
 
 
-            return True
+                return True
         except Exception as e:
         except Exception as e:
             log.error(f"delete_tag: {e}")
             log.error(f"delete_tag: {e}")
             return False
             return False
@@ -234,4 +269,4 @@ class TagTable:
         return True
         return True
 
 
 
 
-Tags = TagTable(DB)
+Tags = TagTable()

+ 74 - 64
backend/apps/webui/models/tools.py

@@ -1,10 +1,10 @@
-from pydantic import BaseModel
-from peewee import *
-from playhouse.shortcuts import model_to_dict
-from typing import List, Union, Optional
+from pydantic import BaseModel, ConfigDict
+from typing import List, Optional
 import time
 import time
 import logging
 import logging
-from apps.webui.internal.db import DB, JSONField
+from sqlalchemy import String, Column, BigInteger, Text
+
+from apps.webui.internal.db import Base, JSONField, get_db
 from apps.webui.models.users import Users
 from apps.webui.models.users import Users
 
 
 import json
 import json
@@ -21,19 +21,18 @@ log.setLevel(SRC_LOG_LEVELS["MODELS"])
 ####################
 ####################
 
 
 
 
-class Tool(Model):
-    id = CharField(unique=True)
-    user_id = CharField()
-    name = TextField()
-    content = TextField()
-    specs = JSONField()
-    meta = JSONField()
-    valves = JSONField()
-    updated_at = BigIntegerField()
-    created_at = BigIntegerField()
+class Tool(Base):
+    __tablename__ = "tool"
 
 
-    class Meta:
-        database = DB
+    id = Column(String, primary_key=True)
+    user_id = Column(String)
+    name = Column(Text)
+    content = Column(Text)
+    specs = Column(JSONField)
+    meta = Column(JSONField)
+    valves = Column(JSONField)
+    updated_at = Column(BigInteger)
+    created_at = Column(BigInteger)
 
 
 
 
 class ToolMeta(BaseModel):
 class ToolMeta(BaseModel):
@@ -51,6 +50,8 @@ class ToolModel(BaseModel):
     updated_at: int  # timestamp in epoch
     updated_at: int  # timestamp in epoch
     created_at: int  # timestamp in epoch
     created_at: int  # timestamp in epoch
 
 
+    model_config = ConfigDict(from_attributes=True)
+
 
 
 ####################
 ####################
 # Forms
 # Forms
@@ -78,61 +79,68 @@ class ToolValves(BaseModel):
 
 
 
 
 class ToolsTable:
 class ToolsTable:
-    def __init__(self, db):
-        self.db = db
-        self.db.create_tables([Tool])
 
 
     def insert_new_tool(
     def insert_new_tool(
         self, user_id: str, form_data: ToolForm, specs: List[dict]
         self, user_id: str, form_data: ToolForm, specs: List[dict]
     ) -> Optional[ToolModel]:
     ) -> Optional[ToolModel]:
-        tool = ToolModel(
-            **{
-                **form_data.model_dump(),
-                "specs": specs,
-                "user_id": user_id,
-                "updated_at": int(time.time()),
-                "created_at": int(time.time()),
-            }
-        )
 
 
-        try:
-            result = Tool.create(**tool.model_dump())
-            if result:
-                return tool
-            else:
+        with get_db() as db:
+
+            tool = ToolModel(
+                **{
+                    **form_data.model_dump(),
+                    "specs": specs,
+                    "user_id": user_id,
+                    "updated_at": int(time.time()),
+                    "created_at": int(time.time()),
+                }
+            )
+
+            try:
+                result = Tool(**tool.model_dump())
+                db.add(result)
+                db.commit()
+                db.refresh(result)
+                if result:
+                    return ToolModel.model_validate(result)
+                else:
+                    return None
+            except Exception as e:
+                print(f"Error creating tool: {e}")
                 return None
                 return None
-        except Exception as e:
-            print(f"Error creating tool: {e}")
-            return None
 
 
     def get_tool_by_id(self, id: str) -> Optional[ToolModel]:
     def get_tool_by_id(self, id: str) -> Optional[ToolModel]:
         try:
         try:
-            tool = Tool.get(Tool.id == id)
-            return ToolModel(**model_to_dict(tool))
+            with get_db() as db:
+
+                tool = db.get(Tool, id)
+                return ToolModel.model_validate(tool)
         except:
         except:
             return None
             return None
 
 
     def get_tools(self) -> List[ToolModel]:
     def get_tools(self) -> List[ToolModel]:
-        return [ToolModel(**model_to_dict(tool)) for tool in Tool.select()]
+        with get_db() as db:
+            return [ToolModel.model_validate(tool) for tool in db.query(Tool).all()]
 
 
     def get_tool_valves_by_id(self, id: str) -> Optional[dict]:
     def get_tool_valves_by_id(self, id: str) -> Optional[dict]:
         try:
         try:
-            tool = Tool.get(Tool.id == id)
-            return tool.valves if tool.valves else {}
+            with get_db() as db:
+
+                tool = db.get(Tool, id)
+                return tool.valves if tool.valves else {}
         except Exception as e:
         except Exception as e:
             print(f"An error occurred: {e}")
             print(f"An error occurred: {e}")
             return None
             return None
 
 
     def update_tool_valves_by_id(self, id: str, valves: dict) -> Optional[ToolValves]:
     def update_tool_valves_by_id(self, id: str, valves: dict) -> Optional[ToolValves]:
         try:
         try:
-            query = Tool.update(
-                **{"valves": valves},
-                updated_at=int(time.time()),
-            ).where(Tool.id == id)
-            query.execute()
-
-            tool = Tool.get(Tool.id == id)
-            return ToolValves(**model_to_dict(tool))
+            with get_db() as db:
+
+                db.query(Tool).filter_by(id=id).update(
+                    {"valves": valves, "updated_at": int(time.time())}
+                )
+                db.commit()
+                return self.get_tool_by_id(id)
         except:
         except:
             return None
             return None
 
 
@@ -141,7 +149,7 @@ class ToolsTable:
     ) -> Optional[dict]:
     ) -> Optional[dict]:
         try:
         try:
             user = Users.get_user_by_id(user_id)
             user = Users.get_user_by_id(user_id)
-            user_settings = user.settings.model_dump()
+            user_settings = user.settings.model_dump() if user.settings else {}
 
 
             # Check if user has "tools" and "valves" settings
             # Check if user has "tools" and "valves" settings
             if "tools" not in user_settings:
             if "tools" not in user_settings:
@@ -159,7 +167,7 @@ class ToolsTable:
     ) -> Optional[dict]:
     ) -> Optional[dict]:
         try:
         try:
             user = Users.get_user_by_id(user_id)
             user = Users.get_user_by_id(user_id)
-            user_settings = user.settings.model_dump()
+            user_settings = user.settings.model_dump() if user.settings else {}
 
 
             # Check if user has "tools" and "valves" settings
             # Check if user has "tools" and "valves" settings
             if "tools" not in user_settings:
             if "tools" not in user_settings:
@@ -179,25 +187,27 @@ class ToolsTable:
 
 
     def update_tool_by_id(self, id: str, updated: dict) -> Optional[ToolModel]:
     def update_tool_by_id(self, id: str, updated: dict) -> Optional[ToolModel]:
         try:
         try:
-            query = Tool.update(
-                **updated,
-                updated_at=int(time.time()),
-            ).where(Tool.id == id)
-            query.execute()
-
-            tool = Tool.get(Tool.id == id)
-            return ToolModel(**model_to_dict(tool))
+            with get_db() as db:
+                db.query(Tool).filter_by(id=id).update(
+                    {**updated, "updated_at": int(time.time())}
+                )
+                db.commit()
+
+                tool = db.query(Tool).get(id)
+                db.refresh(tool)
+                return ToolModel.model_validate(tool)
         except:
         except:
             return None
             return None
 
 
     def delete_tool_by_id(self, id: str) -> bool:
     def delete_tool_by_id(self, id: str) -> bool:
         try:
         try:
-            query = Tool.delete().where((Tool.id == id))
-            query.execute()  # Remove the rows, return number of rows removed.
+            with get_db() as db:
+                db.query(Tool).filter_by(id=id).delete()
+                db.commit()
 
 
-            return True
+                return True
         except:
         except:
             return False
             return False
 
 
 
 
-Tools = ToolsTable(DB)
+Tools = ToolsTable()

+ 118 - 94
backend/apps/webui/models/users.py

@@ -1,11 +1,12 @@
-from pydantic import BaseModel, ConfigDict
-from peewee import *
-from playhouse.shortcuts import model_to_dict
+from pydantic import BaseModel, ConfigDict, parse_obj_as
 from typing import List, Union, Optional
 from typing import List, Union, Optional
 import time
 import time
+
+from sqlalchemy import String, Column, BigInteger, Text
+
 from utils.misc import get_gravatar_url
 from utils.misc import get_gravatar_url
 
 
-from apps.webui.internal.db import DB, JSONField
+from apps.webui.internal.db import Base, JSONField, Session, get_db
 from apps.webui.models.chats import Chats
 from apps.webui.models.chats import Chats
 
 
 ####################
 ####################
@@ -13,25 +14,24 @@ from apps.webui.models.chats import Chats
 ####################
 ####################
 
 
 
 
-class User(Model):
-    id = CharField(unique=True)
-    name = CharField()
-    email = CharField()
-    role = CharField()
-    profile_image_url = TextField()
+class User(Base):
+    __tablename__ = "user"
 
 
-    last_active_at = BigIntegerField()
-    updated_at = BigIntegerField()
-    created_at = BigIntegerField()
+    id = Column(String, primary_key=True)
+    name = Column(String)
+    email = Column(String)
+    role = Column(String)
+    profile_image_url = Column(Text)
 
 
-    api_key = CharField(null=True, unique=True)
-    settings = JSONField(null=True)
-    info = JSONField(null=True)
+    last_active_at = Column(BigInteger)
+    updated_at = Column(BigInteger)
+    created_at = Column(BigInteger)
 
 
-    oauth_sub = TextField(null=True, unique=True)
+    api_key = Column(String, nullable=True, unique=True)
+    settings = Column(JSONField, nullable=True)
+    info = Column(JSONField, nullable=True)
 
 
-    class Meta:
-        database = DB
+    oauth_sub = Column(Text, unique=True)
 
 
 
 
 class UserSettings(BaseModel):
 class UserSettings(BaseModel):
@@ -57,6 +57,8 @@ class UserModel(BaseModel):
 
 
     oauth_sub: Optional[str] = None
     oauth_sub: Optional[str] = None
 
 
+    model_config = ConfigDict(from_attributes=True)
+
 
 
 ####################
 ####################
 # Forms
 # Forms
@@ -76,9 +78,6 @@ class UserUpdateForm(BaseModel):
 
 
 
 
 class UsersTable:
 class UsersTable:
-    def __init__(self, db):
-        self.db = db
-        self.db.create_tables([User])
 
 
     def insert_new_user(
     def insert_new_user(
         self,
         self,
@@ -89,77 +88,92 @@ class UsersTable:
         role: str = "pending",
         role: str = "pending",
         oauth_sub: Optional[str] = None,
         oauth_sub: Optional[str] = None,
     ) -> Optional[UserModel]:
     ) -> Optional[UserModel]:
-        user = UserModel(
-            **{
-                "id": id,
-                "name": name,
-                "email": email,
-                "role": role,
-                "profile_image_url": profile_image_url,
-                "last_active_at": int(time.time()),
-                "created_at": int(time.time()),
-                "updated_at": int(time.time()),
-                "oauth_sub": oauth_sub,
-            }
-        )
-        result = User.create(**user.model_dump())
-        if result:
-            return user
-        else:
-            return None
+        with get_db() as db:
+            user = UserModel(
+                **{
+                    "id": id,
+                    "name": name,
+                    "email": email,
+                    "role": role,
+                    "profile_image_url": profile_image_url,
+                    "last_active_at": int(time.time()),
+                    "created_at": int(time.time()),
+                    "updated_at": int(time.time()),
+                    "oauth_sub": oauth_sub,
+                }
+            )
+            result = User(**user.model_dump())
+            db.add(result)
+            db.commit()
+            db.refresh(result)
+            if result:
+                return user
+            else:
+                return None
 
 
     def get_user_by_id(self, id: str) -> Optional[UserModel]:
     def get_user_by_id(self, id: str) -> Optional[UserModel]:
         try:
         try:
-            user = User.get(User.id == id)
-            return UserModel(**model_to_dict(user))
-        except:
+            with get_db() as db:
+                user = db.query(User).filter_by(id=id).first()
+                return UserModel.model_validate(user)
+        except Exception as e:
             return None
             return None
 
 
     def get_user_by_api_key(self, api_key: str) -> Optional[UserModel]:
     def get_user_by_api_key(self, api_key: str) -> Optional[UserModel]:
         try:
         try:
-            user = User.get(User.api_key == api_key)
-            return UserModel(**model_to_dict(user))
+            with get_db() as db:
+
+                user = db.query(User).filter_by(api_key=api_key).first()
+                return UserModel.model_validate(user)
         except:
         except:
             return None
             return None
 
 
     def get_user_by_email(self, email: str) -> Optional[UserModel]:
     def get_user_by_email(self, email: str) -> Optional[UserModel]:
         try:
         try:
-            user = User.get(User.email == email)
-            return UserModel(**model_to_dict(user))
+            with get_db() as db:
+
+                user = db.query(User).filter_by(email=email).first()
+                return UserModel.model_validate(user)
         except:
         except:
             return None
             return None
 
 
     def get_user_by_oauth_sub(self, sub: str) -> Optional[UserModel]:
     def get_user_by_oauth_sub(self, sub: str) -> Optional[UserModel]:
         try:
         try:
-            user = User.get(User.oauth_sub == sub)
-            return UserModel(**model_to_dict(user))
+            with get_db() as db:
+
+                user = db.query(User).filter_by(oauth_sub=sub).first()
+                return UserModel.model_validate(user)
         except:
         except:
             return None
             return None
 
 
     def get_users(self, skip: int = 0, limit: int = 50) -> List[UserModel]:
     def get_users(self, skip: int = 0, limit: int = 50) -> List[UserModel]:
-        return [
-            UserModel(**model_to_dict(user))
-            for user in User.select()
-            # .limit(limit).offset(skip)
-        ]
+        with get_db() as db:
+            users = (
+                db.query(User)
+                # .offset(skip).limit(limit)
+                .all()
+            )
+            return [UserModel.model_validate(user) for user in users]
 
 
     def get_num_users(self) -> Optional[int]:
     def get_num_users(self) -> Optional[int]:
-        return User.select().count()
+        with get_db() as db:
+            return db.query(User).count()
 
 
     def get_first_user(self) -> UserModel:
     def get_first_user(self) -> UserModel:
         try:
         try:
-            user = User.select().order_by(User.created_at).first()
-            return UserModel(**model_to_dict(user))
+            with get_db() as db:
+                user = db.query(User).order_by(User.created_at).first()
+                return UserModel.model_validate(user)
         except:
         except:
             return None
             return None
 
 
     def update_user_role_by_id(self, id: str, role: str) -> Optional[UserModel]:
     def update_user_role_by_id(self, id: str, role: str) -> Optional[UserModel]:
         try:
         try:
-            query = User.update(role=role).where(User.id == id)
-            query.execute()
-
-            user = User.get(User.id == id)
-            return UserModel(**model_to_dict(user))
+            with get_db() as db:
+                db.query(User).filter_by(id=id).update({"role": role})
+                db.commit()
+                user = db.query(User).filter_by(id=id).first()
+                return UserModel.model_validate(user)
         except:
         except:
             return None
             return None
 
 
@@ -167,23 +181,28 @@ class UsersTable:
         self, id: str, profile_image_url: str
         self, id: str, profile_image_url: str
     ) -> Optional[UserModel]:
     ) -> Optional[UserModel]:
         try:
         try:
-            query = User.update(profile_image_url=profile_image_url).where(
-                User.id == id
-            )
-            query.execute()
-
-            user = User.get(User.id == id)
-            return UserModel(**model_to_dict(user))
+            with get_db() as db:
+                db.query(User).filter_by(id=id).update(
+                    {"profile_image_url": profile_image_url}
+                )
+                db.commit()
+
+                user = db.query(User).filter_by(id=id).first()
+                return UserModel.model_validate(user)
         except:
         except:
             return None
             return None
 
 
     def update_user_last_active_by_id(self, id: str) -> Optional[UserModel]:
     def update_user_last_active_by_id(self, id: str) -> Optional[UserModel]:
         try:
         try:
-            query = User.update(last_active_at=int(time.time())).where(User.id == id)
-            query.execute()
+            with get_db() as db:
 
 
-            user = User.get(User.id == id)
-            return UserModel(**model_to_dict(user))
+                db.query(User).filter_by(id=id).update(
+                    {"last_active_at": int(time.time())}
+                )
+                db.commit()
+
+                user = db.query(User).filter_by(id=id).first()
+                return UserModel.model_validate(user)
         except:
         except:
             return None
             return None
 
 
@@ -191,22 +210,25 @@ class UsersTable:
         self, id: str, oauth_sub: str
         self, id: str, oauth_sub: str
     ) -> Optional[UserModel]:
     ) -> Optional[UserModel]:
         try:
         try:
-            query = User.update(oauth_sub=oauth_sub).where(User.id == id)
-            query.execute()
+            with get_db() as db:
+                db.query(User).filter_by(id=id).update({"oauth_sub": oauth_sub})
+                db.commit()
 
 
-            user = User.get(User.id == id)
-            return UserModel(**model_to_dict(user))
+                user = db.query(User).filter_by(id=id).first()
+                return UserModel.model_validate(user)
         except:
         except:
             return None
             return None
 
 
     def update_user_by_id(self, id: str, updated: dict) -> Optional[UserModel]:
     def update_user_by_id(self, id: str, updated: dict) -> Optional[UserModel]:
         try:
         try:
-            query = User.update(**updated).where(User.id == id)
-            query.execute()
-
-            user = User.get(User.id == id)
-            return UserModel(**model_to_dict(user))
-        except:
+            with get_db() as db:
+                db.query(User).filter_by(id=id).update(updated)
+                db.commit()
+
+                user = db.query(User).filter_by(id=id).first()
+                return UserModel.model_validate(user)
+                # return UserModel(**user.dict())
+        except Exception as e:
             return None
             return None
 
 
     def delete_user_by_id(self, id: str) -> bool:
     def delete_user_by_id(self, id: str) -> bool:
@@ -215,9 +237,10 @@ class UsersTable:
             result = Chats.delete_chats_by_user_id(id)
             result = Chats.delete_chats_by_user_id(id)
 
 
             if result:
             if result:
-                # Delete User
-                query = User.delete().where(User.id == id)
-                query.execute()  # Remove the rows, return number of rows removed.
+                with get_db() as db:
+                    # Delete User
+                    db.query(User).filter_by(id=id).delete()
+                    db.commit()
 
 
                 return True
                 return True
             else:
             else:
@@ -227,19 +250,20 @@ class UsersTable:
 
 
     def update_user_api_key_by_id(self, id: str, api_key: str) -> str:
     def update_user_api_key_by_id(self, id: str, api_key: str) -> str:
         try:
         try:
-            query = User.update(api_key=api_key).where(User.id == id)
-            result = query.execute()
-
-            return True if result == 1 else False
+            with get_db() as db:
+                result = db.query(User).filter_by(id=id).update({"api_key": api_key})
+                db.commit()
+                return True if result == 1 else False
         except:
         except:
             return False
             return False
 
 
     def get_user_api_key_by_id(self, id: str) -> Optional[str]:
     def get_user_api_key_by_id(self, id: str) -> Optional[str]:
         try:
         try:
-            user = User.get(User.id == id)
-            return user.api_key
-        except:
+            with get_db() as db:
+                user = db.query(User).filter_by(id=id).first()
+                return user.api_key
+        except Exception as e:
             return None
             return None
 
 
 
 
-Users = UsersTable(DB)
+Users = UsersTable()

+ 5 - 3
backend/apps/webui/routers/chats.py

@@ -76,7 +76,10 @@ async def delete_all_user_chats(request: Request, user=Depends(get_verified_user
 
 
 @router.get("/list/user/{user_id}", response_model=List[ChatTitleIdResponse])
 @router.get("/list/user/{user_id}", response_model=List[ChatTitleIdResponse])
 async def get_user_chat_list_by_user_id(
 async def get_user_chat_list_by_user_id(
-    user_id: str, user=Depends(get_admin_user), skip: int = 0, limit: int = 50
+    user_id: str,
+    user=Depends(get_admin_user),
+    skip: int = 0,
+    limit: int = 50,
 ):
 ):
     return Chats.get_chat_list_by_user_id(
     return Chats.get_chat_list_by_user_id(
         user_id, include_archived=True, skip=skip, limit=limit
         user_id, include_archived=True, skip=skip, limit=limit
@@ -119,7 +122,7 @@ async def get_user_chats(user=Depends(get_verified_user)):
 
 
 
 
 @router.get("/all/archived", response_model=List[ChatResponse])
 @router.get("/all/archived", response_model=List[ChatResponse])
-async def get_user_chats(user=Depends(get_verified_user)):
+async def get_user_archived_chats(user=Depends(get_verified_user)):
     return [
     return [
         ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
         ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
         for chat in Chats.get_archived_chats_by_user_id(user.id)
         for chat in Chats.get_archived_chats_by_user_id(user.id)
@@ -207,7 +210,6 @@ async def get_user_chat_list_by_tag_name(
     form_data: TagNameForm, user=Depends(get_verified_user)
     form_data: TagNameForm, user=Depends(get_verified_user)
 ):
 ):
 
 
-    print(form_data)
     chat_ids = [
     chat_ids = [
         chat_id_tag.chat_id
         chat_id_tag.chat_id
         for chat_id_tag in Tags.get_chat_ids_by_tag_name_and_user_id(
         for chat_id_tag in Tags.get_chat_ids_by_tag_name_and_user_id(

+ 3 - 1
backend/apps/webui/routers/documents.py

@@ -130,7 +130,9 @@ async def tag_doc_by_name(form_data: TagDocumentForm, user=Depends(get_verified_
 
 
 @router.post("/doc/update", response_model=Optional[DocumentResponse])
 @router.post("/doc/update", response_model=Optional[DocumentResponse])
 async def update_doc_by_name(
 async def update_doc_by_name(
-    name: str, form_data: DocumentUpdateForm, user=Depends(get_admin_user)
+    name: str,
+    form_data: DocumentUpdateForm,
+    user=Depends(get_admin_user),
 ):
 ):
     doc = Documents.update_doc_by_name(name, form_data)
     doc = Documents.update_doc_by_name(name, form_data)
     if doc:
     if doc:

+ 1 - 4
backend/apps/webui/routers/files.py

@@ -50,10 +50,7 @@ router = APIRouter()
 
 
 
 
 @router.post("/")
 @router.post("/")
-def upload_file(
-    file: UploadFile = File(...),
-    user=Depends(get_verified_user),
-):
+def upload_file(file: UploadFile = File(...), user=Depends(get_verified_user)):
     log.info(f"file.content_type: {file.content_type}")
     log.info(f"file.content_type: {file.content_type}")
     try:
     try:
         unsanitized_filename = file.filename
         unsanitized_filename = file.filename

+ 4 - 1
backend/apps/webui/routers/functions.py

@@ -233,7 +233,10 @@ async def delete_function_by_id(
 
 
         # delete the function file
         # delete the function file
         function_path = os.path.join(FUNCTIONS_DIR, f"{id}.py")
         function_path = os.path.join(FUNCTIONS_DIR, f"{id}.py")
-        os.remove(function_path)
+        try:
+            os.remove(function_path)
+        except:
+            pass
 
 
     return result
     return result
 
 

+ 3 - 1
backend/apps/webui/routers/memories.py

@@ -50,7 +50,9 @@ class MemoryUpdateModel(BaseModel):
 
 
 @router.post("/add", response_model=Optional[MemoryModel])
 @router.post("/add", response_model=Optional[MemoryModel])
 async def add_memory(
 async def add_memory(
-    request: Request, form_data: AddMemoryForm, user=Depends(get_verified_user)
+    request: Request,
+    form_data: AddMemoryForm,
+    user=Depends(get_verified_user),
 ):
 ):
     memory = Memories.insert_new_memory(user.id, form_data.content)
     memory = Memories.insert_new_memory(user.id, form_data.content)
     memory_embedding = request.app.state.EMBEDDING_FUNCTION(memory.content)
     memory_embedding = request.app.state.EMBEDDING_FUNCTION(memory.content)

+ 8 - 2
backend/apps/webui/routers/models.py

@@ -5,6 +5,7 @@ from typing import List, Union, Optional
 from fastapi import APIRouter
 from fastapi import APIRouter
 from pydantic import BaseModel
 from pydantic import BaseModel
 import json
 import json
+
 from apps.webui.models.models import Models, ModelModel, ModelForm, ModelResponse
 from apps.webui.models.models import Models, ModelModel, ModelForm, ModelResponse
 
 
 from utils.utils import get_verified_user, get_admin_user
 from utils.utils import get_verified_user, get_admin_user
@@ -29,7 +30,9 @@ async def get_models(user=Depends(get_verified_user)):
 
 
 @router.post("/add", response_model=Optional[ModelModel])
 @router.post("/add", response_model=Optional[ModelModel])
 async def add_new_model(
 async def add_new_model(
-    request: Request, form_data: ModelForm, user=Depends(get_admin_user)
+    request: Request,
+    form_data: ModelForm,
+    user=Depends(get_admin_user),
 ):
 ):
     if form_data.id in request.app.state.MODELS:
     if form_data.id in request.app.state.MODELS:
         raise HTTPException(
         raise HTTPException(
@@ -73,7 +76,10 @@ async def get_model_by_id(id: str, user=Depends(get_verified_user)):
 
 
 @router.post("/update", response_model=Optional[ModelModel])
 @router.post("/update", response_model=Optional[ModelModel])
 async def update_model_by_id(
 async def update_model_by_id(
-    request: Request, id: str, form_data: ModelForm, user=Depends(get_admin_user)
+    request: Request,
+    id: str,
+    form_data: ModelForm,
+    user=Depends(get_admin_user),
 ):
 ):
     model = Models.get_model_by_id(id)
     model = Models.get_model_by_id(id)
     if model:
     if model:

+ 3 - 1
backend/apps/webui/routers/prompts.py

@@ -71,7 +71,9 @@ async def get_prompt_by_command(command: str, user=Depends(get_verified_user)):
 
 
 @router.post("/command/{command}/update", response_model=Optional[PromptModel])
 @router.post("/command/{command}/update", response_model=Optional[PromptModel])
 async def update_prompt_by_command(
 async def update_prompt_by_command(
-    command: str, form_data: PromptForm, user=Depends(get_admin_user)
+    command: str,
+    form_data: PromptForm,
+    user=Depends(get_admin_user),
 ):
 ):
     prompt = Prompts.update_prompt_by_command(f"/{command}", form_data)
     prompt = Prompts.update_prompt_by_command(f"/{command}", form_data)
     if prompt:
     if prompt:

+ 7 - 3
backend/apps/webui/routers/tools.py

@@ -6,7 +6,6 @@ from fastapi import APIRouter
 from pydantic import BaseModel
 from pydantic import BaseModel
 import json
 import json
 
 
-
 from apps.webui.models.users import Users
 from apps.webui.models.users import Users
 from apps.webui.models.tools import Tools, ToolForm, ToolModel, ToolResponse
 from apps.webui.models.tools import Tools, ToolForm, ToolModel, ToolResponse
 from apps.webui.utils import load_toolkit_module_by_id
 from apps.webui.utils import load_toolkit_module_by_id
@@ -57,7 +56,9 @@ async def get_toolkits(user=Depends(get_admin_user)):
 
 
 @router.post("/create", response_model=Optional[ToolResponse])
 @router.post("/create", response_model=Optional[ToolResponse])
 async def create_new_toolkit(
 async def create_new_toolkit(
-    request: Request, form_data: ToolForm, user=Depends(get_admin_user)
+    request: Request,
+    form_data: ToolForm,
+    user=Depends(get_admin_user),
 ):
 ):
     if not form_data.id.isidentifier():
     if not form_data.id.isidentifier():
         raise HTTPException(
         raise HTTPException(
@@ -131,7 +132,10 @@ async def get_toolkit_by_id(id: str, user=Depends(get_admin_user)):
 
 
 @router.post("/id/{id}/update", response_model=Optional[ToolModel])
 @router.post("/id/{id}/update", response_model=Optional[ToolModel])
 async def update_toolkit_by_id(
 async def update_toolkit_by_id(
-    request: Request, id: str, form_data: ToolForm, user=Depends(get_admin_user)
+    request: Request,
+    id: str,
+    form_data: ToolForm,
+    user=Depends(get_admin_user),
 ):
 ):
     toolkit_path = os.path.join(TOOLS_DIR, f"{id}.py")
     toolkit_path = os.path.join(TOOLS_DIR, f"{id}.py")
 
 

+ 4 - 2
backend/apps/webui/routers/users.py

@@ -138,7 +138,7 @@ async def get_user_info_by_session_user(user=Depends(get_verified_user)):
 
 
 
 
 @router.post("/user/info/update", response_model=Optional[dict])
 @router.post("/user/info/update", response_model=Optional[dict])
-async def update_user_settings_by_session_user(
+async def update_user_info_by_session_user(
     form_data: dict, user=Depends(get_verified_user)
     form_data: dict, user=Depends(get_verified_user)
 ):
 ):
     user = Users.get_user_by_id(user.id)
     user = Users.get_user_by_id(user.id)
@@ -205,7 +205,9 @@ async def get_user_by_id(user_id: str, user=Depends(get_verified_user)):
 
 
 @router.post("/{user_id}/update", response_model=Optional[UserModel])
 @router.post("/{user_id}/update", response_model=Optional[UserModel])
 async def update_user_by_id(
 async def update_user_by_id(
-    user_id: str, form_data: UserUpdateForm, session_user=Depends(get_admin_user)
+    user_id: str,
+    form_data: UserUpdateForm,
+    session_user=Depends(get_admin_user),
 ):
 ):
     user = Users.get_user_by_id(user_id)
     user = Users.get_user_by_id(user_id)
 
 

+ 4 - 4
backend/apps/webui/routers/utils.py

@@ -1,6 +1,5 @@
 from fastapi import APIRouter, UploadFile, File, Response
 from fastapi import APIRouter, UploadFile, File, Response
 from fastapi import Depends, HTTPException, status
 from fastapi import Depends, HTTPException, status
-from peewee import SqliteDatabase
 from starlette.responses import StreamingResponse, FileResponse
 from starlette.responses import StreamingResponse, FileResponse
 from pydantic import BaseModel
 from pydantic import BaseModel
 
 
@@ -10,7 +9,6 @@ import markdown
 import black
 import black
 
 
 
 
-from apps.webui.internal.db import DB
 from utils.utils import get_admin_user
 from utils.utils import get_admin_user
 from utils.misc import calculate_sha256, get_gravatar_url
 from utils.misc import calculate_sha256, get_gravatar_url
 
 
@@ -114,13 +112,15 @@ async def download_db(user=Depends(get_admin_user)):
             status_code=status.HTTP_401_UNAUTHORIZED,
             status_code=status.HTTP_401_UNAUTHORIZED,
             detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
             detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
         )
         )
-    if not isinstance(DB, SqliteDatabase):
+    from apps.webui.internal.db import engine
+
+    if engine.name != "sqlite":
         raise HTTPException(
         raise HTTPException(
             status_code=status.HTTP_400_BAD_REQUEST,
             status_code=status.HTTP_400_BAD_REQUEST,
             detail=ERROR_MESSAGES.DB_NOT_SQLITE,
             detail=ERROR_MESSAGES.DB_NOT_SQLITE,
         )
         )
     return FileResponse(
     return FileResponse(
-        DB.database,
+        engine.url.database,
         media_type="application/octet-stream",
         media_type="application/octet-stream",
         filename="webui.db",
         filename="webui.db",
     )
     )

+ 43 - 2
backend/config.py

@@ -393,6 +393,18 @@ OAUTH_PROVIDER_NAME = PersistentConfig(
     os.environ.get("OAUTH_PROVIDER_NAME", "SSO"),
     os.environ.get("OAUTH_PROVIDER_NAME", "SSO"),
 )
 )
 
 
+OAUTH_USERNAME_CLAIM = PersistentConfig(
+    "OAUTH_USERNAME_CLAIM",
+    "oauth.oidc.username_claim",
+    os.environ.get("OAUTH_USERNAME_CLAIM", "name"),
+)
+
+OAUTH_PICTURE_CLAIM = PersistentConfig(
+    "OAUTH_USERNAME_CLAIM",
+    "oauth.oidc.avatar_claim",
+    os.environ.get("OAUTH_PICTURE_CLAIM", "picture"),
+)
+
 
 
 def load_oauth_providers():
 def load_oauth_providers():
     OAUTH_PROVIDERS.clear()
     OAUTH_PROVIDERS.clear()
@@ -438,16 +450,27 @@ load_oauth_providers()
 
 
 STATIC_DIR = Path(os.getenv("STATIC_DIR", BACKEND_DIR / "static")).resolve()
 STATIC_DIR = Path(os.getenv("STATIC_DIR", BACKEND_DIR / "static")).resolve()
 
 
-frontend_favicon = FRONTEND_BUILD_DIR / "favicon.png"
+frontend_favicon = FRONTEND_BUILD_DIR / "static" / "favicon.png"
+
 if frontend_favicon.exists():
 if frontend_favicon.exists():
     try:
     try:
         shutil.copyfile(frontend_favicon, STATIC_DIR / "favicon.png")
         shutil.copyfile(frontend_favicon, STATIC_DIR / "favicon.png")
     except Exception as e:
     except Exception as e:
         logging.error(f"An error occurred: {e}")
         logging.error(f"An error occurred: {e}")
-
 else:
 else:
     logging.warning(f"Frontend favicon not found at {frontend_favicon}")
     logging.warning(f"Frontend favicon not found at {frontend_favicon}")
 
 
+frontend_splash = FRONTEND_BUILD_DIR / "static" / "splash.png"
+
+if frontend_splash.exists():
+    try:
+        shutil.copyfile(frontend_splash, STATIC_DIR / "splash.png")
+    except Exception as e:
+        logging.error(f"An error occurred: {e}")
+else:
+    logging.warning(f"Frontend splash not found at {frontend_splash}")
+
+
 ####################################
 ####################################
 # CUSTOM_NAME
 # CUSTOM_NAME
 ####################################
 ####################################
@@ -472,6 +495,19 @@ if CUSTOM_NAME:
                         r.raw.decode_content = True
                         r.raw.decode_content = True
                         shutil.copyfileobj(r.raw, f)
                         shutil.copyfileobj(r.raw, f)
 
 
+            if "splash" in data:
+                url = (
+                    f"https://api.openwebui.com{data['splash']}"
+                    if data["splash"][0] == "/"
+                    else data["splash"]
+                )
+
+                r = requests.get(url, stream=True)
+                if r.status_code == 200:
+                    with open(f"{STATIC_DIR}/splash.png", "wb") as f:
+                        r.raw.decode_content = True
+                        shutil.copyfileobj(r.raw, f)
+
             WEBUI_NAME = data["name"]
             WEBUI_NAME = data["name"]
     except Exception as e:
     except Exception as e:
         log.exception(e)
         log.exception(e)
@@ -766,6 +802,7 @@ class BannerModel(BaseModel):
     dismissible: bool
     dismissible: bool
     timestamp: int
     timestamp: int
 
 
+
 try:
 try:
     banners = json.loads(os.environ.get("WEBUI_BANNERS", "[]"))
     banners = json.loads(os.environ.get("WEBUI_BANNERS", "[]"))
     banners = [BannerModel(**banner) for banner in banners]
     banners = [BannerModel(**banner) for banner in banners]
@@ -1318,3 +1355,7 @@ AUDIO_TTS_VOICE = PersistentConfig(
 ####################################
 ####################################
 
 
 DATABASE_URL = os.environ.get("DATABASE_URL", f"sqlite:///{DATA_DIR}/webui.db")
 DATABASE_URL = os.environ.get("DATABASE_URL", f"sqlite:///{DATA_DIR}/webui.db")
+
+# Replace the postgres:// with postgresql://
+if "postgres://" in DATABASE_URL:
+    DATABASE_URL = DATABASE_URL.replace("postgres://", "postgresql://")

+ 11 - 0
backend/constants.py

@@ -89,3 +89,14 @@ class ERROR_MESSAGES(str, Enum):
     OLLAMA_API_DISABLED = (
     OLLAMA_API_DISABLED = (
         "The Ollama API is disabled. Please enable it to use this feature."
         "The Ollama API is disabled. Please enable it to use this feature."
     )
     )
+
+
+class TASKS(str, Enum):
+    def __str__(self) -> str:
+        return super().__str__()
+
+    DEFAULT = lambda task="": f"{task if task else 'default'}"
+    TITLE_GENERATION = "Title Generation"
+    EMOJI_GENERATION = "Emoji Generation"
+    QUERY_GENERATION = "Query Generation"
+    FUNCTION_CALLING = "Function Calling"

+ 100 - 19
backend/main.py

@@ -1,5 +1,6 @@
 import base64
 import base64
 import uuid
 import uuid
+import subprocess
 from contextlib import asynccontextmanager
 from contextlib import asynccontextmanager
 
 
 from authlib.integrations.starlette_client import OAuth
 from authlib.integrations.starlette_client import OAuth
@@ -27,6 +28,7 @@ from fastapi.responses import JSONResponse
 from fastapi import HTTPException
 from fastapi import HTTPException
 from fastapi.middleware.wsgi import WSGIMiddleware
 from fastapi.middleware.wsgi import WSGIMiddleware
 from fastapi.middleware.cors import CORSMiddleware
 from fastapi.middleware.cors import CORSMiddleware
+from sqlalchemy import text
 from starlette.exceptions import HTTPException as StarletteHTTPException
 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
@@ -54,6 +56,7 @@ from apps.webui.main import (
     get_pipe_models,
     get_pipe_models,
     generate_function_chat_completion,
     generate_function_chat_completion,
 )
 )
+from apps.webui.internal.db import Session, SessionLocal
 
 
 
 
 from pydantic import BaseModel
 from pydantic import BaseModel
@@ -125,8 +128,10 @@ from config import (
     WEBUI_SESSION_COOKIE_SAME_SITE,
     WEBUI_SESSION_COOKIE_SAME_SITE,
     WEBUI_SESSION_COOKIE_SECURE,
     WEBUI_SESSION_COOKIE_SECURE,
     AppConfig,
     AppConfig,
+    BACKEND_DIR,
+    DATABASE_URL,
 )
 )
-from constants import ERROR_MESSAGES, WEBHOOK_MESSAGES
+from constants import ERROR_MESSAGES, WEBHOOK_MESSAGES, TASKS
 from utils.webhook import post_webhook
 from utils.webhook import post_webhook
 
 
 if SAFE_MODE:
 if SAFE_MODE:
@@ -167,8 +172,20 @@ https://github.com/open-webui/open-webui
 )
 )
 
 
 
 
+def run_migrations():
+    try:
+        from alembic.config import Config
+        from alembic import command
+
+        alembic_cfg = Config("alembic.ini")
+        command.upgrade(alembic_cfg, "head")
+    except Exception as e:
+        print(f"Error: {e}")
+
+
 @asynccontextmanager
 @asynccontextmanager
 async def lifespan(app: FastAPI):
 async def lifespan(app: FastAPI):
+    run_migrations()
     yield
     yield
 
 
 
 
@@ -285,6 +302,7 @@ async def get_function_call_response(
     user,
     user,
     model,
     model,
     __event_emitter__=None,
     __event_emitter__=None,
+    __event_call__=None,
 ):
 ):
     tool = Tools.get_tool_by_id(tool_id)
     tool = Tools.get_tool_by_id(tool_id)
     tools_specs = json.dumps(tool.specs, indent=2)
     tools_specs = json.dumps(tool.specs, indent=2)
@@ -311,7 +329,7 @@ async def get_function_call_response(
             {"role": "user", "content": f"Query: {prompt}"},
             {"role": "user", "content": f"Query: {prompt}"},
         ],
         ],
         "stream": False,
         "stream": False,
-        "function": True,
+        "task": TASKS.FUNCTION_CALLING,
     }
     }
 
 
     try:
     try:
@@ -324,7 +342,6 @@ async def get_function_call_response(
     response = None
     response = None
     try:
     try:
         response = await generate_chat_completions(form_data=payload, user=user)
         response = await generate_chat_completions(form_data=payload, user=user)
-
         content = None
         content = None
 
 
         if hasattr(response, "body_iterator"):
         if hasattr(response, "body_iterator"):
@@ -429,6 +446,13 @@ async def get_function_call_response(
                             "__event_emitter__": __event_emitter__,
                             "__event_emitter__": __event_emitter__,
                         }
                         }
 
 
+                    if "__event_call__" in sig.parameters:
+                        # Call the function with the '__event_call__' parameter included
+                        params = {
+                            **params,
+                            "__event_call__": __event_call__,
+                        }
+
                     if inspect.iscoroutinefunction(function):
                     if inspect.iscoroutinefunction(function):
                         function_result = await function(**params)
                         function_result = await function(**params)
                     else:
                     else:
@@ -452,7 +476,9 @@ async def get_function_call_response(
     return None, None, False
     return None, None, False
 
 
 
 
-async def chat_completion_functions_handler(body, model, user, __event_emitter__):
+async def chat_completion_functions_handler(
+    body, model, user, __event_emitter__, __event_call__
+):
     skip_files = None
     skip_files = None
 
 
     filter_ids = get_filter_function_ids(model)
     filter_ids = get_filter_function_ids(model)
@@ -518,12 +544,19 @@ async def chat_completion_functions_handler(body, model, user, __event_emitter__
                             **params,
                             **params,
                             "__model__": model,
                             "__model__": model,
                         }
                         }
+
                     if "__event_emitter__" in sig.parameters:
                     if "__event_emitter__" in sig.parameters:
                         params = {
                         params = {
                             **params,
                             **params,
                             "__event_emitter__": __event_emitter__,
                             "__event_emitter__": __event_emitter__,
                         }
                         }
 
 
+                    if "__event_call__" in sig.parameters:
+                        params = {
+                            **params,
+                            "__event_call__": __event_call__,
+                        }
+
                     if inspect.iscoroutinefunction(inlet):
                     if inspect.iscoroutinefunction(inlet):
                         body = await inlet(**params)
                         body = await inlet(**params)
                     else:
                     else:
@@ -540,7 +573,9 @@ async def chat_completion_functions_handler(body, model, user, __event_emitter__
     return body, {}
     return body, {}
 
 
 
 
-async def chat_completion_tools_handler(body, model, user, __event_emitter__):
+async def chat_completion_tools_handler(
+    body, model, user, __event_emitter__, __event_call__
+):
     skip_files = None
     skip_files = None
 
 
     contexts = []
     contexts = []
@@ -563,6 +598,7 @@ async def chat_completion_tools_handler(body, model, user, __event_emitter__):
                     user=user,
                     user=user,
                     model=model,
                     model=model,
                     __event_emitter__=__event_emitter__,
                     __event_emitter__=__event_emitter__,
+                    __event_call__=__event_call__,
                 )
                 )
 
 
                 print(file_handler)
                 print(file_handler)
@@ -660,6 +696,14 @@ class ChatCompletionMiddleware(BaseHTTPMiddleware):
                     to=session_id,
                     to=session_id,
                 )
                 )
 
 
+            async def __event_call__(data):
+                response = await sio.call(
+                    "chat-events",
+                    {"chat_id": chat_id, "message_id": message_id, "data": data},
+                    to=session_id,
+                )
+                return response
+
             # Initialize data_items to store additional data to be sent to the client
             # Initialize data_items to store additional data to be sent to the client
             data_items = []
             data_items = []
 
 
@@ -669,7 +713,7 @@ class ChatCompletionMiddleware(BaseHTTPMiddleware):
 
 
             try:
             try:
                 body, flags = await chat_completion_functions_handler(
                 body, flags = await chat_completion_functions_handler(
-                    body, model, user, __event_emitter__
+                    body, model, user, __event_emitter__, __event_call__
                 )
                 )
             except Exception as e:
             except Exception as e:
                 return JSONResponse(
                 return JSONResponse(
@@ -679,7 +723,7 @@ class ChatCompletionMiddleware(BaseHTTPMiddleware):
 
 
             try:
             try:
                 body, flags = await chat_completion_tools_handler(
                 body, flags = await chat_completion_tools_handler(
-                    body, model, user, __event_emitter__
+                    body, model, user, __event_emitter__, __event_call__
                 )
                 )
 
 
                 contexts.extend(flags.get("contexts", []))
                 contexts.extend(flags.get("contexts", []))
@@ -834,9 +878,8 @@ def filter_pipeline(payload, user):
                 pass
                 pass
 
 
     if "pipeline" not in app.state.MODELS[model_id]:
     if "pipeline" not in app.state.MODELS[model_id]:
-        for key in ["title", "task", "function"]:
-            if key in payload:
-                del payload[key]
+        if "task" in payload:
+            del payload["task"]
 
 
     return payload
     return payload
 
 
@@ -901,6 +944,14 @@ app.add_middleware(
 )
 )
 
 
 
 
+@app.middleware("http")
+async def commit_session_after_request(request: Request, call_next):
+    response = await call_next(request)
+    log.debug("Commit session after request")
+    Session.commit()
+    return response
+
+
 @app.middleware("http")
 @app.middleware("http")
 async def check_url(request: Request, call_next):
 async def check_url(request: Request, call_next):
     if len(app.state.MODELS) == 0:
     if len(app.state.MODELS) == 0:
@@ -977,12 +1028,16 @@ async def get_all_models():
                     model["info"] = custom_model.model_dump()
                     model["info"] = custom_model.model_dump()
         else:
         else:
             owned_by = "openai"
             owned_by = "openai"
+            pipe = None
+
             for model in models:
             for model in models:
                 if (
                 if (
                     custom_model.base_model_id == model["id"]
                     custom_model.base_model_id == model["id"]
                     or custom_model.base_model_id == model["id"].split(":")[0]
                     or custom_model.base_model_id == model["id"].split(":")[0]
                 ):
                 ):
                     owned_by = model["owned_by"]
                     owned_by = model["owned_by"]
+                    if "pipe" in model:
+                        pipe = model["pipe"]
                     break
                     break
 
 
             models.append(
             models.append(
@@ -994,11 +1049,11 @@ async def get_all_models():
                     "owned_by": owned_by,
                     "owned_by": owned_by,
                     "info": custom_model.model_dump(),
                     "info": custom_model.model_dump(),
                     "preset": True,
                     "preset": True,
+                    **({"pipe": pipe} if pipe is not None else {}),
                 }
                 }
             )
             )
 
 
     app.state.MODELS = {model["id"]: model for model in models}
     app.state.MODELS = {model["id"]: model for model in models}
-
     webui_app.state.MODELS = app.state.MODELS
     webui_app.state.MODELS = app.state.MODELS
 
 
     return models
     return models
@@ -1133,6 +1188,14 @@ async def chat_completed(form_data: dict, user=Depends(get_verified_user)):
             to=data["session_id"],
             to=data["session_id"],
         )
         )
 
 
+    async def __event_call__(data):
+        response = await sio.call(
+            "chat-events",
+            {"chat_id": data["chat_id"], "message_id": data["id"], "data": data},
+            to=data["session_id"],
+        )
+        return response
+
     def get_priority(function_id):
     def get_priority(function_id):
         function = Functions.get_function_by_id(function_id)
         function = Functions.get_function_by_id(function_id)
         if function is not None and hasattr(function, "valves"):
         if function is not None and hasattr(function, "valves"):
@@ -1220,6 +1283,12 @@ async def chat_completed(form_data: dict, user=Depends(get_verified_user)):
                             "__event_emitter__": __event_emitter__,
                             "__event_emitter__": __event_emitter__,
                         }
                         }
 
 
+                    if "__event_call__" in sig.parameters:
+                        params = {
+                            **params,
+                            "__event_call__": __event_call__,
+                        }
+
                     if inspect.iscoroutinefunction(outlet):
                     if inspect.iscoroutinefunction(outlet):
                         data = await outlet(**params)
                         data = await outlet(**params)
                     else:
                     else:
@@ -1337,7 +1406,7 @@ async def generate_title(form_data: dict, user=Depends(get_verified_user)):
         "stream": False,
         "stream": False,
         "max_tokens": 50,
         "max_tokens": 50,
         "chat_id": form_data.get("chat_id", None),
         "chat_id": form_data.get("chat_id", None),
-        "title": True,
+        "task": TASKS.TITLE_GENERATION,
     }
     }
 
 
     log.debug(payload)
     log.debug(payload)
@@ -1400,7 +1469,7 @@ async def generate_search_query(form_data: dict, user=Depends(get_verified_user)
         "messages": [{"role": "user", "content": content}],
         "messages": [{"role": "user", "content": content}],
         "stream": False,
         "stream": False,
         "max_tokens": 30,
         "max_tokens": 30,
-        "task": True,
+        "task": TASKS.QUERY_GENERATION,
     }
     }
 
 
     print(payload)
     print(payload)
@@ -1467,7 +1536,7 @@ Message: """{{prompt}}"""
         "stream": False,
         "stream": False,
         "max_tokens": 4,
         "max_tokens": 4,
         "chat_id": form_data.get("chat_id", None),
         "chat_id": form_data.get("chat_id", None),
-        "task": True,
+        "task": TASKS.EMOJI_GENERATION,
     }
     }
 
 
     log.debug(payload)
     log.debug(payload)
@@ -1742,7 +1811,9 @@ async def get_pipelines(urlIdx: Optional[int] = None, user=Depends(get_admin_use
 
 
 @app.get("/api/pipelines/{pipeline_id}/valves")
 @app.get("/api/pipelines/{pipeline_id}/valves")
 async def get_pipeline_valves(
 async def get_pipeline_valves(
-    urlIdx: Optional[int], pipeline_id: str, user=Depends(get_admin_user)
+    urlIdx: Optional[int],
+    pipeline_id: str,
+    user=Depends(get_admin_user),
 ):
 ):
     models = await get_all_models()
     models = await get_all_models()
     r = None
     r = None
@@ -1780,7 +1851,9 @@ async def get_pipeline_valves(
 
 
 @app.get("/api/pipelines/{pipeline_id}/valves/spec")
 @app.get("/api/pipelines/{pipeline_id}/valves/spec")
 async def get_pipeline_valves_spec(
 async def get_pipeline_valves_spec(
-    urlIdx: Optional[int], pipeline_id: str, user=Depends(get_admin_user)
+    urlIdx: Optional[int],
+    pipeline_id: str,
+    user=Depends(get_admin_user),
 ):
 ):
     models = await get_all_models()
     models = await get_all_models()
 
 
@@ -2066,7 +2139,8 @@ async def oauth_callback(provider: str, request: Request, response: Response):
             if existing_user:
             if existing_user:
                 raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN)
                 raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN)
 
 
-            picture_url = user_data.get("picture", "")
+            picture_claim = webui_app.state.config.OAUTH_PICTURE_CLAIM
+            picture_url = user_data.get(picture_claim, "")
             if picture_url:
             if picture_url:
                 # Download the profile image into a base64 string
                 # Download the profile image into a base64 string
                 try:
                 try:
@@ -2086,6 +2160,7 @@ async def oauth_callback(provider: str, request: Request, response: Response):
                     picture_url = ""
                     picture_url = ""
             if not picture_url:
             if not picture_url:
                 picture_url = "/user.png"
                 picture_url = "/user.png"
+            username_claim = webui_app.state.config.OAUTH_USERNAME_CLAIM
             role = (
             role = (
                 "admin"
                 "admin"
                 if Users.get_num_users() == 0
                 if Users.get_num_users() == 0
@@ -2096,7 +2171,7 @@ async def oauth_callback(provider: str, request: Request, response: Response):
                 password=get_password_hash(
                 password=get_password_hash(
                     str(uuid.uuid4())
                     str(uuid.uuid4())
                 ),  # Random password, not used
                 ),  # Random password, not used
-                name=user_data.get("name", "User"),
+                name=user_data.get(username_claim, "User"),
                 profile_image_url=picture_url,
                 profile_image_url=picture_url,
                 role=role,
                 role=role,
                 oauth_sub=provider_sub,
                 oauth_sub=provider_sub,
@@ -2154,7 +2229,7 @@ async def get_opensearch_xml():
     <ShortName>{WEBUI_NAME}</ShortName>
     <ShortName>{WEBUI_NAME}</ShortName>
     <Description>Search {WEBUI_NAME}</Description>
     <Description>Search {WEBUI_NAME}</Description>
     <InputEncoding>UTF-8</InputEncoding>
     <InputEncoding>UTF-8</InputEncoding>
-    <Image width="16" height="16" type="image/x-icon">{WEBUI_URL}/favicon.png</Image>
+    <Image width="16" height="16" type="image/x-icon">{WEBUI_URL}/static/favicon.png</Image>
     <Url type="text/html" method="get" template="{WEBUI_URL}/?q={"{searchTerms}"}"/>
     <Url type="text/html" method="get" template="{WEBUI_URL}/?q={"{searchTerms}"}"/>
     <moz:SearchForm>{WEBUI_URL}</moz:SearchForm>
     <moz:SearchForm>{WEBUI_URL}</moz:SearchForm>
     </OpenSearchDescription>
     </OpenSearchDescription>
@@ -2167,6 +2242,12 @@ async def healthcheck():
     return {"status": True}
     return {"status": True}
 
 
 
 
+@app.get("/health/db")
+async def healthcheck_with_db():
+    Session.execute(text("SELECT 1;")).all()
+    return {"status": True}
+
+
 app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
 app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
 app.mount("/cache", StaticFiles(directory=CACHE_DIR), name="cache")
 app.mount("/cache", StaticFiles(directory=CACHE_DIR), name="cache")
 
 

+ 4 - 0
backend/migrations/README

@@ -0,0 +1,4 @@
+Generic single-database configuration.
+
+Create new migrations with
+DATABASE_URL=<replace with actual url> alembic revision --autogenerate -m "a description"

+ 96 - 0
backend/migrations/env.py

@@ -0,0 +1,96 @@
+import os
+from logging.config import fileConfig
+
+from sqlalchemy import engine_from_config
+from sqlalchemy import pool
+
+from alembic import context
+
+from apps.webui.models.auths import Auth
+from apps.webui.models.chats import Chat
+from apps.webui.models.documents import Document
+from apps.webui.models.memories import Memory
+from apps.webui.models.models import Model
+from apps.webui.models.prompts import Prompt
+from apps.webui.models.tags import Tag, ChatIdTag
+from apps.webui.models.tools import Tool
+from apps.webui.models.users import User
+from apps.webui.models.files import File
+from apps.webui.models.functions import Function
+
+from config import DATABASE_URL
+
+# this is the Alembic Config object, which provides
+# access to the values within the .ini file in use.
+config = context.config
+
+# Interpret the config file for Python logging.
+# This line sets up loggers basically.
+if config.config_file_name is not None:
+    fileConfig(config.config_file_name)
+
+# add your model's MetaData object here
+# for 'autogenerate' support
+# from myapp import mymodel
+# target_metadata = mymodel.Base.metadata
+target_metadata = Auth.metadata
+
+# other values from the config, defined by the needs of env.py,
+# can be acquired:
+# my_important_option = config.get_main_option("my_important_option")
+# ... etc.
+
+DB_URL = DATABASE_URL
+
+if DB_URL:
+    config.set_main_option("sqlalchemy.url", DB_URL.replace("%", "%%"))
+
+
+def run_migrations_offline() -> None:
+    """Run migrations in 'offline' mode.
+
+    This configures the context with just a URL
+    and not an Engine, though an Engine is acceptable
+    here as well.  By skipping the Engine creation
+    we don't even need a DBAPI to be available.
+
+    Calls to context.execute() here emit the given string to the
+    script output.
+
+    """
+    url = config.get_main_option("sqlalchemy.url")
+    context.configure(
+        url=url,
+        target_metadata=target_metadata,
+        literal_binds=True,
+        dialect_opts={"paramstyle": "named"},
+    )
+
+    with context.begin_transaction():
+        context.run_migrations()
+
+
+def run_migrations_online() -> None:
+    """Run migrations in 'online' mode.
+
+    In this scenario we need to create an Engine
+    and associate a connection with the context.
+
+    """
+    connectable = engine_from_config(
+        config.get_section(config.config_ini_section, {}),
+        prefix="sqlalchemy.",
+        poolclass=pool.NullPool,
+    )
+
+    with connectable.connect() as connection:
+        context.configure(connection=connection, target_metadata=target_metadata)
+
+        with context.begin_transaction():
+            context.run_migrations()
+
+
+if context.is_offline_mode():
+    run_migrations_offline()
+else:
+    run_migrations_online()

+ 27 - 0
backend/migrations/script.py.mako

@@ -0,0 +1,27 @@
+"""${message}
+
+Revision ID: ${up_revision}
+Revises: ${down_revision | comma,n}
+Create Date: ${create_date}
+
+"""
+from typing import Sequence, Union
+
+from alembic import op
+import sqlalchemy as sa
+import apps.webui.internal.db
+${imports if imports else ""}
+
+# revision identifiers, used by Alembic.
+revision: str = ${repr(up_revision)}
+down_revision: Union[str, None] = ${repr(down_revision)}
+branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
+depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
+
+
+def upgrade() -> None:
+    ${upgrades if upgrades else "pass"}
+
+
+def downgrade() -> None:
+    ${downgrades if downgrades else "pass"}

+ 9 - 0
backend/migrations/util.py

@@ -0,0 +1,9 @@
+from alembic import op
+from sqlalchemy import Inspector
+
+
+def get_existing_tables():
+    con = op.get_bind()
+    inspector = Inspector.from_engine(con)
+    tables = set(inspector.get_table_names())
+    return tables

+ 202 - 0
backend/migrations/versions/7e5b5dc7342b_init.py

@@ -0,0 +1,202 @@
+"""init
+
+Revision ID: 7e5b5dc7342b
+Revises: 
+Create Date: 2024-06-24 13:15:33.808998
+
+"""
+
+from typing import Sequence, Union
+
+from alembic import op
+import sqlalchemy as sa
+import apps.webui.internal.db
+from migrations.util import get_existing_tables
+
+# revision identifiers, used by Alembic.
+revision: str = "7e5b5dc7342b"
+down_revision: Union[str, None] = None
+branch_labels: Union[str, Sequence[str], None] = None
+depends_on: Union[str, Sequence[str], None] = None
+
+
+def upgrade() -> None:
+    existing_tables = set(get_existing_tables())
+
+    # ### commands auto generated by Alembic - please adjust! ###
+    if "auth" not in existing_tables:
+        op.create_table(
+            "auth",
+            sa.Column("id", sa.String(), nullable=False),
+            sa.Column("email", sa.String(), nullable=True),
+            sa.Column("password", sa.Text(), nullable=True),
+            sa.Column("active", sa.Boolean(), nullable=True),
+            sa.PrimaryKeyConstraint("id"),
+        )
+
+    if "chat" not in existing_tables:
+        op.create_table(
+            "chat",
+            sa.Column("id", sa.String(), nullable=False),
+            sa.Column("user_id", sa.String(), nullable=True),
+            sa.Column("title", sa.Text(), nullable=True),
+            sa.Column("chat", sa.Text(), nullable=True),
+            sa.Column("created_at", sa.BigInteger(), nullable=True),
+            sa.Column("updated_at", sa.BigInteger(), nullable=True),
+            sa.Column("share_id", sa.Text(), nullable=True),
+            sa.Column("archived", sa.Boolean(), nullable=True),
+            sa.PrimaryKeyConstraint("id"),
+            sa.UniqueConstraint("share_id"),
+        )
+
+    if "chatidtag" not in existing_tables:
+        op.create_table(
+            "chatidtag",
+            sa.Column("id", sa.String(), nullable=False),
+            sa.Column("tag_name", sa.String(), nullable=True),
+            sa.Column("chat_id", sa.String(), nullable=True),
+            sa.Column("user_id", sa.String(), nullable=True),
+            sa.Column("timestamp", sa.BigInteger(), nullable=True),
+            sa.PrimaryKeyConstraint("id"),
+        )
+
+    if "document" not in existing_tables:
+        op.create_table(
+            "document",
+            sa.Column("collection_name", sa.String(), nullable=False),
+            sa.Column("name", sa.String(), nullable=True),
+            sa.Column("title", sa.Text(), nullable=True),
+            sa.Column("filename", sa.Text(), nullable=True),
+            sa.Column("content", sa.Text(), nullable=True),
+            sa.Column("user_id", sa.String(), nullable=True),
+            sa.Column("timestamp", sa.BigInteger(), nullable=True),
+            sa.PrimaryKeyConstraint("collection_name"),
+            sa.UniqueConstraint("name"),
+        )
+
+    if "file" not in existing_tables:
+        op.create_table(
+            "file",
+            sa.Column("id", sa.String(), nullable=False),
+            sa.Column("user_id", sa.String(), nullable=True),
+            sa.Column("filename", sa.Text(), nullable=True),
+            sa.Column("meta", apps.webui.internal.db.JSONField(), nullable=True),
+            sa.Column("created_at", sa.BigInteger(), nullable=True),
+            sa.PrimaryKeyConstraint("id"),
+        )
+
+    if "function" not in existing_tables:
+        op.create_table(
+            "function",
+            sa.Column("id", sa.String(), nullable=False),
+            sa.Column("user_id", sa.String(), nullable=True),
+            sa.Column("name", sa.Text(), nullable=True),
+            sa.Column("type", sa.Text(), nullable=True),
+            sa.Column("content", sa.Text(), nullable=True),
+            sa.Column("meta", apps.webui.internal.db.JSONField(), nullable=True),
+            sa.Column("valves", apps.webui.internal.db.JSONField(), nullable=True),
+            sa.Column("is_active", sa.Boolean(), nullable=True),
+            sa.Column("is_global", sa.Boolean(), nullable=True),
+            sa.Column("updated_at", sa.BigInteger(), nullable=True),
+            sa.Column("created_at", sa.BigInteger(), nullable=True),
+            sa.PrimaryKeyConstraint("id"),
+        )
+
+    if "memory" not in existing_tables:
+        op.create_table(
+            "memory",
+            sa.Column("id", sa.String(), nullable=False),
+            sa.Column("user_id", sa.String(), nullable=True),
+            sa.Column("content", sa.Text(), nullable=True),
+            sa.Column("updated_at", sa.BigInteger(), nullable=True),
+            sa.Column("created_at", sa.BigInteger(), nullable=True),
+            sa.PrimaryKeyConstraint("id"),
+        )
+
+    if "model" not in existing_tables:
+        op.create_table(
+            "model",
+            sa.Column("id", sa.Text(), nullable=False),
+            sa.Column("user_id", sa.Text(), nullable=True),
+            sa.Column("base_model_id", sa.Text(), nullable=True),
+            sa.Column("name", sa.Text(), nullable=True),
+            sa.Column("params", apps.webui.internal.db.JSONField(), nullable=True),
+            sa.Column("meta", apps.webui.internal.db.JSONField(), nullable=True),
+            sa.Column("updated_at", sa.BigInteger(), nullable=True),
+            sa.Column("created_at", sa.BigInteger(), nullable=True),
+            sa.PrimaryKeyConstraint("id"),
+        )
+
+    if "prompt" not in existing_tables:
+        op.create_table(
+            "prompt",
+            sa.Column("command", sa.String(), nullable=False),
+            sa.Column("user_id", sa.String(), nullable=True),
+            sa.Column("title", sa.Text(), nullable=True),
+            sa.Column("content", sa.Text(), nullable=True),
+            sa.Column("timestamp", sa.BigInteger(), nullable=True),
+            sa.PrimaryKeyConstraint("command"),
+        )
+
+    if "tag" not in existing_tables:
+        op.create_table(
+            "tag",
+            sa.Column("id", sa.String(), nullable=False),
+            sa.Column("name", sa.String(), nullable=True),
+            sa.Column("user_id", sa.String(), nullable=True),
+            sa.Column("data", sa.Text(), nullable=True),
+            sa.PrimaryKeyConstraint("id"),
+        )
+
+    if "tool" not in existing_tables:
+        op.create_table(
+            "tool",
+            sa.Column("id", sa.String(), nullable=False),
+            sa.Column("user_id", sa.String(), nullable=True),
+            sa.Column("name", sa.Text(), nullable=True),
+            sa.Column("content", sa.Text(), nullable=True),
+            sa.Column("specs", apps.webui.internal.db.JSONField(), nullable=True),
+            sa.Column("meta", apps.webui.internal.db.JSONField(), nullable=True),
+            sa.Column("valves", apps.webui.internal.db.JSONField(), nullable=True),
+            sa.Column("updated_at", sa.BigInteger(), nullable=True),
+            sa.Column("created_at", sa.BigInteger(), nullable=True),
+            sa.PrimaryKeyConstraint("id"),
+        )
+
+    if "user" not in existing_tables:
+        op.create_table(
+            "user",
+            sa.Column("id", sa.String(), nullable=False),
+            sa.Column("name", sa.String(), nullable=True),
+            sa.Column("email", sa.String(), nullable=True),
+            sa.Column("role", sa.String(), nullable=True),
+            sa.Column("profile_image_url", sa.Text(), nullable=True),
+            sa.Column("last_active_at", sa.BigInteger(), nullable=True),
+            sa.Column("updated_at", sa.BigInteger(), nullable=True),
+            sa.Column("created_at", sa.BigInteger(), nullable=True),
+            sa.Column("api_key", sa.String(), nullable=True),
+            sa.Column("settings", apps.webui.internal.db.JSONField(), nullable=True),
+            sa.Column("info", apps.webui.internal.db.JSONField(), nullable=True),
+            sa.Column("oauth_sub", sa.Text(), nullable=True),
+            sa.PrimaryKeyConstraint("id"),
+            sa.UniqueConstraint("api_key"),
+            sa.UniqueConstraint("oauth_sub"),
+        )
+    # ### end Alembic commands ###
+
+
+def downgrade() -> None:
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.drop_table("user")
+    op.drop_table("tool")
+    op.drop_table("tag")
+    op.drop_table("prompt")
+    op.drop_table("model")
+    op.drop_table("memory")
+    op.drop_table("function")
+    op.drop_table("file")
+    op.drop_table("document")
+    op.drop_table("chatidtag")
+    op.drop_table("chat")
+    op.drop_table("auth")
+    # ### end Alembic commands ###

+ 11 - 4
backend/requirements.txt

@@ -12,7 +12,9 @@ passlib[bcrypt]==1.7.4
 
 
 requests==2.32.3
 requests==2.32.3
 aiohttp==3.9.5
 aiohttp==3.9.5
-peewee==3.17.5
+sqlalchemy==2.0.30
+alembic==1.13.2
+peewee==3.17.6
 peewee-migrate==1.12.2
 peewee-migrate==1.12.2
 psycopg2-binary==2.9.9
 psycopg2-binary==2.9.9
 PyMySQL==1.1.1
 PyMySQL==1.1.1
@@ -49,7 +51,7 @@ pyxlsb==1.0.10
 xlrd==2.0.1
 xlrd==2.0.1
 validators==0.28.1
 validators==0.28.1
 
 
-opencv-python-headless==4.9.0.80
+opencv-python-headless==4.10.0.84
 rapidocr-onnxruntime==1.3.22
 rapidocr-onnxruntime==1.3.22
 
 
 fpdf2==2.7.9
 fpdf2==2.7.9
@@ -61,10 +63,15 @@ PyJWT[crypto]==2.8.0
 authlib==1.3.1
 authlib==1.3.1
 
 
 black==24.4.2
 black==24.4.2
-langfuse==2.36.2
+langfuse==2.38.0
 youtube-transcript-api==0.6.2
 youtube-transcript-api==0.6.2
 pytube==15.0.0
 pytube==15.0.0
 
 
 extract_msg
 extract_msg
 pydub
 pydub
-duckduckgo-search~=6.1.7
+duckduckgo-search~=6.1.7
+
+## Tests
+docker~=7.1.0
+pytest~=8.2.2
+pytest-docker~=3.1.1

BIN
backend/static/splash.png


+ 0 - 0
backend/test/__init__.py


+ 202 - 0
backend/test/apps/webui/routers/test_auths.py

@@ -0,0 +1,202 @@
+import pytest
+
+from test.util.abstract_integration_test import AbstractPostgresTest
+from test.util.mock_user import mock_webui_user
+
+
+class TestAuths(AbstractPostgresTest):
+    BASE_PATH = "/api/v1/auths"
+
+    def setup_class(cls):
+        super().setup_class()
+        from apps.webui.models.users import Users
+        from apps.webui.models.auths import Auths
+
+        cls.users = Users
+        cls.auths = Auths
+
+    def test_get_session_user(self):
+        with mock_webui_user():
+            response = self.fast_api_client.get(self.create_url(""))
+        assert response.status_code == 200
+        assert response.json() == {
+            "id": "1",
+            "name": "John Doe",
+            "email": "john.doe@openwebui.com",
+            "role": "user",
+            "profile_image_url": "/user.png",
+        }
+
+    def test_update_profile(self):
+        from utils.utils import get_password_hash
+
+        user = self.auths.insert_new_auth(
+            email="john.doe@openwebui.com",
+            password=get_password_hash("old_password"),
+            name="John Doe",
+            profile_image_url="/user.png",
+            role="user",
+        )
+
+        with mock_webui_user(id=user.id):
+            response = self.fast_api_client.post(
+                self.create_url("/update/profile"),
+                json={"name": "John Doe 2", "profile_image_url": "/user2.png"},
+            )
+        assert response.status_code == 200
+        db_user = self.users.get_user_by_id(user.id)
+        assert db_user.name == "John Doe 2"
+        assert db_user.profile_image_url == "/user2.png"
+
+    def test_update_password(self):
+        from utils.utils import get_password_hash
+
+        user = self.auths.insert_new_auth(
+            email="john.doe@openwebui.com",
+            password=get_password_hash("old_password"),
+            name="John Doe",
+            profile_image_url="/user.png",
+            role="user",
+        )
+
+        with mock_webui_user(id=user.id):
+            response = self.fast_api_client.post(
+                self.create_url("/update/password"),
+                json={"password": "old_password", "new_password": "new_password"},
+            )
+        assert response.status_code == 200
+
+        old_auth = self.auths.authenticate_user(
+            "john.doe@openwebui.com", "old_password"
+        )
+        assert old_auth is None
+        new_auth = self.auths.authenticate_user(
+            "john.doe@openwebui.com", "new_password"
+        )
+        assert new_auth is not None
+
+    def test_signin(self):
+        from utils.utils import get_password_hash
+
+        user = self.auths.insert_new_auth(
+            email="john.doe@openwebui.com",
+            password=get_password_hash("password"),
+            name="John Doe",
+            profile_image_url="/user.png",
+            role="user",
+        )
+        response = self.fast_api_client.post(
+            self.create_url("/signin"),
+            json={"email": "john.doe@openwebui.com", "password": "password"},
+        )
+        assert response.status_code == 200
+        data = response.json()
+        assert data["id"] == user.id
+        assert data["name"] == "John Doe"
+        assert data["email"] == "john.doe@openwebui.com"
+        assert data["role"] == "user"
+        assert data["profile_image_url"] == "/user.png"
+        assert data["token"] is not None and len(data["token"]) > 0
+        assert data["token_type"] == "Bearer"
+
+    def test_signup(self):
+        response = self.fast_api_client.post(
+            self.create_url("/signup"),
+            json={
+                "name": "John Doe",
+                "email": "john.doe@openwebui.com",
+                "password": "password",
+            },
+        )
+        assert response.status_code == 200
+        data = response.json()
+        assert data["id"] is not None and len(data["id"]) > 0
+        assert data["name"] == "John Doe"
+        assert data["email"] == "john.doe@openwebui.com"
+        assert data["role"] in ["admin", "user", "pending"]
+        assert data["profile_image_url"] == "/user.png"
+        assert data["token"] is not None and len(data["token"]) > 0
+        assert data["token_type"] == "Bearer"
+
+    def test_add_user(self):
+        with mock_webui_user():
+            response = self.fast_api_client.post(
+                self.create_url("/add"),
+                json={
+                    "name": "John Doe 2",
+                    "email": "john.doe2@openwebui.com",
+                    "password": "password2",
+                    "role": "admin",
+                },
+            )
+        assert response.status_code == 200
+        data = response.json()
+        assert data["id"] is not None and len(data["id"]) > 0
+        assert data["name"] == "John Doe 2"
+        assert data["email"] == "john.doe2@openwebui.com"
+        assert data["role"] == "admin"
+        assert data["profile_image_url"] == "/user.png"
+        assert data["token"] is not None and len(data["token"]) > 0
+        assert data["token_type"] == "Bearer"
+
+    def test_get_admin_details(self):
+        self.auths.insert_new_auth(
+            email="john.doe@openwebui.com",
+            password="password",
+            name="John Doe",
+            profile_image_url="/user.png",
+            role="admin",
+        )
+        with mock_webui_user():
+            response = self.fast_api_client.get(self.create_url("/admin/details"))
+
+        assert response.status_code == 200
+        assert response.json() == {
+            "name": "John Doe",
+            "email": "john.doe@openwebui.com",
+        }
+
+    def test_create_api_key_(self):
+        user = self.auths.insert_new_auth(
+            email="john.doe@openwebui.com",
+            password="password",
+            name="John Doe",
+            profile_image_url="/user.png",
+            role="admin",
+        )
+        with mock_webui_user(id=user.id):
+            response = self.fast_api_client.post(self.create_url("/api_key"))
+        assert response.status_code == 200
+        data = response.json()
+        assert data["api_key"] is not None
+        assert len(data["api_key"]) > 0
+
+    def test_delete_api_key(self):
+        user = self.auths.insert_new_auth(
+            email="john.doe@openwebui.com",
+            password="password",
+            name="John Doe",
+            profile_image_url="/user.png",
+            role="admin",
+        )
+        self.users.update_user_api_key_by_id(user.id, "abc")
+        with mock_webui_user(id=user.id):
+            response = self.fast_api_client.delete(self.create_url("/api_key"))
+        assert response.status_code == 200
+        assert response.json() == True
+        db_user = self.users.get_user_by_id(user.id)
+        assert db_user.api_key is None
+
+    def test_get_api_key(self):
+        user = self.auths.insert_new_auth(
+            email="john.doe@openwebui.com",
+            password="password",
+            name="John Doe",
+            profile_image_url="/user.png",
+            role="admin",
+        )
+        self.users.update_user_api_key_by_id(user.id, "abc")
+        with mock_webui_user(id=user.id):
+            response = self.fast_api_client.get(self.create_url("/api_key"))
+        assert response.status_code == 200
+        assert response.json() == {"api_key": "abc"}

+ 238 - 0
backend/test/apps/webui/routers/test_chats.py

@@ -0,0 +1,238 @@
+import uuid
+
+from test.util.abstract_integration_test import AbstractPostgresTest
+from test.util.mock_user import mock_webui_user
+
+
+class TestChats(AbstractPostgresTest):
+
+    BASE_PATH = "/api/v1/chats"
+
+    def setup_class(cls):
+        super().setup_class()
+
+    def setup_method(self):
+        super().setup_method()
+        from apps.webui.models.chats import ChatForm
+        from apps.webui.models.chats import Chats
+
+        self.chats = Chats
+        self.chats.insert_new_chat(
+            "2",
+            ChatForm(
+                **{
+                    "chat": {
+                        "name": "chat1",
+                        "description": "chat1 description",
+                        "tags": ["tag1", "tag2"],
+                        "history": {"currentId": "1", "messages": []},
+                    }
+                }
+            ),
+        )
+
+    def test_get_session_user_chat_list(self):
+        with mock_webui_user(id="2"):
+            response = self.fast_api_client.get(self.create_url("/"))
+        assert response.status_code == 200
+        first_chat = response.json()[0]
+        assert first_chat["id"] is not None
+        assert first_chat["title"] == "New Chat"
+        assert first_chat["created_at"] is not None
+        assert first_chat["updated_at"] is not None
+
+    def test_delete_all_user_chats(self):
+        with mock_webui_user(id="2"):
+            response = self.fast_api_client.delete(self.create_url("/"))
+        assert response.status_code == 200
+        assert len(self.chats.get_chats()) == 0
+
+    def test_get_user_chat_list_by_user_id(self):
+        with mock_webui_user(id="3"):
+            response = self.fast_api_client.get(self.create_url("/list/user/2"))
+        assert response.status_code == 200
+        first_chat = response.json()[0]
+        assert first_chat["id"] is not None
+        assert first_chat["title"] == "New Chat"
+        assert first_chat["created_at"] is not None
+        assert first_chat["updated_at"] is not None
+
+    def test_create_new_chat(self):
+        with mock_webui_user(id="2"):
+            response = self.fast_api_client.post(
+                self.create_url("/new"),
+                json={
+                    "chat": {
+                        "name": "chat2",
+                        "description": "chat2 description",
+                        "tags": ["tag1", "tag2"],
+                    }
+                },
+            )
+        assert response.status_code == 200
+        data = response.json()
+        assert data["archived"] is False
+        assert data["chat"] == {
+            "name": "chat2",
+            "description": "chat2 description",
+            "tags": ["tag1", "tag2"],
+        }
+        assert data["user_id"] == "2"
+        assert data["id"] is not None
+        assert data["share_id"] is None
+        assert data["title"] == "New Chat"
+        assert data["updated_at"] is not None
+        assert data["created_at"] is not None
+        assert len(self.chats.get_chats()) == 2
+
+    def test_get_user_chats(self):
+        self.test_get_session_user_chat_list()
+
+    def test_get_user_archived_chats(self):
+        self.chats.archive_all_chats_by_user_id("2")
+        from apps.webui.internal.db import Session
+
+        Session.commit()
+        with mock_webui_user(id="2"):
+            response = self.fast_api_client.get(self.create_url("/all/archived"))
+        assert response.status_code == 200
+        first_chat = response.json()[0]
+        assert first_chat["id"] is not None
+        assert first_chat["title"] == "New Chat"
+        assert first_chat["created_at"] is not None
+        assert first_chat["updated_at"] is not None
+
+    def test_get_all_user_chats_in_db(self):
+        with mock_webui_user(id="4"):
+            response = self.fast_api_client.get(self.create_url("/all/db"))
+        assert response.status_code == 200
+        assert len(response.json()) == 1
+
+    def test_get_archived_session_user_chat_list(self):
+        self.test_get_user_archived_chats()
+
+    def test_archive_all_chats(self):
+        with mock_webui_user(id="2"):
+            response = self.fast_api_client.post(self.create_url("/archive/all"))
+        assert response.status_code == 200
+        assert len(self.chats.get_archived_chats_by_user_id("2")) == 1
+
+    def test_get_shared_chat_by_id(self):
+        chat_id = self.chats.get_chats()[0].id
+        self.chats.update_chat_share_id_by_id(chat_id, chat_id)
+        with mock_webui_user(id="2"):
+            response = self.fast_api_client.get(self.create_url(f"/share/{chat_id}"))
+        assert response.status_code == 200
+        data = response.json()
+        assert data["id"] == chat_id
+        assert data["chat"] == {
+            "name": "chat1",
+            "description": "chat1 description",
+            "tags": ["tag1", "tag2"],
+            "history": {"currentId": "1", "messages": []},
+        }
+        assert data["id"] == chat_id
+        assert data["share_id"] == chat_id
+        assert data["title"] == "New Chat"
+
+    def test_get_chat_by_id(self):
+        chat_id = self.chats.get_chats()[0].id
+        with mock_webui_user(id="2"):
+            response = self.fast_api_client.get(self.create_url(f"/{chat_id}"))
+        assert response.status_code == 200
+        data = response.json()
+        assert data["id"] == chat_id
+        assert data["chat"] == {
+            "name": "chat1",
+            "description": "chat1 description",
+            "tags": ["tag1", "tag2"],
+            "history": {"currentId": "1", "messages": []},
+        }
+        assert data["share_id"] is None
+        assert data["title"] == "New Chat"
+        assert data["user_id"] == "2"
+
+    def test_update_chat_by_id(self):
+        chat_id = self.chats.get_chats()[0].id
+        with mock_webui_user(id="2"):
+            response = self.fast_api_client.post(
+                self.create_url(f"/{chat_id}"),
+                json={
+                    "chat": {
+                        "name": "chat2",
+                        "description": "chat2 description",
+                        "tags": ["tag2", "tag4"],
+                        "title": "Just another title",
+                    }
+                },
+            )
+        assert response.status_code == 200
+        data = response.json()
+        assert data["id"] == chat_id
+        assert data["chat"] == {
+            "name": "chat2",
+            "title": "Just another title",
+            "description": "chat2 description",
+            "tags": ["tag2", "tag4"],
+            "history": {"currentId": "1", "messages": []},
+        }
+        assert data["share_id"] is None
+        assert data["title"] == "Just another title"
+        assert data["user_id"] == "2"
+
+    def test_delete_chat_by_id(self):
+        chat_id = self.chats.get_chats()[0].id
+        with mock_webui_user(id="2"):
+            response = self.fast_api_client.delete(self.create_url(f"/{chat_id}"))
+        assert response.status_code == 200
+        assert response.json() is True
+
+    def test_clone_chat_by_id(self):
+        chat_id = self.chats.get_chats()[0].id
+        with mock_webui_user(id="2"):
+            response = self.fast_api_client.get(self.create_url(f"/{chat_id}/clone"))
+
+        assert response.status_code == 200
+        data = response.json()
+        assert data["id"] != chat_id
+        assert data["chat"] == {
+            "branchPointMessageId": "1",
+            "description": "chat1 description",
+            "history": {"currentId": "1", "messages": []},
+            "name": "chat1",
+            "originalChatId": chat_id,
+            "tags": ["tag1", "tag2"],
+            "title": "Clone of New Chat",
+        }
+        assert data["share_id"] is None
+        assert data["title"] == "Clone of New Chat"
+        assert data["user_id"] == "2"
+
+    def test_archive_chat_by_id(self):
+        chat_id = self.chats.get_chats()[0].id
+        with mock_webui_user(id="2"):
+            response = self.fast_api_client.get(self.create_url(f"/{chat_id}/archive"))
+        assert response.status_code == 200
+
+        chat = self.chats.get_chat_by_id(chat_id)
+        assert chat.archived is True
+
+    def test_share_chat_by_id(self):
+        chat_id = self.chats.get_chats()[0].id
+        with mock_webui_user(id="2"):
+            response = self.fast_api_client.post(self.create_url(f"/{chat_id}/share"))
+        assert response.status_code == 200
+
+        chat = self.chats.get_chat_by_id(chat_id)
+        assert chat.share_id is not None
+
+    def test_delete_shared_chat_by_id(self):
+        chat_id = self.chats.get_chats()[0].id
+        share_id = str(uuid.uuid4())
+        self.chats.update_chat_share_id_by_id(chat_id, share_id)
+        with mock_webui_user(id="2"):
+            response = self.fast_api_client.delete(self.create_url(f"/{chat_id}/share"))
+        assert response.status_code
+
+        chat = self.chats.get_chat_by_id(chat_id)
+        assert chat.share_id is None

+ 106 - 0
backend/test/apps/webui/routers/test_documents.py

@@ -0,0 +1,106 @@
+from test.util.abstract_integration_test import AbstractPostgresTest
+from test.util.mock_user import mock_webui_user
+
+
+class TestDocuments(AbstractPostgresTest):
+
+    BASE_PATH = "/api/v1/documents"
+
+    def setup_class(cls):
+        super().setup_class()
+        from apps.webui.models.documents import Documents
+
+        cls.documents = Documents
+
+    def test_documents(self):
+        # Empty database
+        assert len(self.documents.get_docs()) == 0
+        with mock_webui_user(id="2"):
+            response = self.fast_api_client.get(self.create_url("/"))
+        assert response.status_code == 200
+        assert len(response.json()) == 0
+
+        # Create a new document
+        with mock_webui_user(id="2"):
+            response = self.fast_api_client.post(
+                self.create_url("/create"),
+                json={
+                    "name": "doc_name",
+                    "title": "doc title",
+                    "collection_name": "custom collection",
+                    "filename": "doc_name.pdf",
+                    "content": "",
+                },
+            )
+        assert response.status_code == 200
+        assert response.json()["name"] == "doc_name"
+        assert len(self.documents.get_docs()) == 1
+
+        # Get the document
+        with mock_webui_user(id="2"):
+            response = self.fast_api_client.get(self.create_url("/doc?name=doc_name"))
+        assert response.status_code == 200
+        data = response.json()
+        assert data["collection_name"] == "custom collection"
+        assert data["name"] == "doc_name"
+        assert data["title"] == "doc title"
+        assert data["filename"] == "doc_name.pdf"
+        assert data["content"] == {}
+
+        # Create another document
+        with mock_webui_user(id="2"):
+            response = self.fast_api_client.post(
+                self.create_url("/create"),
+                json={
+                    "name": "doc_name 2",
+                    "title": "doc title 2",
+                    "collection_name": "custom collection 2",
+                    "filename": "doc_name2.pdf",
+                    "content": "",
+                },
+            )
+        assert response.status_code == 200
+        assert response.json()["name"] == "doc_name 2"
+        assert len(self.documents.get_docs()) == 2
+
+        # Get all documents
+        with mock_webui_user(id="2"):
+            response = self.fast_api_client.get(self.create_url("/"))
+        assert response.status_code == 200
+        assert len(response.json()) == 2
+
+        # Update the first document
+        with mock_webui_user(id="2"):
+            response = self.fast_api_client.post(
+                self.create_url("/doc/update?name=doc_name"),
+                json={"name": "doc_name rework", "title": "updated title"},
+            )
+        assert response.status_code == 200
+        data = response.json()
+        assert data["name"] == "doc_name rework"
+        assert data["title"] == "updated title"
+
+        # Tag the first document
+        with mock_webui_user(id="2"):
+            response = self.fast_api_client.post(
+                self.create_url("/doc/tags"),
+                json={
+                    "name": "doc_name rework",
+                    "tags": [{"name": "testing-tag"}, {"name": "another-tag"}],
+                },
+            )
+        assert response.status_code == 200
+        data = response.json()
+        assert data["name"] == "doc_name rework"
+        assert data["content"] == {
+            "tags": [{"name": "testing-tag"}, {"name": "another-tag"}]
+        }
+        assert len(self.documents.get_docs()) == 2
+
+        # Delete the first document
+        with mock_webui_user(id="2"):
+            response = self.fast_api_client.delete(
+                self.create_url("/doc/delete?name=doc_name rework")
+            )
+        assert response.status_code == 200
+        assert len(self.documents.get_docs()) == 1

+ 62 - 0
backend/test/apps/webui/routers/test_models.py

@@ -0,0 +1,62 @@
+from test.util.abstract_integration_test import AbstractPostgresTest
+from test.util.mock_user import mock_webui_user
+
+
+class TestModels(AbstractPostgresTest):
+
+    BASE_PATH = "/api/v1/models"
+
+    def setup_class(cls):
+        super().setup_class()
+        from apps.webui.models.models import Model
+
+        cls.models = Model
+
+    def test_models(self):
+        with mock_webui_user(id="2"):
+            response = self.fast_api_client.get(self.create_url("/"))
+        assert response.status_code == 200
+        assert len(response.json()) == 0
+
+        with mock_webui_user(id="2"):
+            response = self.fast_api_client.post(
+                self.create_url("/add"),
+                json={
+                    "id": "my-model",
+                    "base_model_id": "base-model-id",
+                    "name": "Hello World",
+                    "meta": {
+                        "profile_image_url": "/static/favicon.png",
+                        "description": "description",
+                        "capabilities": None,
+                        "model_config": {},
+                    },
+                    "params": {},
+                },
+            )
+        assert response.status_code == 200
+
+        with mock_webui_user(id="2"):
+            response = self.fast_api_client.get(self.create_url("/"))
+        assert response.status_code == 200
+        assert len(response.json()) == 1
+
+        with mock_webui_user(id="2"):
+            response = self.fast_api_client.get(
+                self.create_url(query_params={"id": "my-model"})
+            )
+        assert response.status_code == 200
+        data = response.json()[0]
+        assert data["id"] == "my-model"
+        assert data["name"] == "Hello World"
+
+        with mock_webui_user(id="2"):
+            response = self.fast_api_client.delete(
+                self.create_url("/delete?id=my-model")
+            )
+        assert response.status_code == 200
+
+        with mock_webui_user(id="2"):
+            response = self.fast_api_client.get(self.create_url("/"))
+        assert response.status_code == 200
+        assert len(response.json()) == 0

+ 92 - 0
backend/test/apps/webui/routers/test_prompts.py

@@ -0,0 +1,92 @@
+from test.util.abstract_integration_test import AbstractPostgresTest
+from test.util.mock_user import mock_webui_user
+
+
+class TestPrompts(AbstractPostgresTest):
+
+    BASE_PATH = "/api/v1/prompts"
+
+    def test_prompts(self):
+        # Get all prompts
+        with mock_webui_user(id="2"):
+            response = self.fast_api_client.get(self.create_url("/"))
+        assert response.status_code == 200
+        assert len(response.json()) == 0
+
+        # Create a two new prompts
+        with mock_webui_user(id="2"):
+            response = self.fast_api_client.post(
+                self.create_url("/create"),
+                json={
+                    "command": "/my-command",
+                    "title": "Hello World",
+                    "content": "description",
+                },
+            )
+        assert response.status_code == 200
+        with mock_webui_user(id="3"):
+            response = self.fast_api_client.post(
+                self.create_url("/create"),
+                json={
+                    "command": "/my-command2",
+                    "title": "Hello World 2",
+                    "content": "description 2",
+                },
+            )
+        assert response.status_code == 200
+
+        # Get all prompts
+        with mock_webui_user(id="2"):
+            response = self.fast_api_client.get(self.create_url("/"))
+        assert response.status_code == 200
+        assert len(response.json()) == 2
+
+        # Get prompt by command
+        with mock_webui_user(id="2"):
+            response = self.fast_api_client.get(self.create_url("/command/my-command"))
+        assert response.status_code == 200
+        data = response.json()
+        assert data["command"] == "/my-command"
+        assert data["title"] == "Hello World"
+        assert data["content"] == "description"
+        assert data["user_id"] == "2"
+
+        # Update prompt
+        with mock_webui_user(id="2"):
+            response = self.fast_api_client.post(
+                self.create_url("/command/my-command2/update"),
+                json={
+                    "command": "irrelevant for request",
+                    "title": "Hello World Updated",
+                    "content": "description Updated",
+                },
+            )
+        assert response.status_code == 200
+        data = response.json()
+        assert data["command"] == "/my-command2"
+        assert data["title"] == "Hello World Updated"
+        assert data["content"] == "description Updated"
+        assert data["user_id"] == "3"
+
+        # Get prompt by command
+        with mock_webui_user(id="2"):
+            response = self.fast_api_client.get(self.create_url("/command/my-command2"))
+        assert response.status_code == 200
+        data = response.json()
+        assert data["command"] == "/my-command2"
+        assert data["title"] == "Hello World Updated"
+        assert data["content"] == "description Updated"
+        assert data["user_id"] == "3"
+
+        # Delete prompt
+        with mock_webui_user(id="2"):
+            response = self.fast_api_client.delete(
+                self.create_url("/command/my-command/delete")
+            )
+        assert response.status_code == 200
+
+        # Get all prompts
+        with mock_webui_user(id="2"):
+            response = self.fast_api_client.get(self.create_url("/"))
+        assert response.status_code == 200
+        assert len(response.json()) == 1

+ 168 - 0
backend/test/apps/webui/routers/test_users.py

@@ -0,0 +1,168 @@
+from test.util.abstract_integration_test import AbstractPostgresTest
+from test.util.mock_user import mock_webui_user
+
+
+def _get_user_by_id(data, param):
+    return next((item for item in data if item["id"] == param), None)
+
+
+def _assert_user(data, id, **kwargs):
+    user = _get_user_by_id(data, id)
+    assert user is not None
+    comparison_data = {
+        "name": f"user {id}",
+        "email": f"user{id}@openwebui.com",
+        "profile_image_url": f"/user{id}.png",
+        "role": "user",
+        **kwargs,
+    }
+    for key, value in comparison_data.items():
+        assert user[key] == value
+
+
+class TestUsers(AbstractPostgresTest):
+
+    BASE_PATH = "/api/v1/users"
+
+    def setup_class(cls):
+        super().setup_class()
+        from apps.webui.models.users import Users
+
+        cls.users = Users
+
+    def setup_method(self):
+        super().setup_method()
+        self.users.insert_new_user(
+            id="1",
+            name="user 1",
+            email="user1@openwebui.com",
+            profile_image_url="/user1.png",
+            role="user",
+        )
+        self.users.insert_new_user(
+            id="2",
+            name="user 2",
+            email="user2@openwebui.com",
+            profile_image_url="/user2.png",
+            role="user",
+        )
+
+    def test_users(self):
+        # Get all users
+        with mock_webui_user(id="3"):
+            response = self.fast_api_client.get(self.create_url(""))
+        assert response.status_code == 200
+        assert len(response.json()) == 2
+        data = response.json()
+        _assert_user(data, "1")
+        _assert_user(data, "2")
+
+        # update role
+        with mock_webui_user(id="3"):
+            response = self.fast_api_client.post(
+                self.create_url("/update/role"), json={"id": "2", "role": "admin"}
+            )
+        assert response.status_code == 200
+        _assert_user([response.json()], "2", role="admin")
+
+        # Get all users
+        with mock_webui_user(id="3"):
+            response = self.fast_api_client.get(self.create_url(""))
+        assert response.status_code == 200
+        assert len(response.json()) == 2
+        data = response.json()
+        _assert_user(data, "1")
+        _assert_user(data, "2", role="admin")
+
+        # Get (empty) user settings
+        with mock_webui_user(id="2"):
+            response = self.fast_api_client.get(self.create_url("/user/settings"))
+        assert response.status_code == 200
+        assert response.json() is None
+
+        # Update user settings
+        with mock_webui_user(id="2"):
+            response = self.fast_api_client.post(
+                self.create_url("/user/settings/update"),
+                json={
+                    "ui": {"attr1": "value1", "attr2": "value2"},
+                    "model_config": {"attr3": "value3", "attr4": "value4"},
+                },
+            )
+        assert response.status_code == 200
+
+        # Get user settings
+        with mock_webui_user(id="2"):
+            response = self.fast_api_client.get(self.create_url("/user/settings"))
+        assert response.status_code == 200
+        assert response.json() == {
+            "ui": {"attr1": "value1", "attr2": "value2"},
+            "model_config": {"attr3": "value3", "attr4": "value4"},
+        }
+
+        # Get (empty) user info
+        with mock_webui_user(id="1"):
+            response = self.fast_api_client.get(self.create_url("/user/info"))
+        assert response.status_code == 200
+        assert response.json() is None
+
+        # Update user info
+        with mock_webui_user(id="1"):
+            response = self.fast_api_client.post(
+                self.create_url("/user/info/update"),
+                json={"attr1": "value1", "attr2": "value2"},
+            )
+        assert response.status_code == 200
+
+        # Get user info
+        with mock_webui_user(id="1"):
+            response = self.fast_api_client.get(self.create_url("/user/info"))
+        assert response.status_code == 200
+        assert response.json() == {"attr1": "value1", "attr2": "value2"}
+
+        # Get user by id
+        with mock_webui_user(id="1"):
+            response = self.fast_api_client.get(self.create_url("/2"))
+        assert response.status_code == 200
+        assert response.json() == {"name": "user 2", "profile_image_url": "/user2.png"}
+
+        # Update user by id
+        with mock_webui_user(id="1"):
+            response = self.fast_api_client.post(
+                self.create_url("/2/update"),
+                json={
+                    "name": "user 2 updated",
+                    "email": "user2-updated@openwebui.com",
+                    "profile_image_url": "/user2-updated.png",
+                },
+            )
+        assert response.status_code == 200
+
+        # Get all users
+        with mock_webui_user(id="3"):
+            response = self.fast_api_client.get(self.create_url(""))
+        assert response.status_code == 200
+        assert len(response.json()) == 2
+        data = response.json()
+        _assert_user(data, "1")
+        _assert_user(
+            data,
+            "2",
+            role="admin",
+            name="user 2 updated",
+            email="user2-updated@openwebui.com",
+            profile_image_url="/user2-updated.png",
+        )
+
+        # Delete user by id
+        with mock_webui_user(id="1"):
+            response = self.fast_api_client.delete(self.create_url("/2"))
+        assert response.status_code == 200
+
+        # Get all users
+        with mock_webui_user(id="3"):
+            response = self.fast_api_client.get(self.create_url(""))
+        assert response.status_code == 200
+        assert len(response.json()) == 1
+        data = response.json()
+        _assert_user(data, "1")

+ 161 - 0
backend/test/util/abstract_integration_test.py

@@ -0,0 +1,161 @@
+import logging
+import os
+import time
+
+import docker
+import pytest
+from docker import DockerClient
+from pytest_docker.plugin import get_docker_ip
+from fastapi.testclient import TestClient
+from sqlalchemy import text, create_engine
+
+
+log = logging.getLogger(__name__)
+
+
+def get_fast_api_client():
+    from main import app
+
+    with TestClient(app) as c:
+        return c
+
+
+class AbstractIntegrationTest:
+    BASE_PATH = None
+
+    def create_url(self, path="", query_params=None):
+        if self.BASE_PATH is None:
+            raise Exception("BASE_PATH is not set")
+        parts = self.BASE_PATH.split("/")
+        parts = [part.strip() for part in parts if part.strip() != ""]
+        path_parts = path.split("/")
+        path_parts = [part.strip() for part in path_parts if part.strip() != ""]
+        query_parts = ""
+        if query_params:
+            query_parts = "&".join(
+                [f"{key}={value}" for key, value in query_params.items()]
+            )
+            query_parts = f"?{query_parts}"
+        return "/".join(parts + path_parts) + query_parts
+
+    @classmethod
+    def setup_class(cls):
+        pass
+
+    def setup_method(self):
+        pass
+
+    @classmethod
+    def teardown_class(cls):
+        pass
+
+    def teardown_method(self):
+        pass
+
+
+class AbstractPostgresTest(AbstractIntegrationTest):
+    DOCKER_CONTAINER_NAME = "postgres-test-container-will-get-deleted"
+    docker_client: DockerClient
+
+    @classmethod
+    def _create_db_url(cls, env_vars_postgres: dict) -> str:
+        host = get_docker_ip()
+        user = env_vars_postgres["POSTGRES_USER"]
+        pw = env_vars_postgres["POSTGRES_PASSWORD"]
+        port = 8081
+        db = env_vars_postgres["POSTGRES_DB"]
+        return f"postgresql://{user}:{pw}@{host}:{port}/{db}"
+
+    @classmethod
+    def setup_class(cls):
+        super().setup_class()
+        try:
+            env_vars_postgres = {
+                "POSTGRES_USER": "user",
+                "POSTGRES_PASSWORD": "example",
+                "POSTGRES_DB": "openwebui",
+            }
+            cls.docker_client = docker.from_env()
+            cls.docker_client.containers.run(
+                "postgres:16.2",
+                detach=True,
+                environment=env_vars_postgres,
+                name=cls.DOCKER_CONTAINER_NAME,
+                ports={5432: ("0.0.0.0", 8081)},
+                command="postgres -c log_statement=all",
+            )
+            time.sleep(0.5)
+
+            database_url = cls._create_db_url(env_vars_postgres)
+            os.environ["DATABASE_URL"] = database_url
+            retries = 10
+            db = None
+            while retries > 0:
+                try:
+                    from config import BACKEND_DIR
+
+                    db = create_engine(database_url, pool_pre_ping=True)
+                    db = db.connect()
+                    log.info("postgres is ready!")
+                    break
+                except Exception as e:
+                    log.warning(e)
+                    time.sleep(3)
+                    retries -= 1
+
+            if db:
+                # import must be after setting env!
+                cls.fast_api_client = get_fast_api_client()
+                db.close()
+            else:
+                raise Exception("Could not connect to Postgres")
+        except Exception as ex:
+            log.error(ex)
+            cls.teardown_class()
+            pytest.fail(f"Could not setup test environment: {ex}")
+
+    def _check_db_connection(self):
+        from apps.webui.internal.db import Session
+
+        retries = 10
+        while retries > 0:
+            try:
+                Session.execute(text("SELECT 1"))
+                Session.commit()
+                break
+            except Exception as e:
+                Session.rollback()
+                log.warning(e)
+                time.sleep(3)
+                retries -= 1
+
+    def setup_method(self):
+        super().setup_method()
+        self._check_db_connection()
+
+    @classmethod
+    def teardown_class(cls) -> None:
+        super().teardown_class()
+        cls.docker_client.containers.get(cls.DOCKER_CONTAINER_NAME).remove(force=True)
+
+    def teardown_method(self):
+        from apps.webui.internal.db import Session
+
+        # rollback everything not yet committed
+        Session.commit()
+
+        # truncate all tables
+        tables = [
+            "auth",
+            "chat",
+            "chatidtag",
+            "document",
+            "memory",
+            "model",
+            "prompt",
+            "tag",
+            '"user"',
+        ]
+        for table in tables:
+            Session.execute(text(f"TRUNCATE TABLE {table}"))
+        Session.commit()

+ 45 - 0
backend/test/util/mock_user.py

@@ -0,0 +1,45 @@
+from contextlib import contextmanager
+
+from fastapi import FastAPI
+
+
+@contextmanager
+def mock_webui_user(**kwargs):
+    from apps.webui.main import app
+
+    with mock_user(app, **kwargs):
+        yield
+
+
+@contextmanager
+def mock_user(app: FastAPI, **kwargs):
+    from utils.utils import (
+        get_current_user,
+        get_verified_user,
+        get_admin_user,
+        get_current_user_by_api_key,
+    )
+    from apps.webui.models.users import User
+
+    def create_user():
+        user_parameters = {
+            "id": "1",
+            "name": "John Doe",
+            "email": "john.doe@openwebui.com",
+            "role": "user",
+            "profile_image_url": "/user.png",
+            "last_active_at": 1627351200,
+            "updated_at": 1627351200,
+            "created_at": 162735120,
+            **kwargs,
+        }
+        return User(**user_parameters)
+
+    app.dependency_overrides = {
+        get_current_user: create_user,
+        get_verified_user: create_user,
+        get_admin_user: create_user,
+        get_current_user_by_api_key: create_user,
+    }
+    yield
+    app.dependency_overrides = {}

+ 4 - 1
backend/utils/tools.py

@@ -59,7 +59,10 @@ def get_tools_specs(tools) -> List[dict]:
                         for param_name, param_annotation in get_type_hints(
                         for param_name, param_annotation in get_type_hints(
                             function
                             function
                         ).items()
                         ).items()
-                        if param_name != "return" and param_name != "__user__"
+                        if param_name != "return"
+                        and not (
+                            param_name.startswith("__") and param_name.endswith("__")
+                        )
                     },
                     },
                     "required": [
                     "required": [
                         name
                         name

+ 1 - 0
backend/utils/utils.py

@@ -1,5 +1,6 @@
 from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
 from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
 from fastapi import HTTPException, status, Depends, Request
 from fastapi import HTTPException, status, Depends, Request
+from sqlalchemy.orm import Session
 
 
 from apps.webui.models.users import Users
 from apps.webui.models.users import Users
 
 

+ 16 - 2
src/app.css

@@ -1,6 +1,12 @@
 @font-face {
 @font-face {
-	font-family: 'Arimo';
-	src: url('/assets/fonts/Arimo-Variable.ttf');
+	font-family: 'Inter';
+	src: url('/assets/fonts/Inter-Variable.ttf');
+	font-display: swap;
+}
+
+@font-face {
+	font-family: 'Archivo';
+	src: url('/assets/fonts/Archivo-Variable.ttf');
 	font-display: swap;
 	font-display: swap;
 }
 }
 
 
@@ -32,6 +38,10 @@ math {
 	@apply underline;
 	@apply underline;
 }
 }
 
 
+.font-primary {
+	font-family: 'Archivo', sans-serif;
+}
+
 iframe {
 iframe {
 	@apply rounded-lg;
 	@apply rounded-lg;
 }
 }
@@ -140,3 +150,7 @@ input[type='number'] {
 .cm-editor.cm-focused {
 .cm-editor.cm-focused {
 	outline: none;
 	outline: none;
 }
 }
+
+.tippy-box[data-theme~='dark'] {
+	@apply rounded-lg bg-gray-950 text-xs border border-gray-900 shadow-xl;
+}

+ 7 - 5
src/app.html

@@ -23,6 +23,8 @@
 			// 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
 			(() => {
 			(() => {
 				if (localStorage?.theme && localStorage?.theme.includes('oled')) {
 				if (localStorage?.theme && localStorage?.theme.includes('oled')) {
+					document.documentElement.style.setProperty('--color-gray-800', '#101010');
+					document.documentElement.style.setProperty('--color-gray-850', '#050505');
 					document.documentElement.style.setProperty('--color-gray-900', '#000000');
 					document.documentElement.style.setProperty('--color-gray-900', '#000000');
 					document.documentElement.style.setProperty('--color-gray-950', '#000000');
 					document.documentElement.style.setProperty('--color-gray-950', '#000000');
 					document.documentElement.classList.add('dark');
 					document.documentElement.classList.add('dark');
@@ -80,13 +82,13 @@
 				id="logo"
 				id="logo"
 				style="
 				style="
 					position: absolute;
 					position: absolute;
-					width: 6rem;
+					width: auto;
 					height: 6rem;
 					height: 6rem;
-					top: 41%;
+					top: 44%;
 					left: 50%;
 					left: 50%;
 					margin-left: -3rem;
 					margin-left: -3rem;
 				"
 				"
-				src="/logo.svg"
+				src="/static/splash.png"
 			/>
 			/>
 
 
 			<div
 			<div
@@ -105,8 +107,8 @@
 			>
 			>
 				<img
 				<img
 					id="logo-her"
 					id="logo-her"
-					style="width: 13rem; height: 13rem"
-					src="/logo.svg"
+					style="width: auto; height: 13rem"
+					src="/static/splash.png"
 					class="animate-pulse-fast"
 					class="animate-pulse-fast"
 				/>
 				/>
 
 

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

@@ -24,7 +24,7 @@
 <Modal bind:show>
 <Modal bind:show>
 	<div class="px-5 pt-4 dark:text-gray-300 text-gray-700">
 	<div class="px-5 pt-4 dark:text-gray-300 text-gray-700">
 		<div class="flex justify-between items-start">
 		<div class="flex justify-between items-start">
-			<div class="text-xl font-bold">
+			<div class="text-xl font-semibold">
 				{$i18n.t('What’s New in')}
 				{$i18n.t('What’s New in')}
 				{$WEBUI_NAME}
 				{$WEBUI_NAME}
 				<Confetti x={[-1, -0.25]} y={[0, 0.5]} />
 				<Confetti x={[-1, -0.25]} y={[0, 0.5]} />
@@ -63,7 +63,7 @@
 				{#if changelog}
 				{#if changelog}
 					{#each Object.keys(changelog) as version}
 					{#each Object.keys(changelog) as version}
 						<div class=" mb-3 pr-2">
 						<div class=" mb-3 pr-2">
-							<div class="font-bold text-xl mb-1 dark:text-white">
+							<div class="font-semibold text-xl mb-1 dark:text-white">
 								v{version} - {changelog[version].date}
 								v{version} - {changelog[version].date}
 							</div>
 							</div>
 
 
@@ -72,7 +72,7 @@
 							{#each Object.keys(changelog[version]).filter((section) => section !== 'date') as section}
 							{#each Object.keys(changelog[version]).filter((section) => section !== 'date') as section}
 								<div class="">
 								<div class="">
 									<div
 									<div
-										class="font-bold uppercase text-xs {section === 'added'
+										class="font-semibold uppercase text-xs {section === 'added'
 											? 'text-white bg-blue-600'
 											? 'text-white bg-blue-600'
 											: section === 'fixed'
 											: section === 'fixed'
 											? 'text-white bg-green-600'
 											? 'text-white bg-green-600'

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

@@ -1,5 +1,5 @@
 <script>
 <script>
-	import { getContext, tick } from 'svelte';
+	import { getContext, tick, onMount } from 'svelte';
 	import { toast } from 'svelte-sonner';
 	import { toast } from 'svelte-sonner';
 
 
 	import Database from './Settings/Database.svelte';
 	import Database from './Settings/Database.svelte';
@@ -21,17 +21,31 @@
 	const i18n = getContext('i18n');
 	const i18n = getContext('i18n');
 
 
 	let selectedTab = 'general';
 	let selectedTab = 'general';
+
+	onMount(() => {
+		const containerElement = document.getElementById('admin-settings-tabs-container');
+
+		if (containerElement) {
+			containerElement.addEventListener('wheel', function (event) {
+				if (event.deltaY !== 0) {
+					// Adjust horizontal scroll position based on vertical scroll
+					containerElement.scrollLeft += event.deltaY;
+				}
+			});
+		}
+	});
 </script>
 </script>
 
 
 <div class="flex flex-col lg:flex-row w-full h-full py-2 lg:space-x-4">
 <div class="flex flex-col lg:flex-row w-full h-full py-2 lg:space-x-4">
 	<div
 	<div
+		id="admin-settings-tabs-container"
 		class="tabs flex flex-row overflow-x-auto space-x-1 max-w-full lg:space-x-0 lg:space-y-1 lg:flex-col lg:flex-none lg:w-44 dark:text-gray-200 text-xs text-left scrollbar-none"
 		class="tabs flex flex-row overflow-x-auto space-x-1 max-w-full lg:space-x-0 lg:space-y-1 lg:flex-col lg:flex-none lg:w-44 dark:text-gray-200 text-xs text-left scrollbar-none"
 	>
 	>
 		<button
 		<button
 			class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 lg:flex-none flex text-right transition {selectedTab ===
 			class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 lg:flex-none flex text-right transition {selectedTab ===
 			'general'
 			'general'
-				? 'bg-gray-200 dark:bg-gray-800'
-				: ' hover:bg-gray-300 dark:hover:bg-gray-850'}"
+				? 'bg-gray-100 dark:bg-gray-800'
+				: ' hover:bg-gray-50 dark:hover:bg-gray-850'}"
 			on:click={() => {
 			on:click={() => {
 				selectedTab = 'general';
 				selectedTab = 'general';
 			}}
 			}}
@@ -56,8 +70,8 @@
 		<button
 		<button
 			class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
 			class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
 			'users'
 			'users'
-				? 'bg-gray-200 dark:bg-gray-800'
-				: ' hover:bg-gray-300 dark:hover:bg-gray-850'}"
+				? 'bg-gray-100 dark:bg-gray-800'
+				: ' hover:bg-gray-50 dark:hover:bg-gray-850'}"
 			on:click={() => {
 			on:click={() => {
 				selectedTab = 'users';
 				selectedTab = 'users';
 			}}
 			}}
@@ -80,8 +94,8 @@
 		<button
 		<button
 			class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
 			class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
 			'connections'
 			'connections'
-				? 'bg-gray-200 dark:bg-gray-800'
-				: ' hover:bg-gray-300 dark:hover:bg-gray-850'}"
+				? 'bg-gray-100 dark:bg-gray-800'
+				: ' hover:bg-gray-50 dark:hover:bg-gray-850'}"
 			on:click={() => {
 			on:click={() => {
 				selectedTab = 'connections';
 				selectedTab = 'connections';
 			}}
 			}}
@@ -104,8 +118,8 @@
 		<button
 		<button
 			class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
 			class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
 			'models'
 			'models'
-				? 'bg-gray-200 dark:bg-gray-800'
-				: ' hover:bg-gray-300 dark:hover:bg-gray-850'}"
+				? 'bg-gray-100 dark:bg-gray-800'
+				: ' hover:bg-gray-50 dark:hover:bg-gray-850'}"
 			on:click={() => {
 			on:click={() => {
 				selectedTab = 'models';
 				selectedTab = 'models';
 			}}
 			}}
@@ -130,8 +144,8 @@
 		<button
 		<button
 			class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
 			class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
 			'documents'
 			'documents'
-				? 'bg-gray-200 dark:bg-gray-800'
-				: ' hover:bg-gray-300 dark:hover:bg-gray-850'}"
+				? 'bg-gray-100 dark:bg-gray-800'
+				: ' hover:bg-gray-50 dark:hover:bg-gray-850'}"
 			on:click={() => {
 			on:click={() => {
 				selectedTab = 'documents';
 				selectedTab = 'documents';
 			}}
 			}}
@@ -160,8 +174,8 @@
 		<button
 		<button
 			class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
 			class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
 			'web'
 			'web'
-				? 'bg-gray-200 dark:bg-gray-800'
-				: ' hover:bg-gray-300 dark:hover:bg-gray-850'}"
+				? 'bg-gray-100 dark:bg-gray-800'
+				: ' hover:bg-gray-50 dark:hover:bg-gray-850'}"
 			on:click={() => {
 			on:click={() => {
 				selectedTab = 'web';
 				selectedTab = 'web';
 			}}
 			}}
@@ -184,8 +198,8 @@
 		<button
 		<button
 			class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
 			class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
 			'interface'
 			'interface'
-				? 'bg-gray-200 dark:bg-gray-800'
-				: ' hover:bg-gray-300 dark:hover:bg-gray-850'}"
+				? 'bg-gray-100 dark:bg-gray-800'
+				: ' hover:bg-gray-50 dark:hover:bg-gray-850'}"
 			on:click={() => {
 			on:click={() => {
 				selectedTab = 'interface';
 				selectedTab = 'interface';
 			}}
 			}}
@@ -210,8 +224,8 @@
 		<button
 		<button
 			class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
 			class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
 			'audio'
 			'audio'
-				? 'bg-gray-200 dark:bg-gray-800'
-				: ' hover:bg-gray-300 dark:hover:bg-gray-850'}"
+				? 'bg-gray-100 dark:bg-gray-800'
+				: ' hover:bg-gray-50 dark:hover:bg-gray-850'}"
 			on:click={() => {
 			on:click={() => {
 				selectedTab = 'audio';
 				selectedTab = 'audio';
 			}}
 			}}
@@ -237,8 +251,8 @@
 		<button
 		<button
 			class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
 			class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
 			'images'
 			'images'
-				? 'bg-gray-200 dark:bg-gray-800'
-				: ' hover:bg-gray-300 dark:hover:bg-gray-850'}"
+				? 'bg-gray-100 dark:bg-gray-800'
+				: ' hover:bg-gray-50 dark:hover:bg-gray-850'}"
 			on:click={() => {
 			on:click={() => {
 				selectedTab = 'images';
 				selectedTab = 'images';
 			}}
 			}}
@@ -263,8 +277,8 @@
 		<button
 		<button
 			class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
 			class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
 			'pipelines'
 			'pipelines'
-				? 'bg-gray-200 dark:bg-gray-800'
-				: ' hover:bg-gray-300 dark:hover:bg-gray-850'}"
+				? 'bg-gray-100 dark:bg-gray-800'
+				: ' hover:bg-gray-50 dark:hover:bg-gray-850'}"
 			on:click={() => {
 			on:click={() => {
 				selectedTab = 'pipelines';
 				selectedTab = 'pipelines';
 			}}
 			}}
@@ -293,8 +307,8 @@
 		<button
 		<button
 			class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
 			class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
 			'db'
 			'db'
-				? 'bg-gray-200 dark:bg-gray-800'
-				: ' hover:bg-gray-300 dark:hover:bg-gray-850'}"
+				? 'bg-gray-100 dark:bg-gray-800'
+				: ' hover:bg-gray-50 dark:hover:bg-gray-850'}"
 			on:click={() => {
 			on:click={() => {
 				selectedTab = 'db';
 				selectedTab = 'db';
 			}}
 			}}

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

@@ -138,7 +138,7 @@
 					<div>
 					<div>
 						<div class="mt-1 flex gap-2 mb-1">
 						<div class="mt-1 flex gap-2 mb-1">
 							<input
 							<input
-								class="flex-1 w-full rounded-l-lg py-2 pl-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
+								class="flex-1 w-full rounded-l-lg py-2 pl-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
 								placeholder={$i18n.t('API Base URL')}
 								placeholder={$i18n.t('API Base URL')}
 								bind:value={STT_OPENAI_API_BASE_URL}
 								bind:value={STT_OPENAI_API_BASE_URL}
 								required
 								required
@@ -156,7 +156,7 @@
 							<div class="flex-1">
 							<div class="flex-1">
 								<input
 								<input
 									list="model-list"
 									list="model-list"
-									class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
+									class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
 									bind:value={STT_MODEL}
 									bind:value={STT_MODEL}
 									placeholder="Select a model"
 									placeholder="Select a model"
 								/>
 								/>
@@ -203,7 +203,7 @@
 					<div>
 					<div>
 						<div class="mt-1 flex gap-2 mb-1">
 						<div class="mt-1 flex gap-2 mb-1">
 							<input
 							<input
-								class="flex-1 w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
+								class="flex-1 w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
 								placeholder={$i18n.t('API Base URL')}
 								placeholder={$i18n.t('API Base URL')}
 								bind:value={TTS_OPENAI_API_BASE_URL}
 								bind:value={TTS_OPENAI_API_BASE_URL}
 								required
 								required
@@ -222,7 +222,7 @@
 						<div class="flex w-full">
 						<div class="flex w-full">
 							<div class="flex-1">
 							<div class="flex-1">
 								<select
 								<select
-									class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
+									class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
 									bind:value={TTS_VOICE}
 									bind:value={TTS_VOICE}
 								>
 								>
 									<option value="" selected={TTS_VOICE !== ''}>{$i18n.t('Default')}</option>
 									<option value="" selected={TTS_VOICE !== ''}>{$i18n.t('Default')}</option>
@@ -245,7 +245,7 @@
 								<div class="flex-1">
 								<div class="flex-1">
 									<input
 									<input
 										list="voice-list"
 										list="voice-list"
-										class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
+										class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
 										bind:value={TTS_VOICE}
 										bind:value={TTS_VOICE}
 										placeholder="Select a voice"
 										placeholder="Select a voice"
 									/>
 									/>
@@ -264,7 +264,7 @@
 								<div class="flex-1">
 								<div class="flex-1">
 									<input
 									<input
 										list="model-list"
 										list="model-list"
-										class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
+										class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
 										bind:value={TTS_MODEL}
 										bind:value={TTS_MODEL}
 										placeholder="Select a model"
 										placeholder="Select a model"
 									/>
 									/>

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

@@ -200,7 +200,7 @@
 										<input
 										<input
 											class="w-full rounded-lg py-2 px-4 {pipelineUrls[url]
 											class="w-full rounded-lg py-2 px-4 {pipelineUrls[url]
 												? 'pr-8'
 												? 'pr-8'
-												: ''} text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
+												: ''} text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
 											placeholder={$i18n.t('API Base URL')}
 											placeholder={$i18n.t('API Base URL')}
 											bind:value={url}
 											bind:value={url}
 											autocomplete="off"
 											autocomplete="off"
@@ -338,7 +338,7 @@
 							{#each OLLAMA_BASE_URLS as url, idx}
 							{#each OLLAMA_BASE_URLS as url, idx}
 								<div class="flex gap-1.5">
 								<div class="flex gap-1.5">
 									<input
 									<input
-										class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
+										class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
 										placeholder={$i18n.t('Enter URL (e.g. http://localhost:11434)')}
 										placeholder={$i18n.t('Enter URL (e.g. http://localhost:11434)')}
 										bind:value={url}
 										bind:value={url}
 									/>
 									/>

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

@@ -279,7 +279,7 @@
 				</div>
 				</div>
 
 
 				<button
 				<button
-					class=" self-center text-xs p-1 px-3 bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 rounded-lg flex flex-row space-x-1 items-center {scanDirLoading
+					class=" self-center text-xs p-1 px-3 bg-gray-50 dark:bg-gray-800 dark:hover:bg-gray-700 rounded-lg flex flex-row space-x-1 items-center {scanDirLoading
 						? ' cursor-not-allowed'
 						? ' cursor-not-allowed'
 						: ''}"
 						: ''}"
 					on:click={() => {
 					on:click={() => {
@@ -352,7 +352,7 @@
 			{#if embeddingEngine === 'openai'}
 			{#if embeddingEngine === 'openai'}
 				<div class="my-0.5 flex gap-2">
 				<div class="my-0.5 flex gap-2">
 					<input
 					<input
-						class="flex-1 w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
+						class="flex-1 w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
 						placeholder={$i18n.t('API Base URL')}
 						placeholder={$i18n.t('API Base URL')}
 						bind:value={OpenAIUrl}
 						bind:value={OpenAIUrl}
 						required
 						required
@@ -415,7 +415,7 @@
 				<div class="flex w-full">
 				<div class="flex w-full">
 					<div class="flex-1 mr-2">
 					<div class="flex-1 mr-2">
 						<select
 						<select
-							class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
+							class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
 							bind:value={embeddingModel}
 							bind:value={embeddingModel}
 							placeholder={$i18n.t('Select a model')}
 							placeholder={$i18n.t('Select a model')}
 							required
 							required
@@ -424,7 +424,7 @@
 								<option value="" disabled selected>{$i18n.t('Select a model')}</option>
 								<option value="" disabled selected>{$i18n.t('Select a model')}</option>
 							{/if}
 							{/if}
 							{#each $models.filter((m) => m.id && m.ollama && !(m?.preset ?? false)) as model}
 							{#each $models.filter((m) => m.id && m.ollama && !(m?.preset ?? false)) as model}
-								<option value={model.id} class="bg-gray-100 dark:bg-gray-700">{model.name}</option>
+								<option value={model.id} class="bg-gray-50 dark:bg-gray-700">{model.name}</option>
 							{/each}
 							{/each}
 						</select>
 						</select>
 					</div>
 					</div>
@@ -433,7 +433,7 @@
 				<div class="flex w-full">
 				<div class="flex w-full">
 					<div class="flex-1 mr-2">
 					<div class="flex-1 mr-2">
 						<input
 						<input
-							class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
+							class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
 							placeholder={$i18n.t('Set embedding model (e.g. {{model}})', {
 							placeholder={$i18n.t('Set embedding model (e.g. {{model}})', {
 								model: embeddingModel.slice(-40)
 								model: embeddingModel.slice(-40)
 							})}
 							})}
@@ -443,7 +443,7 @@
 
 
 					{#if embeddingEngine === ''}
 					{#if embeddingEngine === ''}
 						<button
 						<button
-							class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
+							class="px-2.5 bg-gray-50 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
 							on:click={() => {
 							on:click={() => {
 								embeddingModelUpdateHandler();
 								embeddingModelUpdateHandler();
 							}}
 							}}
@@ -512,7 +512,7 @@
 					<div class="flex w-full">
 					<div class="flex w-full">
 						<div class="flex-1 mr-2">
 						<div class="flex-1 mr-2">
 							<input
 							<input
-								class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
+								class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
 								placeholder={$i18n.t('Set reranking model (e.g. {{model}})', {
 								placeholder={$i18n.t('Set reranking model (e.g. {{model}})', {
 									model: 'BAAI/bge-reranker-v2-m3'
 									model: 'BAAI/bge-reranker-v2-m3'
 								})}
 								})}
@@ -520,7 +520,7 @@
 							/>
 							/>
 						</div>
 						</div>
 						<button
 						<button
-							class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
+							class="px-2.5 bg-gray-50 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
 							on:click={() => {
 							on:click={() => {
 								rerankingModelUpdateHandler();
 								rerankingModelUpdateHandler();
 							}}
 							}}
@@ -602,7 +602,7 @@
 				<div class="flex w-full mt-2">
 				<div class="flex w-full mt-2">
 					<div class="flex-1 mr-2">
 					<div class="flex-1 mr-2">
 						<input
 						<input
-							class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
+							class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
 							placeholder={$i18n.t('Enter Tika Server URL')}
 							placeholder={$i18n.t('Enter Tika Server URL')}
 							bind:value={tikaServerUrl}
 							bind:value={tikaServerUrl}
 						/>
 						/>
@@ -621,7 +621,7 @@
 
 
 					<div class="self-center p-3">
 					<div class="self-center p-3">
 						<input
 						<input
-							class=" w-full rounded-lg py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
+							class=" w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
 							type="number"
 							type="number"
 							placeholder={$i18n.t('Enter Top K')}
 							placeholder={$i18n.t('Enter Top K')}
 							bind:value={querySettings.k}
 							bind:value={querySettings.k}
@@ -639,7 +639,7 @@
 
 
 						<div class="self-center p-3">
 						<div class="self-center p-3">
 							<input
 							<input
-								class=" w-full rounded-lg py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
+								class=" w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
 								type="number"
 								type="number"
 								step="0.01"
 								step="0.01"
 								placeholder={$i18n.t('Enter Score')}
 								placeholder={$i18n.t('Enter Score')}
@@ -667,7 +667,7 @@
 				<div class=" mb-2.5 text-sm font-medium">{$i18n.t('RAG Template')}</div>
 				<div class=" mb-2.5 text-sm font-medium">{$i18n.t('RAG Template')}</div>
 				<textarea
 				<textarea
 					bind:value={querySettings.template}
 					bind:value={querySettings.template}
-					class="w-full rounded-lg px-4 py-3 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
+					class="w-full rounded-lg px-4 py-3 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
 					rows="4"
 					rows="4"
 				/>
 				/>
 			</div>
 			</div>
@@ -683,7 +683,7 @@
 					<div class="self-center text-xs font-medium min-w-fit mb-1">{$i18n.t('Chunk Size')}</div>
 					<div class="self-center text-xs font-medium min-w-fit mb-1">{$i18n.t('Chunk Size')}</div>
 					<div class="self-center">
 					<div class="self-center">
 						<input
 						<input
-							class=" w-full rounded-lg py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
+							class=" w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
 							type="number"
 							type="number"
 							placeholder={$i18n.t('Enter Chunk Size')}
 							placeholder={$i18n.t('Enter Chunk Size')}
 							bind:value={chunkSize}
 							bind:value={chunkSize}
@@ -700,7 +700,7 @@
 
 
 					<div class="self-center">
 					<div class="self-center">
 						<input
 						<input
-							class="w-full rounded-lg py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
+							class="w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
 							type="number"
 							type="number"
 							placeholder={$i18n.t('Enter Chunk Overlap')}
 							placeholder={$i18n.t('Enter Chunk Overlap')}
 							bind:value={chunkOverlap}
 							bind:value={chunkOverlap}

+ 2 - 2
src/lib/components/admin/Settings/General.svelte

@@ -107,7 +107,7 @@
 
 
 					<div class="flex mt-2 space-x-2">
 					<div class="flex mt-2 space-x-2">
 						<input
 						<input
-							class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
+							class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
 							type="text"
 							type="text"
 							placeholder={`e.g.) "30m","1h", "10d". `}
 							placeholder={`e.g.) "30m","1h", "10d". `}
 							bind:value={adminConfig.JWT_EXPIRES_IN}
 							bind:value={adminConfig.JWT_EXPIRES_IN}
@@ -131,7 +131,7 @@
 
 
 					<div class="flex mt-2 space-x-2">
 					<div class="flex mt-2 space-x-2">
 						<input
 						<input
-							class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
+							class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
 							type="text"
 							type="text"
 							placeholder={`https://example.com/webhook`}
 							placeholder={`https://example.com/webhook`}
 							bind:value={webhookUrl}
 							bind:value={webhookUrl}

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

@@ -240,13 +240,13 @@
 			<div class="flex w-full">
 			<div class="flex w-full">
 				<div class="flex-1 mr-2">
 				<div class="flex-1 mr-2">
 					<input
 					<input
-						class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
+						class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
 						placeholder={$i18n.t('Enter URL (e.g. http://127.0.0.1:7860/)')}
 						placeholder={$i18n.t('Enter URL (e.g. http://127.0.0.1:7860/)')}
 						bind:value={AUTOMATIC1111_BASE_URL}
 						bind:value={AUTOMATIC1111_BASE_URL}
 					/>
 					/>
 				</div>
 				</div>
 				<button
 				<button
-					class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
+					class="px-2.5 bg-gray-50 hover:bg-gray-100 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
 					type="button"
 					type="button"
 					on:click={() => {
 					on:click={() => {
 						updateUrlHandler();
 						updateUrlHandler();
@@ -299,13 +299,13 @@
 			<div class="flex w-full">
 			<div class="flex w-full">
 				<div class="flex-1 mr-2">
 				<div class="flex-1 mr-2">
 					<input
 					<input
-						class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
+						class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
 						placeholder={$i18n.t('Enter URL (e.g. http://127.0.0.1:7860/)')}
 						placeholder={$i18n.t('Enter URL (e.g. http://127.0.0.1:7860/)')}
 						bind:value={COMFYUI_BASE_URL}
 						bind:value={COMFYUI_BASE_URL}
 					/>
 					/>
 				</div>
 				</div>
 				<button
 				<button
-					class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
+					class="px-2.5 bg-gray-50 hover:bg-gray-100 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
 					type="button"
 					type="button"
 					on:click={() => {
 					on:click={() => {
 						updateUrlHandler();
 						updateUrlHandler();
@@ -331,7 +331,7 @@
 
 
 				<div class="flex gap-2 mb-1">
 				<div class="flex gap-2 mb-1">
 					<input
 					<input
-						class="flex-1 w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
+						class="flex-1 w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
 						placeholder={$i18n.t('API Base URL')}
 						placeholder={$i18n.t('API Base URL')}
 						bind:value={OPENAI_API_BASE_URL}
 						bind:value={OPENAI_API_BASE_URL}
 						required
 						required
@@ -354,7 +354,7 @@
 								<div class="flex-1">
 								<div class="flex-1">
 									<input
 									<input
 										list="model-list"
 										list="model-list"
-										class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
+										class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
 										bind:value={selectedModel}
 										bind:value={selectedModel}
 										placeholder="Select a model"
 										placeholder="Select a model"
 									/>
 									/>
@@ -368,7 +368,7 @@
 							</div>
 							</div>
 						{:else}
 						{:else}
 							<select
 							<select
-								class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
+								class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
 								bind:value={selectedModel}
 								bind:value={selectedModel}
 								placeholder={$i18n.t('Select a model')}
 								placeholder={$i18n.t('Select a model')}
 								required
 								required
@@ -391,7 +391,7 @@
 				<div class="flex w-full">
 				<div class="flex w-full">
 					<div class="flex-1 mr-2">
 					<div class="flex-1 mr-2">
 						<input
 						<input
-							class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
+							class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
 							placeholder={$i18n.t('Enter Image Size (e.g. 512x512)')}
 							placeholder={$i18n.t('Enter Image Size (e.g. 512x512)')}
 							bind:value={imageSize}
 							bind:value={imageSize}
 						/>
 						/>
@@ -404,7 +404,7 @@
 				<div class="flex w-full">
 				<div class="flex w-full">
 					<div class="flex-1 mr-2">
 					<div class="flex-1 mr-2">
 						<input
 						<input
-							class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
+							class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
 							placeholder={$i18n.t('Enter Number of Steps (e.g. 50)')}
 							placeholder={$i18n.t('Enter Number of Steps (e.g. 50)')}
 							bind:value={steps}
 							bind:value={steps}
 						/>
 						/>

+ 12 - 10
src/lib/components/admin/Settings/Interface.svelte

@@ -88,7 +88,7 @@
 				<div class="flex-1">
 				<div class="flex-1">
 					<div class=" text-xs mb-1">{$i18n.t('Local Models')}</div>
 					<div class=" text-xs mb-1">{$i18n.t('Local Models')}</div>
 					<select
 					<select
-						class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
+						class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
 						bind:value={taskConfig.TASK_MODEL}
 						bind:value={taskConfig.TASK_MODEL}
 						placeholder={$i18n.t('Select a model')}
 						placeholder={$i18n.t('Select a model')}
 					>
 					>
@@ -104,7 +104,7 @@
 				<div class="flex-1">
 				<div class="flex-1">
 					<div class=" text-xs mb-1">{$i18n.t('External Models')}</div>
 					<div class=" text-xs mb-1">{$i18n.t('External Models')}</div>
 					<select
 					<select
-						class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
+						class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
 						bind:value={taskConfig.TASK_MODEL_EXTERNAL}
 						bind:value={taskConfig.TASK_MODEL_EXTERNAL}
 						placeholder={$i18n.t('Select a model')}
 						placeholder={$i18n.t('Select a model')}
 					>
 					>
@@ -122,7 +122,7 @@
 				<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Title Generation Prompt')}</div>
 				<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Title Generation Prompt')}</div>
 				<textarea
 				<textarea
 					bind:value={taskConfig.TITLE_GENERATION_PROMPT_TEMPLATE}
 					bind:value={taskConfig.TITLE_GENERATION_PROMPT_TEMPLATE}
-					class="w-full rounded-lg py-3 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
+					class="w-full rounded-lg py-3 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
 					rows="6"
 					rows="6"
 				/>
 				/>
 			</div>
 			</div>
@@ -131,7 +131,7 @@
 				<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Search Query Generation Prompt')}</div>
 				<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Search Query Generation Prompt')}</div>
 				<textarea
 				<textarea
 					bind:value={taskConfig.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE}
 					bind:value={taskConfig.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE}
-					class="w-full rounded-lg py-3 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
+					class="w-full rounded-lg py-3 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
 					rows="6"
 					rows="6"
 				/>
 				/>
 			</div>
 			</div>
@@ -142,7 +142,7 @@
 				</div>
 				</div>
 				<input
 				<input
 					bind:value={taskConfig.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD}
 					bind:value={taskConfig.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD}
-					class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
+					class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
 					type="number"
 					type="number"
 				/>
 				/>
 			</div>
 			</div>
@@ -273,24 +273,26 @@
 				</div>
 				</div>
 				<div class="grid lg:grid-cols-2 flex-col gap-1.5">
 				<div class="grid lg:grid-cols-2 flex-col gap-1.5">
 					{#each promptSuggestions as prompt, promptIdx}
 					{#each promptSuggestions as prompt, promptIdx}
-						<div class=" flex dark:bg-gray-850 rounded-xl py-1.5">
+						<div
+							class=" flex border border-gray-100 dark:border-none dark:bg-gray-850 rounded-xl py-1.5"
+						>
 							<div class="flex flex-col flex-1 pl-1">
 							<div class="flex flex-col flex-1 pl-1">
-								<div class="flex border-b dark:border-gray-800 w-full">
+								<div class="flex border-b border-gray-100 dark:border-gray-800 w-full">
 									<input
 									<input
-										class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-800"
+										class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r border-gray-100 dark:border-gray-800"
 										placeholder={$i18n.t('Title (e.g. Tell me a fun fact)')}
 										placeholder={$i18n.t('Title (e.g. Tell me a fun fact)')}
 										bind:value={prompt.title[0]}
 										bind:value={prompt.title[0]}
 									/>
 									/>
 
 
 									<input
 									<input
-										class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-800"
+										class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r border-gray-100 dark:border-gray-800"
 										placeholder={$i18n.t('Subtitle (e.g. about the Roman Empire)')}
 										placeholder={$i18n.t('Subtitle (e.g. about the Roman Empire)')}
 										bind:value={prompt.title[1]}
 										bind:value={prompt.title[1]}
 									/>
 									/>
 								</div>
 								</div>
 
 
 								<input
 								<input
-									class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-800"
+									class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r border-gray-100 dark:border-gray-800"
 									placeholder={$i18n.t('Prompt (e.g. Tell me a fun fact about the Roman Empire)')}
 									placeholder={$i18n.t('Prompt (e.g. Tell me a fun fact about the Roman Empire)')}
 									bind:value={prompt.content}
 									bind:value={prompt.content}
 								/>
 								/>

+ 23 - 21
src/lib/components/admin/Settings/Models.svelte

@@ -158,12 +158,14 @@
 			return;
 			return;
 		}
 		}
 
 
-		const [res, controller] = await pullModel(localStorage.token, sanitizedModelTag, '0').catch(
-			(error) => {
-				toast.error(error);
-				return null;
-			}
-		);
+		const [res, controller] = await pullModel(
+			localStorage.token,
+			sanitizedModelTag,
+			selectedOllamaUrlIdx
+		).catch((error) => {
+			toast.error(error);
+			return null;
+		});
 
 
 		if (res) {
 		if (res) {
 			const reader = res.body
 			const reader = res.body
@@ -570,12 +572,12 @@
 						<div class="flex gap-2">
 						<div class="flex gap-2">
 							<div class="flex-1 pb-1">
 							<div class="flex-1 pb-1">
 								<select
 								<select
-									class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
+									class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
 									bind:value={selectedOllamaUrlIdx}
 									bind:value={selectedOllamaUrlIdx}
 									placeholder={$i18n.t('Select an Ollama instance')}
 									placeholder={$i18n.t('Select an Ollama instance')}
 								>
 								>
 									{#each OLLAMA_URLS as url, idx}
 									{#each OLLAMA_URLS as url, idx}
-										<option value={idx} class="bg-gray-100 dark:bg-gray-700">{url}</option>
+										<option value={idx} class="bg-gray-50 dark:bg-gray-700">{url}</option>
 									{/each}
 									{/each}
 								</select>
 								</select>
 							</div>
 							</div>
@@ -584,7 +586,7 @@
 								<div class="flex w-full justify-end">
 								<div class="flex w-full justify-end">
 									<Tooltip content="Update All Models" placement="top">
 									<Tooltip content="Update All Models" placement="top">
 										<button
 										<button
-											class="p-2.5 flex gap-2 items-center bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
+											class="p-2.5 flex gap-2 items-center bg-gray-50 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
 											on:click={() => {
 											on:click={() => {
 												updateModelsHandler();
 												updateModelsHandler();
 											}}
 											}}
@@ -619,7 +621,7 @@
 							<div class="flex w-full">
 							<div class="flex w-full">
 								<div class="flex-1 mr-2">
 								<div class="flex-1 mr-2">
 									<input
 									<input
-										class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
+										class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
 										placeholder={$i18n.t('Enter model tag (e.g. {{modelTag}})', {
 										placeholder={$i18n.t('Enter model tag (e.g. {{modelTag}})', {
 											modelTag: 'mistral:7b'
 											modelTag: 'mistral:7b'
 										})}
 										})}
@@ -627,7 +629,7 @@
 									/>
 									/>
 								</div>
 								</div>
 								<button
 								<button
-									class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
+									class="px-2.5 bg-gray-50 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
 									on:click={() => {
 									on:click={() => {
 										pullModelHandler();
 										pullModelHandler();
 									}}
 									}}
@@ -753,7 +755,7 @@
 							<div class="flex w-full">
 							<div class="flex w-full">
 								<div class="flex-1 mr-2">
 								<div class="flex-1 mr-2">
 									<select
 									<select
-										class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
+										class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
 										bind:value={deleteModelTag}
 										bind:value={deleteModelTag}
 										placeholder={$i18n.t('Select a model')}
 										placeholder={$i18n.t('Select a model')}
 									>
 									>
@@ -761,7 +763,7 @@
 											<option value="" disabled selected>{$i18n.t('Select a model')}</option>
 											<option value="" disabled selected>{$i18n.t('Select a model')}</option>
 										{/if}
 										{/if}
 										{#each $models.filter((m) => !(m?.preset ?? false) && m.owned_by === 'ollama' && (selectedOllamaUrlIdx === null ? true : (m?.ollama?.urls ?? []).includes(selectedOllamaUrlIdx))) as model}
 										{#each $models.filter((m) => !(m?.preset ?? false) && m.owned_by === 'ollama' && (selectedOllamaUrlIdx === null ? true : (m?.ollama?.urls ?? []).includes(selectedOllamaUrlIdx))) as model}
-											<option value={model.name} class="bg-gray-100 dark:bg-gray-700"
+											<option value={model.name} class="bg-gray-50 dark:bg-gray-700"
 												>{model.name +
 												>{model.name +
 													' (' +
 													' (' +
 													(model.ollama.size / 1024 ** 3).toFixed(1) +
 													(model.ollama.size / 1024 ** 3).toFixed(1) +
@@ -771,7 +773,7 @@
 									</select>
 									</select>
 								</div>
 								</div>
 								<button
 								<button
-									class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
+									class="px-2.5 bg-gray-50 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
 									on:click={() => {
 									on:click={() => {
 										showModelDeleteConfirm = true;
 										showModelDeleteConfirm = true;
 									}}
 									}}
@@ -797,7 +799,7 @@
 							<div class="flex w-full">
 							<div class="flex w-full">
 								<div class="flex-1 mr-2 flex flex-col gap-2">
 								<div class="flex-1 mr-2 flex flex-col gap-2">
 									<input
 									<input
-										class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
+										class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
 										placeholder={$i18n.t('Enter model tag (e.g. {{modelTag}})', {
 										placeholder={$i18n.t('Enter model tag (e.g. {{modelTag}})', {
 											modelTag: 'my-modelfile'
 											modelTag: 'my-modelfile'
 										})}
 										})}
@@ -807,7 +809,7 @@
 
 
 									<textarea
 									<textarea
 										bind:value={createModelContent}
 										bind:value={createModelContent}
-										class="w-full rounded-lg py-2 px-4 text-sm bg-gray-100 dark:text-gray-100 dark:bg-gray-850 outline-none resize-none scrollbar-hidden"
+										class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-100 dark:bg-gray-850 outline-none resize-none scrollbar-hidden"
 										rows="6"
 										rows="6"
 										placeholder={`TEMPLATE """{{ .System }}\nUSER: {{ .Prompt }}\nASSISTANT: """\nPARAMETER num_ctx 4096\nPARAMETER stop "</s>"\nPARAMETER stop "USER:"\nPARAMETER stop "ASSISTANT:"`}
 										placeholder={`TEMPLATE """{{ .System }}\nUSER: {{ .Prompt }}\nASSISTANT: """\nPARAMETER num_ctx 4096\nPARAMETER stop "</s>"\nPARAMETER stop "USER:"\nPARAMETER stop "ASSISTANT:"`}
 										disabled={createModelLoading}
 										disabled={createModelLoading}
@@ -816,7 +818,7 @@
 
 
 								<div class="flex self-start">
 								<div class="flex self-start">
 									<button
 									<button
-										class="px-2.5 py-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition disabled:cursor-not-allowed"
+										class="px-2.5 py-2.5 bg-gray-50 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition disabled:cursor-not-allowed"
 										on:click={() => {
 										on:click={() => {
 											createModelHandler();
 											createModelHandler();
 										}}
 										}}
@@ -925,7 +927,7 @@
 
 
 												<button
 												<button
 													type="button"
 													type="button"
-													class="w-full rounded-lg text-left py-2 px-4 bg-white dark:text-gray-300 dark:bg-gray-850"
+													class="w-full rounded-lg text-left py-2 px-4 bg-gray-50 dark:text-gray-300 dark:bg-gray-850"
 													on:click={() => {
 													on:click={() => {
 														modelUploadInputElement.click();
 														modelUploadInputElement.click();
 													}}
 													}}
@@ -940,7 +942,7 @@
 										{:else}
 										{:else}
 											<div class="flex-1 {modelFileUrl !== '' ? 'mr-2' : ''}">
 											<div class="flex-1 {modelFileUrl !== '' ? 'mr-2' : ''}">
 												<input
 												<input
-													class="w-full rounded-lg text-left py-2 px-4 bg-white dark:text-gray-300 dark:bg-gray-850 outline-none {modelFileUrl !==
+													class="w-full rounded-lg text-left py-2 px-4 bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none {modelFileUrl !==
 													''
 													''
 														? 'mr-2'
 														? 'mr-2'
 														: ''}"
 														: ''}"
@@ -955,7 +957,7 @@
 
 
 									{#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')}
 									{#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')}
 										<button
 										<button
-											class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg disabled:cursor-not-allowed transition"
+											class="px-2.5 bg-gray-50 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg disabled:cursor-not-allowed transition"
 											type="submit"
 											type="submit"
 											disabled={modelTransferring}
 											disabled={modelTransferring}
 										>
 										>
@@ -1014,7 +1016,7 @@
 											<div class=" my-2.5 text-sm font-medium">{$i18n.t('Modelfile Content')}</div>
 											<div class=" my-2.5 text-sm font-medium">{$i18n.t('Modelfile Content')}</div>
 											<textarea
 											<textarea
 												bind:value={modelFileContent}
 												bind:value={modelFileContent}
-												class="w-full rounded-lg py-2 px-4 text-sm bg-gray-100 dark:text-gray-100 dark:bg-gray-850 outline-none resize-none"
+												class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-100 dark:bg-gray-850 outline-none resize-none"
 												rows="6"
 												rows="6"
 											/>
 											/>
 										</div>
 										</div>

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

@@ -214,7 +214,7 @@
 					<div class="flex gap-2">
 					<div class="flex gap-2">
 						<div class="flex-1">
 						<div class="flex-1">
 							<select
 							<select
-								class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
+								class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
 								bind:value={selectedPipelinesUrlIdx}
 								bind:value={selectedPipelinesUrlIdx}
 								placeholder={$i18n.t('Select a pipeline url')}
 								placeholder={$i18n.t('Select a pipeline url')}
 								on:change={async () => {
 								on:change={async () => {
@@ -328,7 +328,7 @@
 					<div class="flex w-full">
 					<div class="flex w-full">
 						<div class="flex-1 mr-2">
 						<div class="flex-1 mr-2">
 							<input
 							<input
-								class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
+								class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
 								placeholder={$i18n.t('Enter Github Raw URL')}
 								placeholder={$i18n.t('Enter Github Raw URL')}
 								bind:value={pipelineDownloadUrl}
 								bind:value={pipelineDownloadUrl}
 							/>
 							/>
@@ -412,7 +412,7 @@
 								<div class="flex gap-2">
 								<div class="flex gap-2">
 									<div class="flex-1">
 									<div class="flex-1">
 										<select
 										<select
-											class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
+											class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
 											bind:value={selectedPipelineIdx}
 											bind:value={selectedPipelineIdx}
 											placeholder={$i18n.t('Select a pipeline')}
 											placeholder={$i18n.t('Select a pipeline')}
 											on:change={async () => {
 											on:change={async () => {
@@ -482,7 +482,7 @@
 														<div class=" flex-1">
 														<div class=" flex-1">
 															{#if valves_spec.properties[property]?.enum ?? null}
 															{#if valves_spec.properties[property]?.enum ?? null}
 																<select
 																<select
-																	class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
+																	class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
 																	bind:value={valves[property]}
 																	bind:value={valves[property]}
 																>
 																>
 																	{#each valves_spec.properties[property].enum as option}
 																	{#each valves_spec.properties[property].enum as option}
@@ -503,7 +503,7 @@
 																</div>
 																</div>
 															{:else}
 															{:else}
 																<input
 																<input
-																	class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
+																	class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
 																	type="text"
 																	type="text"
 																	placeholder={valves_spec.properties[property].title}
 																	placeholder={valves_spec.properties[property].title}
 																	bind:value={valves[property]}
 																	bind:value={valves[property]}

+ 2 - 2
src/lib/components/admin/Settings/Users.svelte

@@ -112,7 +112,7 @@
 
 
 					<div class="flex-1 mr-2">
 					<div class="flex-1 mr-2">
 						<select
 						<select
-							class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
+							class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
 							bind:value={defaultModelId}
 							bind:value={defaultModelId}
 							placeholder="Select a model"
 							placeholder="Select a model"
 						>
 						>
@@ -140,7 +140,7 @@
 									<div class="flex w-full">
 									<div class="flex w-full">
 										<div class="flex-1 mr-2">
 										<div class="flex-1 mr-2">
 											<select
 											<select
-												class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
+												class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
 												bind:value={modelId}
 												bind:value={modelId}
 												placeholder="Select a model"
 												placeholder="Select a model"
 											>
 											>

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

@@ -101,7 +101,7 @@
 								<div class="flex w-full">
 								<div class="flex w-full">
 									<div class="flex-1">
 									<div class="flex-1">
 										<input
 										<input
-											class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
+											class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
 											type="text"
 											type="text"
 											placeholder={$i18n.t('Enter Searxng Query URL')}
 											placeholder={$i18n.t('Enter Searxng Query URL')}
 											bind:value={webConfig.search.searxng_query_url}
 											bind:value={webConfig.search.searxng_query_url}
@@ -129,7 +129,7 @@
 								<div class="flex w-full">
 								<div class="flex w-full">
 									<div class="flex-1">
 									<div class="flex-1">
 										<input
 										<input
-											class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
+											class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
 											type="text"
 											type="text"
 											placeholder={$i18n.t('Enter Google PSE Engine Id')}
 											placeholder={$i18n.t('Enter Google PSE Engine Id')}
 											bind:value={webConfig.search.google_pse_engine_id}
 											bind:value={webConfig.search.google_pse_engine_id}
@@ -205,7 +205,7 @@
 							</div>
 							</div>
 
 
 							<input
 							<input
-								class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
+								class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
 								placeholder={$i18n.t('Search Result Count')}
 								placeholder={$i18n.t('Search Result Count')}
 								bind:value={webConfig.search.result_count}
 								bind:value={webConfig.search.result_count}
 								required
 								required
@@ -218,7 +218,7 @@
 							</div>
 							</div>
 
 
 							<input
 							<input
-								class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
+								class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
 								placeholder={$i18n.t('Concurrent Requests')}
 								placeholder={$i18n.t('Concurrent Requests')}
 								bind:value={webConfig.search.concurrent_requests}
 								bind:value={webConfig.search.concurrent_requests}
 								required
 								required
@@ -267,7 +267,7 @@
 						<div class=" w-20 text-xs font-medium self-center">{$i18n.t('Language')}</div>
 						<div class=" w-20 text-xs font-medium self-center">{$i18n.t('Language')}</div>
 						<div class=" flex-1 self-center">
 						<div class=" flex-1 self-center">
 							<input
 							<input
-								class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
+								class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
 								type="text"
 								type="text"
 								placeholder={$i18n.t('Enter language codes')}
 								placeholder={$i18n.t('Enter language codes')}
 								bind:value={youtubeLanguage}
 								bind:value={youtubeLanguage}

+ 102 - 60
src/lib/components/chat/Chat.svelte

@@ -60,19 +60,26 @@
 	import Navbar from '$lib/components/layout/Navbar.svelte';
 	import Navbar from '$lib/components/layout/Navbar.svelte';
 	import CallOverlay from './MessageInput/CallOverlay.svelte';
 	import CallOverlay from './MessageInput/CallOverlay.svelte';
 	import { error } from '@sveltejs/kit';
 	import { error } from '@sveltejs/kit';
+	import ChatControls from './ChatControls.svelte';
+	import EventConfirmDialog from '../common/ConfirmDialog.svelte';
 
 
 	const i18n: Writable<i18nType> = getContext('i18n');
 	const i18n: Writable<i18nType> = getContext('i18n');
 
 
 	export let chatIdProp = '';
 	export let chatIdProp = '';
 	let loaded = false;
 	let loaded = false;
-
 	const eventTarget = new EventTarget();
 	const eventTarget = new EventTarget();
 
 
+	let showControls = false;
 	let stopResponseFlag = false;
 	let stopResponseFlag = false;
 	let autoScroll = true;
 	let autoScroll = true;
 	let processing = '';
 	let processing = '';
 	let messagesContainerElement: HTMLDivElement;
 	let messagesContainerElement: HTMLDivElement;
 
 
+	let showEventConfirmation = false;
+	let eventConfirmationTitle = '';
+	let eventConfirmationMessage = '';
+	let eventCallback = null;
+
 	let showModelSelector = true;
 	let showModelSelector = true;
 
 
 	let selectedModels = [''];
 	let selectedModels = [''];
@@ -96,6 +103,8 @@
 		currentId: null
 		currentId: null
 	};
 	};
 
 
+	let params = {};
+
 	$: if (history.currentId !== null) {
 	$: if (history.currentId !== null) {
 		let _messages = [];
 		let _messages = [];
 
 
@@ -126,21 +135,35 @@
 		})();
 		})();
 	}
 	}
 
 
-	const chatEventHandler = async (data) => {
-		if (data.chat_id === $chatId) {
+	const chatEventHandler = async (event, cb) => {
+		if (event.chat_id === $chatId) {
 			await tick();
 			await tick();
-			console.log(data);
-			let message = history.messages[data.message_id];
+			console.log(event);
+			let message = history.messages[event.message_id];
 
 
-			const status = {
-				done: data?.data?.done ?? null,
-				description: data?.data?.status ?? null
-			};
+			const type = event?.data?.type ?? null;
+			const data = event?.data?.data ?? null;
+
+			if (type === 'status') {
+				if (message?.statusHistory) {
+					message.statusHistory.push(data);
+				} else {
+					message.statusHistory = [data];
+				}
+			} else if (type === 'citation') {
+				if (message?.citations) {
+					message.citations.push(data);
+				} else {
+					message.citations = [data];
+				}
+			} else if (type === 'confirmation') {
+				eventCallback = cb;
+				showEventConfirmation = true;
 
 
-			if (message.statusHistory) {
-				message.statusHistory.push(status);
+				eventConfirmationTitle = data.title;
+				eventConfirmationMessage = data.message;
 			} else {
 			} else {
-				message.statusHistory = [status];
+				console.log('Unknown message type', data);
 			}
 			}
 
 
 			messages = messages;
 			messages = messages;
@@ -221,6 +244,7 @@
 			messages: {},
 			messages: {},
 			currentId: null
 			currentId: null
 		};
 		};
+		params = {};
 
 
 		if ($page.url.searchParams.get('models')) {
 		if ($page.url.searchParams.get('models')) {
 			selectedModels = $page.url.searchParams.get('models')?.split(',');
 			selectedModels = $page.url.searchParams.get('models')?.split(',');
@@ -290,11 +314,7 @@
 					await settings.set(JSON.parse(localStorage.getItem('settings') ?? '{}'));
 					await settings.set(JSON.parse(localStorage.getItem('settings') ?? '{}'));
 				}
 				}
 
 
-				await settings.set({
-					...$settings,
-					system: chatContent.system ?? $settings.system,
-					params: chatContent.options ?? $settings.params
-				});
+				params = chatContent?.params ?? {};
 
 
 				autoScroll = true;
 				autoScroll = true;
 				await tick();
 				await tick();
@@ -507,9 +527,7 @@
 					title: $i18n.t('New Chat'),
 					title: $i18n.t('New Chat'),
 					models: selectedModels,
 					models: selectedModels,
 					system: $settings.system ?? undefined,
 					system: $settings.system ?? undefined,
-					options: {
-						...($settings.params ?? {})
-					},
+					params: params,
 					messages: messages,
 					messages: messages,
 					history: history,
 					history: history,
 					tags: [],
 					tags: [],
@@ -607,11 +625,11 @@
 		scrollToBottom();
 		scrollToBottom();
 
 
 		const messagesBody = [
 		const messagesBody = [
-			$settings.system || (responseMessage?.userContext ?? null)
+			params?.system || $settings.system || (responseMessage?.userContext ?? null)
 				? {
 				? {
 						role: 'system',
 						role: 'system',
 						content: `${promptTemplate(
 						content: `${promptTemplate(
-							$settings?.system ?? '',
+							params?.system ?? $settings?.system ?? '',
 							$user.name,
 							$user.name,
 							$settings?.userLocation
 							$settings?.userLocation
 								? await getAndUpdateUserLocation(localStorage.token)
 								? await getAndUpdateUserLocation(localStorage.token)
@@ -696,15 +714,16 @@
 			model: model.id,
 			model: model.id,
 			messages: messagesBody,
 			messages: messagesBody,
 			options: {
 			options: {
-				...($settings.params ?? {}),
+				...(params ?? $settings.params ?? {}),
 				stop:
 				stop:
-					$settings?.params?.stop ?? undefined
-						? $settings.params.stop.map((str) =>
+					params?.stop ?? $settings?.params?.stop ?? undefined
+						? (params?.stop ?? $settings.params.stop).map((str) =>
 								decodeURIComponent(JSON.parse('"' + str.replace(/\"/g, '\\"') + '"'))
 								decodeURIComponent(JSON.parse('"' + str.replace(/\"/g, '\\"') + '"'))
 						  )
 						  )
 						: undefined,
 						: undefined,
-				num_predict: $settings?.params?.max_tokens ?? undefined,
-				repeat_penalty: $settings?.params?.frequency_penalty ?? undefined
+				num_predict: params?.max_tokens ?? $settings?.params?.max_tokens ?? undefined,
+				repeat_penalty:
+					params?.frequency_penalty ?? $settings?.params?.frequency_penalty ?? undefined
 			},
 			},
 			format: $settings.requestFormat ?? undefined,
 			format: $settings.requestFormat ?? undefined,
 			keep_alive: $settings.keepAlive ?? undefined,
 			keep_alive: $settings.keepAlive ?? undefined,
@@ -840,7 +859,8 @@
 					chat = await updateChatById(localStorage.token, _chatId, {
 					chat = await updateChatById(localStorage.token, _chatId, {
 						messages: messages,
 						messages: messages,
 						history: history,
 						history: history,
-						models: selectedModels
+						models: selectedModels,
+						params: params
 					});
 					});
 					await chats.set(await getChatList(localStorage.token));
 					await chats.set(await getChatList(localStorage.token));
 				}
 				}
@@ -950,11 +970,11 @@
 							  }
 							  }
 							: undefined,
 							: undefined,
 					messages: [
 					messages: [
-						$settings.system || (responseMessage?.userContext ?? null)
+						params?.system || $settings.system || (responseMessage?.userContext ?? null)
 							? {
 							? {
 									role: 'system',
 									role: 'system',
 									content: `${promptTemplate(
 									content: `${promptTemplate(
-										$settings?.system ?? '',
+										params?.system ?? $settings?.system ?? '',
 										$user.name,
 										$user.name,
 										$settings?.userLocation
 										$settings?.userLocation
 											? await getAndUpdateUserLocation(localStorage.token)
 											? await getAndUpdateUserLocation(localStorage.token)
@@ -999,17 +1019,18 @@
 												: message?.raContent ?? message.content
 												: message?.raContent ?? message.content
 								  })
 								  })
 						})),
 						})),
-					seed: $settings?.params?.seed ?? undefined,
+					seed: params?.seed ?? $settings?.params?.seed ?? undefined,
 					stop:
 					stop:
-						$settings?.params?.stop ?? undefined
-							? $settings.params.stop.map((str) =>
+						params?.stop ?? $settings?.params?.stop ?? undefined
+							? (params?.stop ?? $settings.params.stop).map((str) =>
 									decodeURIComponent(JSON.parse('"' + str.replace(/\"/g, '\\"') + '"'))
 									decodeURIComponent(JSON.parse('"' + str.replace(/\"/g, '\\"') + '"'))
 							  )
 							  )
 							: undefined,
 							: undefined,
-					temperature: $settings?.params?.temperature ?? undefined,
-					top_p: $settings?.params?.top_p ?? undefined,
-					frequency_penalty: $settings?.params?.frequency_penalty ?? undefined,
-					max_tokens: $settings?.params?.max_tokens ?? undefined,
+					temperature: params?.temperature ?? $settings?.params?.temperature ?? undefined,
+					top_p: params?.top_p ?? $settings?.params?.top_p ?? undefined,
+					frequency_penalty:
+						params?.frequency_penalty ?? $settings?.params?.frequency_penalty ?? undefined,
+					max_tokens: params?.max_tokens ?? $settings?.params?.max_tokens ?? undefined,
 					tool_ids: selectedToolIds.length > 0 ? selectedToolIds : undefined,
 					tool_ids: selectedToolIds.length > 0 ? selectedToolIds : undefined,
 					files: files.length > 0 ? files : undefined,
 					files: files.length > 0 ? files : undefined,
 					session_id: $socket?.id,
 					session_id: $socket?.id,
@@ -1115,7 +1136,8 @@
 						chat = await updateChatById(localStorage.token, _chatId, {
 						chat = await updateChatById(localStorage.token, _chatId, {
 							models: selectedModels,
 							models: selectedModels,
 							messages: messages,
 							messages: messages,
-							history: history
+							history: history,
+							params: params
 						});
 						});
 						await chats.set(await getChatList(localStorage.token));
 						await chats.set(await getChatList(localStorage.token));
 					}
 					}
@@ -1382,6 +1404,18 @@
 
 
 <audio id="audioElement" src="" style="display: none;" />
 <audio id="audioElement" src="" style="display: none;" />
 
 
+<EventConfirmDialog
+	bind:show={showEventConfirmation}
+	title={eventConfirmationTitle}
+	message={eventConfirmationMessage}
+	on:confirm={(e) => {
+		eventCallback(true);
+	}}
+	on:cancel={() => {
+		eventCallback(false);
+	}}
+/>
+
 {#if $showCallOverlay}
 {#if $showCallOverlay}
 	<CallOverlay
 	<CallOverlay
 		{submitPrompt}
 		{submitPrompt}
@@ -1416,6 +1450,7 @@
 			{title}
 			{title}
 			bind:selectedModels
 			bind:selectedModels
 			bind:showModelSelector
 			bind:showModelSelector
+			bind:showControls
 			shareEnabled={messages.length > 0}
 			shareEnabled={messages.length > 0}
 			{chat}
 			{chat}
 			{initNewChat}
 			{initNewChat}
@@ -1425,7 +1460,7 @@
 			<div
 			<div
 				class="absolute top-[4.25rem] w-full {$showSidebar
 				class="absolute top-[4.25rem] w-full {$showSidebar
 					? 'md:max-w-[calc(100%-260px)]'
 					? 'md:max-w-[calc(100%-260px)]'
-					: ''} z-20"
+					: ''} {showControls ? 'lg:pr-[24rem]' : ''} z-20"
 			>
 			>
 				<div class=" flex flex-col gap-1 w-full">
 				<div class=" flex flex-col gap-1 w-full">
 					{#each $banners.filter( (b) => (b.dismissible ? !JSON.parse(localStorage.getItem('dismissedBannerIds') ?? '[]').includes(b.id) : true) ) as banner}
 					{#each $banners.filter( (b) => (b.dismissible ? !JSON.parse(localStorage.getItem('dismissedBannerIds') ?? '[]').includes(b.id) : true) ) as banner}
@@ -1452,7 +1487,9 @@
 
 
 		<div class="flex flex-col flex-auto z-10">
 		<div class="flex flex-col flex-auto z-10">
 			<div
 			<div
-				class=" pb-2.5 flex flex-col justify-between w-full flex-auto overflow-auto h-0 max-w-full z-10"
+				class=" pb-2.5 flex flex-col justify-between w-full flex-auto overflow-auto h-0 max-w-full z-10 scrollbar-hidden {showControls
+					? 'lg:pr-[24rem]'
+					: ''}"
 				id="messages-container"
 				id="messages-container"
 				bind:this={messagesContainerElement}
 				bind:this={messagesContainerElement}
 				on:scroll={(e) => {
 				on:scroll={(e) => {
@@ -1477,26 +1514,31 @@
 					/>
 					/>
 				</div>
 				</div>
 			</div>
 			</div>
-			<MessageInput
-				bind:files
-				bind:prompt
-				bind:autoScroll
-				bind:selectedToolIds
-				bind:webSearchEnabled
-				bind:atSelectedModel
-				availableToolIds={selectedModelIds.reduce((a, e, i, arr) => {
-					const model = $models.find((m) => m.id === e);
-					if (model?.info?.meta?.toolIds ?? false) {
-						return [...new Set([...a, ...model.info.meta.toolIds])];
-					}
-					return a;
-				}, [])}
-				transparentBackground={$settings?.backgroundImageUrl ?? false}
-				{selectedModels}
-				{messages}
-				{submitPrompt}
-				{stopResponse}
-			/>
+
+			<div class={showControls ? 'lg:pr-[24rem]' : ''}>
+				<MessageInput
+					bind:files
+					bind:prompt
+					bind:autoScroll
+					bind:selectedToolIds
+					bind:webSearchEnabled
+					bind:atSelectedModel
+					availableToolIds={selectedModelIds.reduce((a, e, i, arr) => {
+						const model = $models.find((m) => m.id === e);
+						if (model?.info?.meta?.toolIds ?? false) {
+							return [...new Set([...a, ...model.info.meta.toolIds])];
+						}
+						return a;
+					}, [])}
+					transparentBackground={$settings?.backgroundImageUrl ?? false}
+					{selectedModels}
+					{messages}
+					{submitPrompt}
+					{stopResponse}
+				/>
+			</div>
 		</div>
 		</div>
+
+		<ChatControls bind:show={showControls} bind:params />
 	</div>
 	</div>
 {/if}
 {/if}

+ 63 - 0
src/lib/components/chat/ChatControls.svelte

@@ -0,0 +1,63 @@
+<script lang="ts">
+	import { slide } from 'svelte/transition';
+	import Modal from '../common/Modal.svelte';
+	import Controls from './Controls/Controls.svelte';
+	import { onMount } from 'svelte';
+
+	export let show = false;
+
+	export let chatId = null;
+	export let params = {};
+
+	let largeScreen = false;
+	onMount(() => {
+		// listen to resize 1024px
+		const mediaQuery = window.matchMedia('(min-width: 1024px)');
+
+		const handleMediaQuery = (e) => {
+			if (e.matches) {
+				largeScreen = true;
+			} else {
+				largeScreen = false;
+			}
+		};
+
+		mediaQuery.addEventListener('change', handleMediaQuery);
+
+		handleMediaQuery(mediaQuery);
+
+		return () => {
+			mediaQuery.removeEventListener('change', handleMediaQuery);
+		};
+	});
+</script>
+
+{#if largeScreen}
+	{#if show}
+		<div class=" absolute bottom-0 right-0 z-20 h-full pointer-events-none">
+			<div class="pr-4 pt-14 pb-8 w-[24rem] h-full" in:slide={{ duration: 200, axis: 'x' }}>
+				<div
+					class="w-full h-full px-5 py-4 bg-white dark:shadow-lg dark:bg-gray-850 border border-gray-50 dark:border-gray-800 rounded-xl z-50 pointer-events-auto overflow-y-auto scrollbar-hidden"
+				>
+					<Controls
+						on:close={() => {
+							show = false;
+						}}
+						bind:params
+					/>
+				</div>
+			</div>
+		</div>
+	{/if}
+{:else}
+	<Modal bind:show>
+		<div class="  px-6 py-4 h-full">
+			<Controls
+				on:close={() => {
+					show = false;
+				}}
+				bind:params
+			/>
+		</div>
+	</Modal>
+{/if}

+ 49 - 0
src/lib/components/chat/Controls/Controls.svelte

@@ -0,0 +1,49 @@
+<script>
+	import { createEventDispatcher, getContext } from 'svelte';
+	const dispatch = createEventDispatcher();
+	const i18n = getContext('i18n');
+
+	import XMark from '$lib/components/icons/XMark.svelte';
+	import AdvancedParams from '../Settings/Advanced/AdvancedParams.svelte';
+
+	export let params = {};
+</script>
+
+<div class=" dark:text-white">
+	<div class=" flex justify-between dark:text-gray-100 mb-2">
+		<div class=" text-lg font-medium self-center font-primary">{$i18n.t('Chat Controls')}</div>
+		<button
+			class="self-center"
+			on:click={() => {
+				dispatch('close');
+			}}
+		>
+			<XMark className="size-4" />
+		</button>
+	</div>
+
+	<div class=" dark:text-gray-200 text-sm font-primary">
+		<div>
+			<div class="mb-1.5 font-medium">System Prompt</div>
+
+			<div>
+				<textarea
+					bind:value={params.system}
+					class="w-full rounded-lg px-4 py-3 text-sm dark:text-gray-300 dark:bg-gray-850 border border-gray-100 dark:border-gray-800 outline-none resize-none"
+					rows="3"
+					placeholder="Enter system prompt"
+				/>
+			</div>
+		</div>
+
+		<hr class="my-2 border-gray-100 dark:border-gray-800" />
+
+		<div>
+			<div class="mb-1.5 font-medium">Advanced Params</div>
+
+			<div>
+				<AdvancedParams bind:params />
+			</div>
+		</div>
+	</div>
+</div>

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

@@ -316,7 +316,7 @@
 	</div>
 	</div>
 {/if}
 {/if}
 
 
-<div class="w-full">
+<div class="w-full font-primary">
 	<div class=" -mb-0.5 mx-auto inset-x-0 bg-transparent flex justify-center">
 	<div class=" -mb-0.5 mx-auto inset-x-0 bg-transparent flex justify-center">
 		<div class="flex flex-col max-w-6xl px-2.5 md:px-6 w-full">
 		<div class="flex flex-col max-w-6xl px-2.5 md:px-6 w-full">
 			<div class="relative">
 			<div class="relative">

+ 6 - 4
src/lib/components/chat/MessageInput/CallOverlay.svelte

@@ -662,10 +662,11 @@
 									: rmsLevel * 100 > 1
 									: rmsLevel * 100 > 1
 									? 'size-14'
 									? 'size-14'
 									: 'size-12'}  transition-all rounded-full {(model?.info?.meta
 									: 'size-12'}  transition-all rounded-full {(model?.info?.meta
-									?.profile_image_url ?? '/favicon.png') !== '/favicon.png'
+									?.profile_image_url ?? '/static/favicon.png') !== '/static/favicon.png'
 									? ' bg-cover bg-center bg-no-repeat'
 									? ' bg-cover bg-center bg-no-repeat'
 									: 'bg-black dark:bg-white'}  bg-black dark:bg-white"
 									: 'bg-black dark:bg-white'}  bg-black dark:bg-white"
-								style={(model?.info?.meta?.profile_image_url ?? '/favicon.png') !== '/favicon.png'
+								style={(model?.info?.meta?.profile_image_url ?? '/static/favicon.png') !==
+								'/static/favicon.png'
 									? `background-image: url('${model?.info?.meta?.profile_image_url}');`
 									? `background-image: url('${model?.info?.meta?.profile_image_url}');`
 									: ''}
 									: ''}
 							/>
 							/>
@@ -743,10 +744,11 @@
 										: rmsLevel * 100 > 1
 										: rmsLevel * 100 > 1
 										? 'size-[11.5rem]'
 										? 'size-[11.5rem]'
 										: 'size-44'}  transition-all rounded-full {(model?.info?.meta
 										: 'size-44'}  transition-all rounded-full {(model?.info?.meta
-										?.profile_image_url ?? '/favicon.png') !== '/favicon.png'
+										?.profile_image_url ?? '/static/favicon.png') !== '/static/favicon.png'
 										? ' bg-cover bg-center bg-no-repeat'
 										? ' bg-cover bg-center bg-no-repeat'
 										: 'bg-black dark:bg-white'} "
 										: 'bg-black dark:bg-white'} "
-									style={(model?.info?.meta?.profile_image_url ?? '/favicon.png') !== '/favicon.png'
+									style={(model?.info?.meta?.profile_image_url ?? '/static/favicon.png') !==
+									'/static/favicon.png'
 										? `background-image: url('${model?.info?.meta?.profile_image_url}');`
 										? `background-image: url('${model?.info?.meta?.profile_image_url}');`
 										: ''}
 										: ''}
 								/>
 								/>

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

@@ -250,7 +250,8 @@ __builtins__.input = input`);
 			stderr ||
 			stderr ||
 			result) &&
 			result) &&
 			'border-bottom-left-radius: 0px; border-bottom-right-radius: 0px;'}"><code
 			'border-bottom-left-radius: 0px; border-bottom-right-radius: 0px;'}"><code
-			class="language-{lang} rounded-t-none whitespace-pre">{@html highlightedCode || code}</code
+			class="language-{lang} rounded-t-none whitespace-pre"
+			>{#if highlightedCode}{@html highlightedCode}{:else}{code}{/if}</code
 		></pre>
 		></pre>
 
 
 	<div
 	<div

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

@@ -1,3 +1,3 @@
-<div class=" self-center font-bold mb-0.5 line-clamp-1 contents">
+<div class=" self-center font-semibold mb-0.5 line-clamp-1 contents">
 	<slot />
 	<slot />
 </div>
 </div>

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

@@ -65,7 +65,7 @@
 		</div>
 		</div>
 
 
 		<div
 		<div
-			class=" mt-2 mb-4 text-3xl text-gray-800 dark:text-gray-100 font-semibold text-left flex items-center gap-4"
+			class=" mt-2 mb-4 text-3xl text-gray-800 dark:text-gray-100 font-semibold text-left flex items-center gap-4 font-primary"
 		>
 		>
 			<div>
 			<div>
 				<div class=" capitalize line-clamp-1" in:fade={{ duration: 200 }}>
 				<div class=" capitalize line-clamp-1" in:fade={{ duration: 200 }}>
@@ -102,7 +102,7 @@
 							</div>
 							</div>
 						{/if}
 						{/if}
 					{:else}
 					{:else}
-						<div class=" font-medium text-gray-400 dark:text-gray-500 line-clamp-1">
+						<div class=" font-medium text-gray-400 dark:text-gray-500 line-clamp-1 font-p">
 							{$i18n.t('How can I help you today?')}
 							{$i18n.t('How can I help you today?')}
 						</div>
 						</div>
 					{/if}
 					{/if}
@@ -110,7 +110,7 @@
 			</div>
 			</div>
 		</div>
 		</div>
 
 
-		<div class=" w-full" in:fade={{ duration: 200, delay: 300 }}>
+		<div class=" w-full font-primary" in:fade={{ duration: 200, delay: 300 }}>
 			<Suggestions
 			<Suggestions
 				suggestionPrompts={models[selectedModelIdx]?.info?.meta?.suggestion_prompts ??
 				suggestionPrompts={models[selectedModelIdx]?.info?.meta?.suggestion_prompts ??
 					$config.default_prompt_suggestions}
 					$config.default_prompt_suggestions}

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

@@ -152,7 +152,10 @@
 			}
 			}
 			tooltipInstance = tippy(`#info-${message.id}`, {
 			tooltipInstance = tippy(`#info-${message.id}`, {
 				content: `<span class="text-xs" id="tooltip-${message.id}">${tooltipContent}</span>`,
 				content: `<span class="text-xs" id="tooltip-${message.id}">${tooltipContent}</span>`,
-				allowHTML: true
+				allowHTML: true,
+				theme: 'dark',
+				arrow: false,
+				offset: [0, 4]
 			});
 			});
 		}
 		}
 	};
 	};

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

@@ -2,32 +2,25 @@
 	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 MagnifyingGlass from '$lib/components/icons/MagnifyingGlass.svelte';
-	import { Collapsible } from 'bits-ui';
-	import { slide } from 'svelte/transition';
+	import Collapsible from '$lib/components/common/Collapsible.svelte';
 
 
 	export let status = { urls: [], query: '' };
 	export let status = { urls: [], query: '' };
 	let state = false;
 	let state = false;
 </script>
 </script>
 
 
-<Collapsible.Root class="w-full space-y-1" bind:open={state}>
-	<Collapsible.Trigger>
-		<div
-			class="flex items-center gap-2 text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition"
-		>
-			<slot />
-
-			{#if state}
-				<ChevronUp strokeWidth="3.5" className="size-3.5 " />
-			{:else}
-				<ChevronDown strokeWidth="3.5" className="size-3.5 " />
-			{/if}
-		</div>
-	</Collapsible.Trigger>
-
-	<Collapsible.Content
-		class=" text-sm border border-gray-300/30 dark:border-gray-700/50 rounded-xl"
-		transition={slide}
+<Collapsible bind:open={state} className="w-full space-y-1">
+	<div
+		class="flex items-center gap-2 text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition"
 	>
 	>
+		<slot />
+
+		{#if state}
+			<ChevronUp strokeWidth="3.5" className="size-3.5 " />
+		{:else}
+			<ChevronDown strokeWidth="3.5" className="size-3.5 " />
+		{/if}
+	</div>
+	<div class="text-sm border border-gray-300/30 dark:border-gray-700/50 rounded-xl" slot="content">
 		{#if status?.query}
 		{#if status?.query}
 			<a
 			<a
 				href="https://www.google.com/search?q={status.query}"
 				href="https://www.google.com/search?q={status.query}"
@@ -93,5 +86,5 @@
 				</div>
 				</div>
 			</a>
 			</a>
 		{/each}
 		{/each}
-	</Collapsible.Content>
-</Collapsible.Root>
+	</div>
+</Collapsible>

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

@@ -34,7 +34,7 @@
 	}
 	}
 </script>
 </script>
 
 
-<div class="flex flex-col w-full items-center md:items-start">
+<div class="flex flex-col w-full items-start">
 	{#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">
@@ -103,7 +103,7 @@
 </div>
 </div>
 
 
 {#if showSetDefault && !$mobile}
 {#if showSetDefault && !$mobile}
-	<div class="text-left mt-0.5 ml-1 text-[0.7rem] text-gray-500">
+	<div class="text-left mt-0.5 ml-1 text-[0.7rem] text-gray-500 font-primary">
 		<button on:click={saveDefaultModel}> {$i18n.t('Set as default')}</button>
 		<button on:click={saveDefaultModel}> {$i18n.t('Set as default')}</button>
 	</div>
 	</div>
 {/if}
 {/if}

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

@@ -206,7 +206,7 @@
 	}}
 	}}
 	closeFocus={false}
 	closeFocus={false}
 >
 >
-	<DropdownMenu.Trigger class="relative w-full" aria-label={placeholder}>
+	<DropdownMenu.Trigger class="relative w-full font-primary" aria-label={placeholder}>
 		<div
 		<div
 			class="flex w-full text-left px-0.5 outline-none bg-transparent truncate text-lg font-semibold placeholder-gray-400 focus:outline-none"
 			class="flex w-full text-left px-0.5 outline-none bg-transparent truncate text-lg font-semibold placeholder-gray-400 focus:outline-none"
 		>
 		>
@@ -222,7 +222,7 @@
 	<DropdownMenu.Content
 	<DropdownMenu.Content
 		class=" z-40 {$mobile
 		class=" z-40 {$mobile
 			? `w-full`
 			? `w-full`
-			: `${className}`} max-w-[calc(100vw-1rem)] justify-start rounded-xl  bg-white dark:bg-gray-850 dark:text-white shadow-lg border border-gray-300/30 dark:border-gray-850/50  outline-none "
+			: `${className}`} max-w-[calc(100vw-1rem)] justify-start rounded-xl  bg-white dark:bg-gray-850 dark:text-white shadow-lg border border-gray-300/30 dark:border-gray-700/40  outline-none"
 		transition={flyAndScale}
 		transition={flyAndScale}
 		side={$mobile ? 'bottom' : 'bottom-start'}
 		side={$mobile ? 'bottom' : 'bottom-start'}
 		sideOffset={4}
 		sideOffset={4}
@@ -260,7 +260,7 @@
 								<div class="flex gap-0.5 self-start h-full mb-0.5 -translate-x-1">
 								<div class="flex gap-0.5 self-start h-full mb-0.5 -translate-x-1">
 									{#each item.model?.info?.meta.tags as tag}
 									{#each item.model?.info?.meta.tags as tag}
 										<div
 										<div
-											class=" text-xs font-black px-1 rounded uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
+											class=" text-xs font-bold px-1 rounded uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
 										>
 										>
 											{tag.name}
 											{tag.name}
 										</div>
 										</div>
@@ -299,7 +299,7 @@
 									<div class="flex gap-0.5 self-center items-center h-full translate-y-[0.5px]">
 									<div class="flex gap-0.5 self-center items-center h-full translate-y-[0.5px]">
 										{#each item.model?.info?.meta.tags as tag}
 										{#each item.model?.info?.meta.tags as tag}
 											<div
 											<div
-												class=" text-xs font-black px-1 rounded uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
+												class=" text-xs font-bold px-1 rounded uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
 											>
 											>
 												{tag.name}
 												{tag.name}
 											</div>
 											</div>

+ 19 - 19
src/lib/components/chat/Settings/Advanced/AdvancedParams.svelte

@@ -44,7 +44,7 @@
 			<div class=" self-center text-xs font-medium">{$i18n.t('Seed')}</div>
 			<div class=" self-center text-xs font-medium">{$i18n.t('Seed')}</div>
 
 
 			<button
 			<button
-				class="p-1 px-3 text-xs flex rounded transition"
+				class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
 				type="button"
 				type="button"
 				on:click={() => {
 				on:click={() => {
 					params.seed = (params?.seed ?? null) === null ? 0 : null;
 					params.seed = (params?.seed ?? null) === null ? 0 : null;
@@ -79,7 +79,7 @@
 			<div class=" self-center text-xs font-medium">{$i18n.t('Stop Sequence')}</div>
 			<div class=" self-center text-xs font-medium">{$i18n.t('Stop Sequence')}</div>
 
 
 			<button
 			<button
-				class="p-1 px-3 text-xs flex rounded transition"
+				class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
 				type="button"
 				type="button"
 				on:click={() => {
 				on:click={() => {
 					params.stop = (params?.stop ?? null) === null ? '' : null;
 					params.stop = (params?.stop ?? null) === null ? '' : null;
@@ -113,7 +113,7 @@
 			<div class=" self-center text-xs font-medium">{$i18n.t('Temperature')}</div>
 			<div class=" self-center text-xs font-medium">{$i18n.t('Temperature')}</div>
 
 
 			<button
 			<button
-				class="p-1 px-3 text-xs flex rounded transition"
+				class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
 				type="button"
 				type="button"
 				on:click={() => {
 				on:click={() => {
 					params.temperature = (params?.temperature ?? null) === null ? 0.8 : null;
 					params.temperature = (params?.temperature ?? null) === null ? 0.8 : null;
@@ -159,7 +159,7 @@
 			<div class=" self-center text-xs font-medium">{$i18n.t('Mirostat')}</div>
 			<div class=" self-center text-xs font-medium">{$i18n.t('Mirostat')}</div>
 
 
 			<button
 			<button
-				class="p-1 px-3 text-xs flex rounded transition"
+				class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
 				type="button"
 				type="button"
 				on:click={() => {
 				on:click={() => {
 					params.mirostat = (params?.mirostat ?? null) === null ? 0 : null;
 					params.mirostat = (params?.mirostat ?? null) === null ? 0 : null;
@@ -205,7 +205,7 @@
 			<div class=" self-center text-xs font-medium">{$i18n.t('Mirostat Eta')}</div>
 			<div class=" self-center text-xs font-medium">{$i18n.t('Mirostat Eta')}</div>
 
 
 			<button
 			<button
-				class="p-1 px-3 text-xs flex rounded transition"
+				class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
 				type="button"
 				type="button"
 				on:click={() => {
 				on:click={() => {
 					params.mirostat_eta = (params?.mirostat_eta ?? null) === null ? 0.1 : null;
 					params.mirostat_eta = (params?.mirostat_eta ?? null) === null ? 0.1 : null;
@@ -251,7 +251,7 @@
 			<div class=" self-center text-xs font-medium">{$i18n.t('Mirostat Tau')}</div>
 			<div class=" self-center text-xs font-medium">{$i18n.t('Mirostat Tau')}</div>
 
 
 			<button
 			<button
-				class="p-1 px-3 text-xs flex rounded transition"
+				class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
 				type="button"
 				type="button"
 				on:click={() => {
 				on:click={() => {
 					params.mirostat_tau = (params?.mirostat_tau ?? null) === null ? 5.0 : null;
 					params.mirostat_tau = (params?.mirostat_tau ?? null) === null ? 5.0 : null;
@@ -297,7 +297,7 @@
 			<div class=" self-center text-xs font-medium">{$i18n.t('Top K')}</div>
 			<div class=" self-center text-xs font-medium">{$i18n.t('Top K')}</div>
 
 
 			<button
 			<button
-				class="p-1 px-3 text-xs flex rounded transition"
+				class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
 				type="button"
 				type="button"
 				on:click={() => {
 				on:click={() => {
 					params.top_k = (params?.top_k ?? null) === null ? 40 : null;
 					params.top_k = (params?.top_k ?? null) === null ? 40 : null;
@@ -343,7 +343,7 @@
 			<div class=" self-center text-xs font-medium">{$i18n.t('Top P')}</div>
 			<div class=" self-center text-xs font-medium">{$i18n.t('Top P')}</div>
 
 
 			<button
 			<button
-				class="p-1 px-3 text-xs flex rounded transition"
+				class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
 				type="button"
 				type="button"
 				on:click={() => {
 				on:click={() => {
 					params.top_p = (params?.top_p ?? null) === null ? 0.9 : null;
 					params.top_p = (params?.top_p ?? null) === null ? 0.9 : null;
@@ -389,7 +389,7 @@
 			<div class=" self-center text-xs font-medium">{$i18n.t('Frequency Penalty')}</div>
 			<div class=" self-center text-xs font-medium">{$i18n.t('Frequency Penalty')}</div>
 
 
 			<button
 			<button
-				class="p-1 px-3 text-xs flex rounded transition"
+				class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
 				type="button"
 				type="button"
 				on:click={() => {
 				on:click={() => {
 					params.frequency_penalty = (params?.frequency_penalty ?? null) === null ? 1.1 : null;
 					params.frequency_penalty = (params?.frequency_penalty ?? null) === null ? 1.1 : null;
@@ -435,7 +435,7 @@
 			<div class=" self-center text-xs font-medium">{$i18n.t('Repeat Last N')}</div>
 			<div class=" self-center text-xs font-medium">{$i18n.t('Repeat Last N')}</div>
 
 
 			<button
 			<button
-				class="p-1 px-3 text-xs flex rounded transition"
+				class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
 				type="button"
 				type="button"
 				on:click={() => {
 				on:click={() => {
 					params.repeat_last_n = (params?.repeat_last_n ?? null) === null ? 64 : null;
 					params.repeat_last_n = (params?.repeat_last_n ?? null) === null ? 64 : null;
@@ -481,7 +481,7 @@
 			<div class=" self-center text-xs font-medium">{$i18n.t('Tfs Z')}</div>
 			<div class=" self-center text-xs font-medium">{$i18n.t('Tfs Z')}</div>
 
 
 			<button
 			<button
-				class="p-1 px-3 text-xs flex rounded transition"
+				class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
 				type="button"
 				type="button"
 				on:click={() => {
 				on:click={() => {
 					params.tfs_z = (params?.tfs_z ?? null) === null ? 1 : null;
 					params.tfs_z = (params?.tfs_z ?? null) === null ? 1 : null;
@@ -527,7 +527,7 @@
 			<div class=" self-center text-xs font-medium">{$i18n.t('Context Length')}</div>
 			<div class=" self-center text-xs font-medium">{$i18n.t('Context Length')}</div>
 
 
 			<button
 			<button
-				class="p-1 px-3 text-xs flex rounded transition"
+				class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
 				type="button"
 				type="button"
 				on:click={() => {
 				on:click={() => {
 					params.num_ctx = (params?.num_ctx ?? null) === null ? 2048 : null;
 					params.num_ctx = (params?.num_ctx ?? null) === null ? 2048 : null;
@@ -572,7 +572,7 @@
 			<div class=" self-center text-xs font-medium">{$i18n.t('Batch Size (num_batch)')}</div>
 			<div class=" self-center text-xs font-medium">{$i18n.t('Batch Size (num_batch)')}</div>
 
 
 			<button
 			<button
-				class="p-1 px-3 text-xs flex rounded transition"
+				class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
 				type="button"
 				type="button"
 				on:click={() => {
 				on:click={() => {
 					params.num_batch = (params?.num_batch ?? null) === null ? 512 : null;
 					params.num_batch = (params?.num_batch ?? null) === null ? 512 : null;
@@ -619,7 +619,7 @@
 			</div>
 			</div>
 
 
 			<button
 			<button
-				class="p-1 px-3 text-xs flex rounded transition"
+				class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
 				type="button"
 				type="button"
 				on:click={() => {
 				on:click={() => {
 					params.num_keep = (params?.num_keep ?? null) === null ? 24 : null;
 					params.num_keep = (params?.num_keep ?? null) === null ? 24 : null;
@@ -664,7 +664,7 @@
 			<div class=" self-center text-xs font-medium">{$i18n.t('Max Tokens (num_predict)')}</div>
 			<div class=" self-center text-xs font-medium">{$i18n.t('Max Tokens (num_predict)')}</div>
 
 
 			<button
 			<button
-				class="p-1 px-3 text-xs flex rounded transition"
+				class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
 				type="button"
 				type="button"
 				on:click={() => {
 				on:click={() => {
 					params.max_tokens = (params?.max_tokens ?? null) === null ? 128 : null;
 					params.max_tokens = (params?.max_tokens ?? null) === null ? 128 : null;
@@ -711,7 +711,7 @@
 				<div class=" self-center text-xs font-medium">{$i18n.t('use_mmap (Ollama)')}</div>
 				<div class=" self-center text-xs font-medium">{$i18n.t('use_mmap (Ollama)')}</div>
 
 
 				<button
 				<button
-					class="p-1 px-3 text-xs flex rounded transition"
+					class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
 					type="button"
 					type="button"
 					on:click={() => {
 					on:click={() => {
 						params.use_mmap = (params?.use_mmap ?? null) === null ? true : null;
 						params.use_mmap = (params?.use_mmap ?? null) === null ? true : null;
@@ -731,7 +731,7 @@
 				<div class=" self-center text-xs font-medium">{$i18n.t('use_mlock (Ollama)')}</div>
 				<div class=" self-center text-xs font-medium">{$i18n.t('use_mlock (Ollama)')}</div>
 
 
 				<button
 				<button
-					class="p-1 px-3 text-xs flex rounded transition"
+					class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
 					type="button"
 					type="button"
 					on:click={() => {
 					on:click={() => {
 						params.use_mlock = (params?.use_mlock ?? null) === null ? true : null;
 						params.use_mlock = (params?.use_mlock ?? null) === null ? true : null;
@@ -751,7 +751,7 @@
 				<div class=" self-center text-xs font-medium">{$i18n.t('num_thread (Ollama)')}</div>
 				<div class=" self-center text-xs font-medium">{$i18n.t('num_thread (Ollama)')}</div>
 
 
 				<button
 				<button
-					class="p-1 px-3 text-xs flex rounded transition"
+					class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
 					type="button"
 					type="button"
 					on:click={() => {
 					on:click={() => {
 						params.num_thread = (params?.num_thread ?? null) === null ? 2 : null;
 						params.num_thread = (params?.num_thread ?? null) === null ? 2 : null;
@@ -797,7 +797,7 @@
 				<div class=" self-center text-xs font-medium">{$i18n.t('Template')}</div>
 				<div class=" self-center text-xs font-medium">{$i18n.t('Template')}</div>
 
 
 				<button
 				<button
-					class="p-1 px-3 text-xs flex rounded transition"
+					class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
 					type="button"
 					type="button"
 					on:click={() => {
 					on:click={() => {
 						params.template = (params?.template ?? null) === null ? '' : null;
 						params.template = (params?.template ?? null) === null ? '' : null;

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

@@ -95,6 +95,8 @@
 		}
 		}
 
 
 		if (themeToApply === 'dark' && !_theme.includes('oled')) {
 		if (themeToApply === 'dark' && !_theme.includes('oled')) {
+			document.documentElement.style.setProperty('--color-gray-800', '#333');
+			document.documentElement.style.setProperty('--color-gray-850', '#262626');
 			document.documentElement.style.setProperty('--color-gray-900', '#171717');
 			document.documentElement.style.setProperty('--color-gray-900', '#171717');
 			document.documentElement.style.setProperty('--color-gray-950', '#0d0d0d');
 			document.documentElement.style.setProperty('--color-gray-950', '#0d0d0d');
 		}
 		}
@@ -118,6 +120,8 @@
 		theme.set(_theme);
 		theme.set(_theme);
 		localStorage.setItem('theme', _theme);
 		localStorage.setItem('theme', _theme);
 		if (_theme.includes('oled')) {
 		if (_theme.includes('oled')) {
+			document.documentElement.style.setProperty('--color-gray-800', '#101010');
+			document.documentElement.style.setProperty('--color-gray-850', '#050505');
 			document.documentElement.style.setProperty('--color-gray-900', '#000000');
 			document.documentElement.style.setProperty('--color-gray-900', '#000000');
 			document.documentElement.style.setProperty('--color-gray-950', '#000000');
 			document.documentElement.style.setProperty('--color-gray-950', '#000000');
 			document.documentElement.classList.add('dark');
 			document.documentElement.classList.add('dark');

+ 35 - 4
src/lib/components/chat/SettingsModal.svelte

@@ -1,9 +1,10 @@
 <script lang="ts">
 <script lang="ts">
-	import { getContext } from 'svelte';
+	import { getContext, tick } from 'svelte';
 	import { toast } from 'svelte-sonner';
 	import { toast } from 'svelte-sonner';
 	import { models, settings, user } from '$lib/stores';
 	import { models, settings, user } from '$lib/stores';
-
+	import { updateUserSettings } from '$lib/apis/users';
 	import { getModels as _getModels } from '$lib/apis';
 	import { getModels as _getModels } from '$lib/apis';
+	import { goto } from '$app/navigation';
 
 
 	import Modal from '../common/Modal.svelte';
 	import Modal from '../common/Modal.svelte';
 	import Account from './Settings/Account.svelte';
 	import Account from './Settings/Account.svelte';
@@ -14,8 +15,6 @@
 	import Chats from './Settings/Chats.svelte';
 	import Chats from './Settings/Chats.svelte';
 	import User from '../icons/User.svelte';
 	import User from '../icons/User.svelte';
 	import Personalization from './Settings/Personalization.svelte';
 	import Personalization from './Settings/Personalization.svelte';
-	import { updateUserSettings } from '$lib/apis/users';
-	import { goto } from '$app/navigation';
 	import Valves from './Settings/Valves.svelte';
 	import Valves from './Settings/Valves.svelte';
 
 
 	const i18n = getContext('i18n');
 	const i18n = getContext('i18n');
@@ -34,6 +33,37 @@
 	};
 	};
 
 
 	let selectedTab = 'general';
 	let selectedTab = 'general';
+
+	// Function to handle sideways scrolling
+	const scrollHandler = (event) => {
+		const settingsTabsContainer = document.getElementById('settings-tabs-container');
+		if (settingsTabsContainer) {
+			event.preventDefault(); // Prevent default vertical scrolling
+			settingsTabsContainer.scrollLeft += event.deltaY; // Scroll sideways
+		}
+	};
+
+	const addScrollListener = async () => {
+		await tick();
+		const settingsTabsContainer = document.getElementById('settings-tabs-container');
+		if (settingsTabsContainer) {
+			settingsTabsContainer.addEventListener('wheel', scrollHandler);
+		}
+	};
+
+	const removeScrollListener = async () => {
+		await tick();
+		const settingsTabsContainer = document.getElementById('settings-tabs-container');
+		if (settingsTabsContainer) {
+			settingsTabsContainer.removeEventListener('wheel', scrollHandler);
+		}
+	};
+
+	$: if (show) {
+		addScrollListener();
+	} else {
+		removeScrollListener();
+	}
 </script>
 </script>
 
 
 <Modal bind:show>
 <Modal bind:show>
@@ -61,6 +91,7 @@
 
 
 		<div class="flex flex-col md:flex-row w-full p-4 md:space-x-4">
 		<div class="flex flex-col md:flex-row w-full p-4 md:space-x-4">
 			<div
 			<div
+				id="settings-tabs-container"
 				class="tabs flex flex-row overflow-x-auto space-x-1 md:space-x-0 md:space-y-1 md:flex-col flex-1 md:flex-none md:w-40 dark:text-gray-200 text-xs text-left mb-3 md:mb-0"
 				class="tabs flex flex-row overflow-x-auto space-x-1 md:space-x-0 md:space-y-1 md:flex-col flex-1 md:flex-none md:w-40 dark:text-gray-200 text-xs text-left mb-3 md:mb-0"
 			>
 			>
 				<button
 				<button

+ 5 - 4
src/lib/components/common/Banner.svelte

@@ -45,7 +45,7 @@
 			<div class=" flex flex-col md:flex-row md:items-center flex-1 text-sm w-fit gap-1.5">
 			<div class=" flex flex-col md:flex-row md:items-center flex-1 text-sm w-fit gap-1.5">
 				<div class="flex justify-between self-start">
 				<div class="flex justify-between self-start">
 					<div
 					<div
-						class=" text-xs font-black {classNames[banner.type] ??
+						class=" text-xs font-bold {classNames[banner.type] ??
 							classNames['info']}  w-fit px-2 rounded uppercase line-clamp-1 mr-0.5"
 							classNames['info']}  w-fit px-2 rounded uppercase line-clamp-1 mr-0.5"
 					>
 					>
 						{banner.type}
 						{banner.type}
@@ -54,7 +54,7 @@
 					{#if banner.url}
 					{#if banner.url}
 						<div class="flex md:hidden group w-fit md:items-center">
 						<div class="flex md:hidden group w-fit md:items-center">
 							<a
 							<a
-								class="text-gray-700 dark:text-white text-xs font-bold underline"
+								class="text-gray-700 dark:text-white text-xs font-semibold underline"
 								href="/assets/files/whitepaper.pdf"
 								href="/assets/files/whitepaper.pdf"
 								target="_blank">Learn More</a
 								target="_blank">Learn More</a
 							>
 							>
@@ -88,7 +88,7 @@
 			{#if banner.url}
 			{#if banner.url}
 				<div class="hidden md:flex group w-fit md:items-center">
 				<div class="hidden md:flex group w-fit md:items-center">
 					<a
 					<a
-						class="text-gray-700 dark:text-white text-xs font-bold underline"
+						class="text-gray-700 dark:text-white text-xs font-semibold underline"
 						href="/"
 						href="/"
 						target="_blank">Learn More</a
 						target="_blank">Learn More</a
 					>
 					>
@@ -116,7 +116,8 @@
 						on:click={() => {
 						on:click={() => {
 							dismiss(banner.id);
 							dismiss(banner.id);
 						}}
 						}}
-						class=" -mt-[3px] ml-1.5 mr-1 text-gray-400 dark:hover:text-white h-1">&times;</button
+						class="  -mt-1 -mb-2 -translate-y-[1px] ml-1.5 mr-1 text-gray-400 dark:hover:text-white"
+						>&times;</button
 					>
 					>
 				{/if}
 				{/if}
 			</div>
 			</div>

+ 18 - 0
src/lib/components/common/Collapsible.svelte

@@ -0,0 +1,18 @@
+<script lang="ts">
+	import { slide } from 'svelte/transition';
+	import { quintOut } from 'svelte/easing';
+	export let open = false;
+	export let className = '';
+</script>
+
+<div class={className}>
+	<button on:click={() => (open = !open)}>
+		<slot />
+	</button>
+
+	{#if open}
+		<div transition:slide={{ duration: 300, easing: quintOut, axis: 'y' }}>
+			<slot name="content" />
+		</div>
+	{/if}
+</div>

+ 15 - 4
src/lib/components/common/ConfirmDialog.svelte

@@ -7,8 +7,8 @@
 
 
 	const dispatch = createEventDispatcher();
 	const dispatch = createEventDispatcher();
 
 
-	export let title = $i18n.t('Confirm your action');
-	export let message = $i18n.t('This action cannot be undone. Do you wish to continue?');
+	export let title = '';
+	export let message = '';
 
 
 	export let cancelLabel = $i18n.t('Cancel');
 	export let cancelLabel = $i18n.t('Cancel');
 	export let confirmLabel = $i18n.t('Confirm');
 	export let confirmLabel = $i18n.t('Confirm');
@@ -58,11 +58,21 @@
 			}}
 			}}
 		>
 		>
 			<div class="px-[1.75rem] py-6">
 			<div class="px-[1.75rem] py-6">
-				<div class=" text-lg font-semibold dark:text-gray-200 mb-2.5">{title}</div>
+				<div class=" text-lg font-semibold dark:text-gray-200 mb-2.5">
+					{#if title !== ''}
+						{title}
+					{:else}
+						{$i18n.t('Confirm your action')}
+					{/if}
+				</div>
 
 
 				<slot>
 				<slot>
 					<div class=" text-sm text-gray-500">
 					<div class=" text-sm text-gray-500">
-						{message}
+						{#if message !== ''}
+							{message}
+						{:else}
+							{$i18n.t('This action cannot be undone. Do you wish to continue?')}
+						{/if}
 					</div>
 					</div>
 				</slot>
 				</slot>
 
 
@@ -71,6 +81,7 @@
 						class="bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-white font-medium w-full py-2.5 rounded-lg transition"
 						class="bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-white font-medium w-full py-2.5 rounded-lg transition"
 						on:click={() => {
 						on:click={() => {
 							show = false;
 							show = false;
+							dispatch('cancel');
 						}}
 						}}
 						type="button"
 						type="button"
 					>
 					>

+ 1 - 1
src/lib/components/common/Modal.svelte

@@ -63,7 +63,7 @@
 		<div
 		<div
 			class=" m-auto rounded-2xl max-w-full {sizeToWidth(
 			class=" m-auto rounded-2xl max-w-full {sizeToWidth(
 				size
 				size
-			)} mx-2 bg-gray-50 dark:bg-gray-900 shadow-3xl"
+			)} mx-2 bg-gray-50 dark:bg-gray-900 shadow-3xl max-h-[100dvh] overflow-y-auto scrollbar-hidden"
 			in:flyAndScale
 			in:flyAndScale
 			on:mousedown={(e) => {
 			on:mousedown={(e) => {
 				e.stopPropagation();
 				e.stopPropagation();

+ 1 - 1
src/lib/components/common/Selector.svelte

@@ -48,7 +48,7 @@
 		<ChevronDown className="absolute end-2 top-1/2 -translate-y-[45%] size-3.5" strokeWidth="2.5" />
 		<ChevronDown className="absolute end-2 top-1/2 -translate-y-[45%] size-3.5" strokeWidth="2.5" />
 	</Select.Trigger>
 	</Select.Trigger>
 	<Select.Content
 	<Select.Content
-		class="w-full rounded-lg  bg-white dark:bg-gray-900 dark:text-white shadow-lg border border-gray-300/30 dark:border-gray-850/50  outline-none"
+		class="w-full rounded-lg  bg-white dark:bg-gray-900 dark:text-white shadow-lg border border-gray-300/30 dark:border-gray-700/40  outline-none"
 		transition={flyAndScale}
 		transition={flyAndScale}
 		sideOffset={4}
 		sideOffset={4}
 	>
 	>

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

@@ -4,8 +4,8 @@
 	export let readOnly = false;
 	export let readOnly = false;
 	export let outerClassName = 'flex flex-1';
 	export let outerClassName = 'flex flex-1';
 	export let inputClassName =
 	export let inputClassName =
-		'w-full rounded-l-lg py-2 pl-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none';
-	export let showButtonClassName = 'px-2 transition rounded-r-lg bg-white dark:bg-gray-850';
+		'w-full rounded-l-lg py-2 pl-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none';
+	export let showButtonClassName = 'px-2 transition rounded-r-lg bg-gray-50 dark:bg-gray-850';
 
 
 	let show = false;
 	let show = false;
 </script>
 </script>

+ 1 - 1
src/lib/components/common/Tags/TagInput.svelte

@@ -22,7 +22,7 @@
 	};
 	};
 </script>
 </script>
 
 
-<div class="flex {showTagInput ? 'flex-row-reverse' : ''}">
+<div class="px-0.5 flex {showTagInput ? 'flex-row-reverse' : ''}">
 	{#if showTagInput}
 	{#if showTagInput}
 		<div class="flex items-center">
 		<div class="flex items-center">
 			<input
 			<input

+ 6 - 1
src/lib/components/common/Tooltip.svelte

@@ -3,11 +3,13 @@
 	import { marked } from 'marked';
 	import { marked } from 'marked';
 
 
 	import tippy from 'tippy.js';
 	import tippy from 'tippy.js';
+	import { roundArrow } from 'tippy.js';
 
 
 	export let placement = 'top';
 	export let placement = 'top';
 	export let content = `I'm a tooltip!`;
 	export let content = `I'm a tooltip!`;
 	export let touch = true;
 	export let touch = true;
 	export let className = 'flex';
 	export let className = 'flex';
+	export let theme = '';
 
 
 	let tooltipElement;
 	let tooltipElement;
 	let tooltipInstance;
 	let tooltipInstance;
@@ -20,7 +22,10 @@
 				content: content,
 				content: content,
 				placement: placement,
 				placement: placement,
 				allowHTML: true,
 				allowHTML: true,
-				touch: touch
+				touch: touch,
+				...(theme !== '' ? { theme } : { theme: 'dark' }),
+				arrow: false,
+				offset: [0, 4]
 			});
 			});
 		}
 		}
 	} else if (tooltipInstance && content === '') {
 	} else if (tooltipInstance && content === '') {

+ 1 - 1
src/lib/components/documents/EditDocModal.svelte

@@ -109,7 +109,7 @@
 
 
 							<div class="flex flex-1">
 							<div class="flex flex-1">
 								<div
 								<div
-									class="bg-gray-200 dark:bg-gray-800 font-bold px-3 py-0.5 border border-r-0 dark:border-gray-800 rounded-l-xl flex items-center"
+									class="bg-gray-200 dark:bg-gray-800 font-semibold px-3 py-0.5 border border-r-0 dark:border-gray-800 rounded-l-xl flex items-center"
 								>
 								>
 									#
 									#
 								</div>
 								</div>

+ 17 - 0
src/lib/components/icons/AdjustmentsHorizontal.svelte

@@ -0,0 +1,17 @@
+<script lang="ts">
+	export let className = 'w-4 h-4';
+	export let strokeWidth = '1.5';
+</script>
+
+<svg
+	xmlns="http://www.w3.org/2000/svg"
+	viewBox="0 0 24 24"
+	stroke="currentColor"
+	fill="currentColor"
+	class={className}
+	stroke-width={strokeWidth}
+>
+	<path
+		d="M18.75 12.75h1.5a.75.75 0 0 0 0-1.5h-1.5a.75.75 0 0 0 0 1.5ZM12 6a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 12 6ZM12 18a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 12 18ZM3.75 6.75h1.5a.75.75 0 1 0 0-1.5h-1.5a.75.75 0 0 0 0 1.5ZM5.25 18.75h-1.5a.75.75 0 0 1 0-1.5h1.5a.75.75 0 0 1 0 1.5ZM3 12a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 3 12ZM9 3.75a2.25 2.25 0 1 0 0 4.5 2.25 2.25 0 0 0 0-4.5ZM12.75 12a2.25 2.25 0 1 1 4.5 0 2.25 2.25 0 0 1-4.5 0ZM9 15.75a2.25 2.25 0 1 0 0 4.5 2.25 2.25 0 0 0 0-4.5Z"
+	/>
+</svg>

+ 21 - 4
src/lib/components/layout/Navbar.svelte

@@ -21,6 +21,7 @@
 	import { page } from '$app/stores';
 	import { page } from '$app/stores';
 	import UserMenu from './Sidebar/UserMenu.svelte';
 	import UserMenu from './Sidebar/UserMenu.svelte';
 	import MenuLines from '../icons/MenuLines.svelte';
 	import MenuLines from '../icons/MenuLines.svelte';
+	import AdjustmentsHorizontal from '../icons/AdjustmentsHorizontal.svelte';
 
 
 	const i18n = getContext('i18n');
 	const i18n = getContext('i18n');
 
 
@@ -32,6 +33,7 @@
 	export let selectedModels;
 	export let selectedModels;
 
 
 	export let showModelSelector = true;
 	export let showModelSelector = true;
+	export let showControls = false;
 
 
 	let showShareChatModal = false;
 	let showShareChatModal = false;
 	let showDownloadChatModal = false;
 	let showDownloadChatModal = false;
@@ -48,7 +50,7 @@
 			>
 			>
 				<button
 				<button
 					id="sidebar-toggle-button"
 					id="sidebar-toggle-button"
-					class="cursor-pointer px-2 py-2 flex rounded-xl hover:bg-gray-100 dark:hover:bg-gray-850 transition"
+					class="cursor-pointer px-2 py-2 flex rounded-xl hover:bg-gray-50 dark:hover:bg-gray-850 transition"
 					on:click={() => {
 					on:click={() => {
 						showSidebar.set(!$showSidebar);
 						showSidebar.set(!$showSidebar);
 					}}
 					}}
@@ -58,6 +60,7 @@
 					</div>
 					</div>
 				</button>
 				</button>
 			</div>
 			</div>
+
 			<div class="flex-1 overflow-hidden max-w-full">
 			<div class="flex-1 overflow-hidden max-w-full">
 				{#if showModelSelector}
 				{#if showModelSelector}
 					<ModelSelector bind:selectedModels showSetDefault={!shareEnabled} />
 					<ModelSelector bind:selectedModels showSetDefault={!shareEnabled} />
@@ -79,7 +82,7 @@
 						}}
 						}}
 					>
 					>
 						<button
 						<button
-							class="hidden md:flex cursor-pointer px-2 py-2 rounded-xl hover:bg-gray-100 dark:hover:bg-gray-850 transition"
+							class="hidden md:flex cursor-pointer px-2 py-2 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-850 transition"
 							id="chat-context-menu-button"
 							id="chat-context-menu-button"
 						>
 						>
 							<div class=" m-auto self-center">
 							<div class=" m-auto self-center">
@@ -101,12 +104,26 @@
 						</button>
 						</button>
 					</Menu>
 					</Menu>
 				{/if}
 				{/if}
+
+				<Tooltip content={$i18n.t('Controls')}>
+					<button
+						class=" flex cursor-pointer px-2 py-2 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-850 transition"
+						on:click={() => {
+							showControls = !showControls;
+						}}
+					>
+						<div class=" m-auto self-center">
+							<AdjustmentsHorizontal className=" size-5" strokeWidth="0.5" />
+						</div>
+					</button>
+				</Tooltip>
+
 				<Tooltip content={$i18n.t('New Chat')}>
 				<Tooltip content={$i18n.t('New Chat')}>
 					<button
 					<button
 						id="new-chat-button"
 						id="new-chat-button"
 						class=" flex {$showSidebar
 						class=" flex {$showSidebar
 							? 'md:hidden'
 							? 'md:hidden'
-							: ''} cursor-pointer px-2 py-2 rounded-xl hover:bg-gray-100 dark:hover:bg-gray-850 transition"
+							: ''} cursor-pointer px-2 py-2 rounded-xl text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-850 transition"
 						on:click={() => {
 						on:click={() => {
 							initNewChat();
 							initNewChat();
 						}}
 						}}
@@ -140,7 +157,7 @@
 						}}
 						}}
 					>
 					>
 						<button
 						<button
-							class="select-none flex rounded-xl p-1.5 w-full hover:bg-gray-100 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"
 							aria-label="User Menu"
 						>
 						>
 							<div class=" self-center">
 							<div class=" self-center">

+ 5 - 41
src/lib/components/layout/Sidebar.svelte

@@ -186,6 +186,7 @@
 				goto('/');
 				goto('/');
 			}
 			}
 			await chats.set(await getChatList(localStorage.token));
 			await chats.set(await getChatList(localStorage.token));
+			await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned'));
 		}
 		}
 	};
 	};
 </script>
 </script>
@@ -260,7 +261,7 @@
 						alt="logo"
 						alt="logo"
 					/>
 					/>
 				</div>
 				</div>
-				<div class=" self-center font-medium text-sm text-gray-850 dark:text-white">
+				<div class=" self-center font-medium text-sm text-gray-850 dark:text-white font-primary">
 					{$i18n.t('New Chat')}
 					{$i18n.t('New Chat')}
 				</div>
 				</div>
 				<div class="self-center ml-auto">
 				<div class="self-center ml-auto">
@@ -338,7 +339,7 @@
 					</div>
 					</div>
 
 
 					<div class="flex self-center">
 					<div class="flex self-center">
-						<div class=" self-center font-medium text-sm">{$i18n.t('Workspace')}</div>
+						<div class=" self-center font-medium text-sm font-primary">{$i18n.t('Workspace')}</div>
 					</div>
 					</div>
 				</a>
 				</a>
 			</div>
 			</div>
@@ -532,7 +533,7 @@
 		<div class="px-2.5">
 		<div class="px-2.5">
 			<!-- <hr class=" border-gray-900 mb-1 w-full" /> -->
 			<!-- <hr class=" border-gray-900 mb-1 w-full" /> -->
 
 
-			<div class="flex flex-col">
+			<div class="flex flex-col font-primary">
 				{#if $user !== undefined}
 				{#if $user !== undefined}
 					<UserMenu
 					<UserMenu
 						role={$user.role}
 						role={$user.role}
@@ -555,50 +556,13 @@
 									alt="User profile"
 									alt="User profile"
 								/>
 								/>
 							</div>
 							</div>
-							<div class=" self-center font-semibold">{$user.name}</div>
+							<div class=" self-center font-medium">{$user.name}</div>
 						</button>
 						</button>
 					</UserMenu>
 					</UserMenu>
 				{/if}
 				{/if}
 			</div>
 			</div>
 		</div>
 		</div>
 	</div>
 	</div>
-
-	<!-- <div
-		id="sidebar-handle"
-		class=" hidden md:fixed left-0 top-[50dvh] -translate-y-1/2 transition-transform translate-x-[255px] md:translate-x-[260px] rotate-0"
-	>
-		<Tooltip
-			placement="right"
-			content={`${$showSidebar ? $i18n.t('Close') : $i18n.t('Open')} ${$i18n.t('sidebar')}`}
-			touch={false}
-		>
-			<button
-				id="sidebar-toggle-button"
-				class=" group"
-				on:click={() => {
-					showSidebar.set(!$showSidebar);
-				}}
-				><span class="" data-state="closed"
-					><div
-						class="flex h-[72px] w-8 items-center justify-center opacity-50 group-hover:opacity-100 transition"
-					>
-						<div class="flex h-6 w-6 flex-col items-center">
-							<div
-								class="h-3 w-1 rounded-full bg-[#0f0f0f] dark:bg-white rotate-0 translate-y-[0.15rem] {$showSidebar
-									? 'group-hover:rotate-[15deg]'
-									: 'group-hover:rotate-[-15deg]'}"
-							/>
-							<div
-								class="h-3 w-1 rounded-full bg-[#0f0f0f] dark:bg-white rotate-0 translate-y-[-0.15rem] {$showSidebar
-									? 'group-hover:rotate-[-15deg]'
-									: 'group-hover:rotate-[15deg]'}"
-							/>
-						</div>
-					</div>
-				</span>
-			</button>
-		</Tooltip>
-	</div> -->
 </div>
 </div>
 
 
 <style>
 <style>

+ 1 - 1
src/lib/components/layout/Sidebar/UserMenu.svelte

@@ -30,7 +30,7 @@
 
 
 	<slot name="content">
 	<slot name="content">
 		<DropdownMenu.Content
 		<DropdownMenu.Content
-			class="w-full {className} text-sm rounded-xl px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow"
+			class="w-full {className} text-sm rounded-xl px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow font-primary"
 			sideOffset={8}
 			sideOffset={8}
 			side="bottom"
 			side="bottom"
 			align="start"
 			align="start"

+ 1 - 1
src/lib/components/workspace/Documents.svelte

@@ -407,7 +407,7 @@
 						{/if}
 						{/if}
 					</div>
 					</div>
 					<div class=" self-center flex-1">
 					<div class=" self-center flex-1">
-						<div class=" font-bold line-clamp-1">#{doc.name} ({doc.filename})</div>
+						<div class=" font-semibold line-clamp-1">#{doc.name} ({doc.filename})</div>
 						<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
 						<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
 							{doc.title}
 							{doc.title}
 						</div>
 						</div>

+ 3 - 3
src/lib/components/workspace/Functions.svelte

@@ -200,14 +200,14 @@
 					<div class=" flex-1 self-center pl-1">
 					<div class=" flex-1 self-center pl-1">
 						<div class=" font-semibold flex items-center gap-1.5">
 						<div class=" font-semibold flex items-center gap-1.5">
 							<div
 							<div
-								class=" text-xs font-black px-1 rounded uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
+								class=" text-xs font-bold px-1 rounded uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
 							>
 							>
 								{func.type}
 								{func.type}
 							</div>
 							</div>
 
 
 							{#if func?.meta?.manifest?.version}
 							{#if func?.meta?.manifest?.version}
 								<div
 								<div
-									class="text-xs font-black px-1 rounded line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
+									class="text-xs font-bold px-1 rounded line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
 								>
 								>
 									v{func?.meta?.manifest?.version ?? ''}
 									v{func?.meta?.manifest?.version ?? ''}
 								</div>
 								</div>
@@ -430,7 +430,7 @@
 		</div>
 		</div>
 
 
 		<div class=" self-center">
 		<div class=" self-center">
-			<div class=" font-bold line-clamp-1">{$i18n.t('Discover a function')}</div>
+			<div class=" font-semibold line-clamp-1">{$i18n.t('Discover a function')}</div>
 			<div class=" text-sm line-clamp-1">
 			<div class=" text-sm line-clamp-1">
 				{$i18n.t('Discover, download, and explore custom functions')}
 				{$i18n.t('Discover, download, and explore custom functions')}
 			</div>
 			</div>

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

@@ -271,7 +271,7 @@
 	</div>
 	</div>
 
 
 	<div class=" self-center">
 	<div class=" self-center">
-		<div class=" font-bold line-clamp-1">{$i18n.t('Create a model')}</div>
+		<div class=" font-semibold line-clamp-1">{$i18n.t('Create a model')}</div>
 		<div class=" text-sm line-clamp-1">{$i18n.t('Customize models for a specific purpose')}</div>
 		<div class=" text-sm line-clamp-1">{$i18n.t('Customize models for a specific purpose')}</div>
 	</div>
 	</div>
 </a>
 </a>
@@ -297,7 +297,7 @@
 							: ''} "
 							: ''} "
 					>
 					>
 						<img
 						<img
-							src={model?.info?.meta?.profile_image_url ?? '/favicon.png'}
+							src={model?.info?.meta?.profile_image_url ?? '/static/favicon.png'}
 							alt="modelfile profile"
 							alt="modelfile profile"
 							class=" rounded-full w-full h-auto object-cover"
 							class=" rounded-full w-full h-auto object-cover"
 						/>
 						/>
@@ -307,7 +307,7 @@
 				<div
 				<div
 					class=" flex-1 self-center {model?.info?.meta?.hidden ?? false ? 'text-gray-500' : ''}"
 					class=" flex-1 self-center {model?.info?.meta?.hidden ?? false ? 'text-gray-500' : ''}"
 				>
 				>
-					<div class="  font-bold line-clamp-1">{model.name}</div>
+					<div class="  font-semibold line-clamp-1">{model.name}</div>
 					<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
 					<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
 						{!!model?.info?.meta?.description ? model?.info?.meta?.description : model.id}
 						{!!model?.info?.meta?.description ? model?.info?.meta?.description : model.id}
 					</div>
 					</div>
@@ -518,7 +518,7 @@
 		</div>
 		</div>
 
 
 		<div class=" self-center">
 		<div class=" self-center">
-			<div class=" font-bold line-clamp-1">{$i18n.t('Discover a model')}</div>
+			<div class=" font-semibold line-clamp-1">{$i18n.t('Discover a model')}</div>
 			<div class=" text-sm line-clamp-1">
 			<div class=" text-sm line-clamp-1">
 				{$i18n.t('Discover, download, and explore model presets')}
 				{$i18n.t('Discover, download, and explore model presets')}
 			</div>
 			</div>

+ 1 - 1
src/lib/components/workspace/Models/Knowledge/Selector.svelte

@@ -119,7 +119,7 @@
 							<div class="flex items-center">
 							<div class="flex items-center">
 								<div class="flex flex-col">
 								<div class="flex flex-col">
 									<div
 									<div
-										class=" w-fit text-xs font-black px-1 rounded uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
+										class=" w-fit text-xs font-bold px-1 rounded uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
 									>
 									>
 										{item?.type ?? 'Document'}
 										{item?.type ?? 'Document'}
 									</div>
 									</div>

+ 2 - 2
src/lib/components/workspace/Prompts.svelte

@@ -119,7 +119,7 @@
 			<div class=" flex flex-1 space-x-4 cursor-pointer w-full">
 			<div class=" flex flex-1 space-x-4 cursor-pointer w-full">
 				<a href={`/workspace/prompts/edit?command=${encodeURIComponent(prompt.command)}`}>
 				<a href={`/workspace/prompts/edit?command=${encodeURIComponent(prompt.command)}`}>
 					<div class=" flex-1 self-center pl-5">
 					<div class=" flex-1 self-center pl-5">
-						<div class=" font-bold line-clamp-1">{prompt.command}</div>
+						<div class=" font-semibold line-clamp-1">{prompt.command}</div>
 						<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
 						<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
 							{prompt.title}
 							{prompt.title}
 						</div>
 						</div>
@@ -299,7 +299,7 @@
 		</div>
 		</div>
 
 
 		<div class=" self-center">
 		<div class=" self-center">
-			<div class=" font-bold line-clamp-1">{$i18n.t('Discover a prompt')}</div>
+			<div class=" font-semibold line-clamp-1">{$i18n.t('Discover a prompt')}</div>
 			<div class=" text-sm line-clamp-1">
 			<div class=" text-sm line-clamp-1">
 				{$i18n.t('Discover, download, and explore custom prompts')}
 				{$i18n.t('Discover, download, and explore custom prompts')}
 			</div>
 			</div>

+ 3 - 3
src/lib/components/workspace/Tools.svelte

@@ -177,14 +177,14 @@
 					<div class=" flex-1 self-center pl-1">
 					<div class=" flex-1 self-center pl-1">
 						<div class=" font-semibold flex items-center gap-1.5">
 						<div class=" font-semibold flex items-center gap-1.5">
 							<div
 							<div
-								class=" text-xs font-black px-1 rounded uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
+								class=" text-xs font-bold px-1 rounded uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
 							>
 							>
 								TOOL
 								TOOL
 							</div>
 							</div>
 
 
 							{#if tool?.meta?.manifest?.version}
 							{#if tool?.meta?.manifest?.version}
 								<div
 								<div
-									class="text-xs font-black px-1 rounded line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
+									class="text-xs font-bold px-1 rounded line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
 								>
 								>
 									v{tool?.meta?.manifest?.version ?? ''}
 									v{tool?.meta?.manifest?.version ?? ''}
 								</div>
 								</div>
@@ -389,7 +389,7 @@
 		</div>
 		</div>
 
 
 		<div class=" self-center">
 		<div class=" self-center">
-			<div class=" font-bold line-clamp-1">{$i18n.t('Discover a tool')}</div>
+			<div class=" font-semibold line-clamp-1">{$i18n.t('Discover a tool')}</div>
 			<div class=" text-sm line-clamp-1">
 			<div class=" text-sm line-clamp-1">
 				{$i18n.t('Discover, download, and explore custom tools')}
 				{$i18n.t('Discover, download, and explore custom tools')}
 			</div>
 			</div>

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

@@ -88,6 +88,7 @@
 	"Chat": "المحادثة",
 	"Chat": "المحادثة",
 	"Chat Background Image": "",
 	"Chat Background Image": "",
 	"Chat Bubble UI": "UI الدردشة",
 	"Chat Bubble UI": "UI الدردشة",
+	"Chat Controls": "",
 	"Chat direction": "اتجاه المحادثة",
 	"Chat direction": "اتجاه المحادثة",
 	"Chat History": "تاريخ المحادثة",
 	"Chat History": "تاريخ المحادثة",
 	"Chat History is off for this browser.": "سجل الدردشة معطل لهذا المتصفح",
 	"Chat History is off for this browser.": "سجل الدردشة معطل لهذا المتصفح",
@@ -130,6 +131,7 @@
 	"Context Length": "طول السياق",
 	"Context Length": "طول السياق",
 	"Continue Response": "متابعة الرد",
 	"Continue Response": "متابعة الرد",
 	"Continue with {{provider}}": "",
 	"Continue with {{provider}}": "",
+	"Controls": "",
 	"Copied shared chat URL to clipboard!": "تم نسخ عنوان URL للدردشة المشتركة إلى الحافظة",
 	"Copied shared chat URL to clipboard!": "تم نسخ عنوان URL للدردشة المشتركة إلى الحافظة",
 	"Copy": "نسخ",
 	"Copy": "نسخ",
 	"Copy last code block": "انسخ كتلة التعليمات البرمجية الأخيرة",
 	"Copy last code block": "انسخ كتلة التعليمات البرمجية الأخيرة",
@@ -409,7 +411,6 @@
 	"Oops! Looks like the URL is invalid. Please double-check and try again.": "خطاء! يبدو أن عنوان URL غير صالح. يرجى التحقق مرة أخرى والمحاولة مرة أخرى.",
 	"Oops! Looks like the URL is invalid. Please double-check and try again.": "خطاء! يبدو أن عنوان URL غير صالح. يرجى التحقق مرة أخرى والمحاولة مرة أخرى.",
 	"Oops! There was an error in the previous response. Please try again or contact admin.": "",
 	"Oops! There was an error in the previous response. Please try again or contact admin.": "",
 	"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "خطاء! أنت تستخدم طريقة غير مدعومة (الواجهة الأمامية فقط). يرجى تقديم واجهة WebUI من الواجهة الخلفية.",
 	"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "خطاء! أنت تستخدم طريقة غير مدعومة (الواجهة الأمامية فقط). يرجى تقديم واجهة WebUI من الواجهة الخلفية.",
-	"Open": "فتح",
 	"Open AI (Dall-E)": "AI (Dall-E) فتح",
 	"Open AI (Dall-E)": "AI (Dall-E) فتح",
 	"Open new chat": "فتح محادثة جديده",
 	"Open new chat": "فتح محادثة جديده",
 	"Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "",
 	"Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "",
@@ -546,7 +547,6 @@
 	"Show shortcuts": "إظهار الاختصارات",
 	"Show shortcuts": "إظهار الاختصارات",
 	"Show your support!": "",
 	"Show your support!": "",
 	"Showcased creativity": "أظهر الإبداع",
 	"Showcased creativity": "أظهر الإبداع",
-	"sidebar": "الشريط الجانبي",
 	"Sign in": "تسجيل الدخول",
 	"Sign in": "تسجيل الدخول",
 	"Sign Out": "تسجيل الخروج",
 	"Sign Out": "تسجيل الخروج",
 	"Sign up": "تسجيل",
 	"Sign up": "تسجيل",

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

@@ -88,6 +88,7 @@
 	"Chat": "Чат",
 	"Chat": "Чат",
 	"Chat Background Image": "",
 	"Chat Background Image": "",
 	"Chat Bubble UI": "UI за чат бублон",
 	"Chat Bubble UI": "UI за чат бублон",
+	"Chat Controls": "",
 	"Chat direction": "Направление на чата",
 	"Chat direction": "Направление на чата",
 	"Chat History": "Чат История",
 	"Chat History": "Чат История",
 	"Chat History is off for this browser.": "Чат История е изключен за този браузър.",
 	"Chat History is off for this browser.": "Чат История е изключен за този браузър.",
@@ -130,6 +131,7 @@
 	"Context Length": "Дължина на Контекста",
 	"Context Length": "Дължина на Контекста",
 	"Continue Response": "Продължи отговора",
 	"Continue Response": "Продължи отговора",
 	"Continue with {{provider}}": "",
 	"Continue with {{provider}}": "",
+	"Controls": "",
 	"Copied shared chat URL to clipboard!": "Копирана е връзката за чат!",
 	"Copied shared chat URL to clipboard!": "Копирана е връзката за чат!",
 	"Copy": "Копирай",
 	"Copy": "Копирай",
 	"Copy last code block": "Копиране на последен код блок",
 	"Copy last code block": "Копиране на последен код блок",
@@ -409,7 +411,6 @@
 	"Oops! Looks like the URL is invalid. Please double-check and try again.": "Упс! Изглежда URL адресът е невалиден. Моля, проверете отново и опитайте пак.",
 	"Oops! Looks like the URL is invalid. Please double-check and try again.": "Упс! Изглежда URL адресът е невалиден. Моля, проверете отново и опитайте пак.",
 	"Oops! There was an error in the previous response. Please try again or contact admin.": "",
 	"Oops! There was an error in the previous response. Please try again or contact admin.": "",
 	"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Упс! Използвате неподдържан метод (само фронтенд). Моля, сервирайте WebUI от бекенда.",
 	"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Упс! Използвате неподдържан метод (само фронтенд). Моля, сервирайте WebUI от бекенда.",
-	"Open": "Отвори",
 	"Open AI (Dall-E)": "Open AI (Dall-E)",
 	"Open AI (Dall-E)": "Open AI (Dall-E)",
 	"Open new chat": "Отвори нов чат",
 	"Open new chat": "Отвори нов чат",
 	"Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "",
 	"Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "",
@@ -542,7 +543,6 @@
 	"Show shortcuts": "Покажи",
 	"Show shortcuts": "Покажи",
 	"Show your support!": "",
 	"Show your support!": "",
 	"Showcased creativity": "Показана креативност",
 	"Showcased creativity": "Показана креативност",
-	"sidebar": "sidebar",
 	"Sign in": "Вписване",
 	"Sign in": "Вписване",
 	"Sign Out": "Изход",
 	"Sign Out": "Изход",
 	"Sign up": "Регистрация",
 	"Sign up": "Регистрация",

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

@@ -88,6 +88,7 @@
 	"Chat": "চ্যাট",
 	"Chat": "চ্যাট",
 	"Chat Background Image": "",
 	"Chat Background Image": "",
 	"Chat Bubble UI": "চ্যাট বাবল UI",
 	"Chat Bubble UI": "চ্যাট বাবল UI",
+	"Chat Controls": "",
 	"Chat direction": "চ্যাট দিকনির্দেশ",
 	"Chat direction": "চ্যাট দিকনির্দেশ",
 	"Chat History": "চ্যাট হিস্টোরি",
 	"Chat History": "চ্যাট হিস্টোরি",
 	"Chat History is off for this browser.": "এই ব্রাউজারের জন্য চ্যাট হিস্টোরি বন্ধ আছে",
 	"Chat History is off for this browser.": "এই ব্রাউজারের জন্য চ্যাট হিস্টোরি বন্ধ আছে",
@@ -130,6 +131,7 @@
 	"Context Length": "কনটেক্সটের দৈর্ঘ্য",
 	"Context Length": "কনটেক্সটের দৈর্ঘ্য",
 	"Continue Response": "যাচাই করুন",
 	"Continue Response": "যাচাই করুন",
 	"Continue with {{provider}}": "",
 	"Continue with {{provider}}": "",
+	"Controls": "",
 	"Copied shared chat URL to clipboard!": "শেয়ারকৃত কথা-ব্যবহারের URL ক্লিপবোর্ডে কপি করা হয়েছে!",
 	"Copied shared chat URL to clipboard!": "শেয়ারকৃত কথা-ব্যবহারের URL ক্লিপবোর্ডে কপি করা হয়েছে!",
 	"Copy": "অনুলিপি",
 	"Copy": "অনুলিপি",
 	"Copy last code block": "সর্বশেষ কোড ব্লক কপি করুন",
 	"Copy last code block": "সর্বশেষ কোড ব্লক কপি করুন",
@@ -409,7 +411,6 @@
 	"Oops! Looks like the URL is invalid. Please double-check and try again.": "ওহ, মনে হচ্ছে ইউআরএলটা ইনভ্যালিড। দয়া করে আর চেক করে চেষ্টা করুন।",
 	"Oops! Looks like the URL is invalid. Please double-check and try again.": "ওহ, মনে হচ্ছে ইউআরএলটা ইনভ্যালিড। দয়া করে আর চেক করে চেষ্টা করুন।",
 	"Oops! There was an error in the previous response. Please try again or contact admin.": "",
 	"Oops! There was an error in the previous response. Please try again or contact admin.": "",
 	"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "আপনি একটা আনসাপোর্টেড পদ্ধতি (শুধু ফ্রন্টএন্ড) ব্যবহার করছেন। দয়া করে WebUI ব্যাকএন্ড থেকে চালনা করুন।",
 	"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "আপনি একটা আনসাপোর্টেড পদ্ধতি (শুধু ফ্রন্টএন্ড) ব্যবহার করছেন। দয়া করে WebUI ব্যাকএন্ড থেকে চালনা করুন।",
-	"Open": "খোলা",
 	"Open AI (Dall-E)": "Open AI (Dall-E)",
 	"Open AI (Dall-E)": "Open AI (Dall-E)",
 	"Open new chat": "নতুন চ্যাট খুলুন",
 	"Open new chat": "নতুন চ্যাট খুলুন",
 	"Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "",
 	"Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "",
@@ -542,7 +543,6 @@
 	"Show shortcuts": "শর্টকাটগুলো দেখান",
 	"Show shortcuts": "শর্টকাটগুলো দেখান",
 	"Show your support!": "",
 	"Show your support!": "",
 	"Showcased creativity": "সৃজনশীলতা প্রদর্শন",
 	"Showcased creativity": "সৃজনশীলতা প্রদর্শন",
-	"sidebar": "সাইডবার",
 	"Sign in": "সাইন ইন",
 	"Sign in": "সাইন ইন",
 	"Sign Out": "সাইন আউট",
 	"Sign Out": "সাইন আউট",
 	"Sign up": "সাইন আপ",
 	"Sign up": "সাইন আপ",

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