diff --git a/CHANGELOG.md b/CHANGELOG.md index 86ff57384..240fd6324 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,27 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.5.13] - 2025-02-17 + +### Added + +- **🌐 Full Context Mode for Web Search**: Enable highly accurate web searches by utilizing full context mode—ideal for models with large context windows, ensuring more precise and insightful results. +- **⚡ Optimized Asynchronous Web Search**: Web searches now load significantly faster with optimized async support, providing users with quicker, more efficient information retrieval. +- **🔄 Auto Text Direction for RTL Languages**: Automatic text alignment based on language input, ensuring seamless conversation flow for Arabic, Hebrew, and other right-to-left scripts. +- **🚀 Jupyter Notebook Support for Code Execution**: The "Run" button in code blocks can now use Jupyter for execution, offering a powerful, dynamic coding experience directly in the chat. +- **🗑️ Message Delete Confirmation Dialog**: Prevent accidental deletions with a new confirmation prompt before removing messages, adding an additional layer of security to your chat history. +- **📥 Download Button for SVG Diagrams**: SVG diagrams generated within chat can now be downloaded instantly, making it easier to save and share complex visual data. +- **✨ General UI/UX Improvements and Backend Stability**: A refined interface with smoother interactions, improved layouts, and backend stability enhancements for a more reliable, polished experience. + +### Fixed + +- **🛠️ Temporary Chat Message Continue Button Fixed**: The "Continue Response" button for temporary chats now works as expected, ensuring an uninterrupted conversation flow. + +### Changed + +- **📝 Prompt Variable Update**: Deprecated square bracket '[]' indicators for prompt variables; now requires double curly brackets '{{}}' for consistency and clarity. +- **🔧 Stability Enhancements**: Error handling improved in chat history, ensuring smoother operations when reviewing previous messages. + ## [0.5.12] - 2025-02-13 ### Added diff --git a/README.md b/README.md index 56ab09b05..54ad41503 100644 --- a/README.md +++ b/README.md @@ -13,10 +13,15 @@ **Open WebUI is an [extensible](https://docs.openwebui.com/features/plugin/), feature-rich, and user-friendly self-hosted AI platform designed to operate entirely offline.** It supports various LLM runners like **Ollama** and **OpenAI-compatible APIs**, with **built-in inference engine** for RAG, making it a **powerful AI deployment solution**. -For more information, be sure to check out our [Open WebUI Documentation](https://docs.openwebui.com/). - ![Open WebUI Demo](./demo.gif) +> [!TIP] +> **Looking for an [Enterprise Plan](https://docs.openwebui.com/enterprise)?** – **[Speak with Our Sales Team Today!](mailto:sales@openwebui.com)** +> +> Get **enhanced capabilities**, including **custom theming and branding**, **Service Level Agreement (SLA) support**, **Long-Term Support (LTS) versions**, and **more!** + +For more information, be sure to check out our [Open WebUI Documentation](https://docs.openwebui.com/). + ## Key Features of Open WebUI ⭐ - 🚀 **Effortless Setup**: Install seamlessly using Docker or Kubernetes (kubectl, kustomize or helm) for a hassle-free experience with support for both `:ollama` and `:cuda` tagged images. diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index adfdcfec8..7fff81ed3 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -2,6 +2,8 @@ import json import logging import os import shutil +import base64 + from datetime import datetime from pathlib import Path from typing import Generic, Optional, TypeVar @@ -586,6 +588,20 @@ load_oauth_providers() STATIC_DIR = Path(os.getenv("STATIC_DIR", OPEN_WEBUI_DIR / "static")).resolve() + +def override_static(path: str, content: str): + # Ensure path is safe + if "/" in path or ".." in path: + log.error(f"Invalid path: {path}") + return + + file_path = os.path.join(STATIC_DIR, path) + os.makedirs(os.path.dirname(file_path), exist_ok=True) + + with open(file_path, "wb") as f: + f.write(base64.b64decode(content)) # Convert Base64 back to raw binary + + frontend_favicon = FRONTEND_BUILD_DIR / "static" / "favicon.png" if frontend_favicon.exists(): @@ -593,8 +609,6 @@ if frontend_favicon.exists(): shutil.copyfile(frontend_favicon, STATIC_DIR / "favicon.png") except Exception as e: logging.error(f"An error occurred: {e}") -else: - logging.warning(f"Frontend favicon not found at {frontend_favicon}") frontend_splash = FRONTEND_BUILD_DIR / "static" / "splash.png" @@ -603,12 +617,18 @@ if frontend_splash.exists(): shutil.copyfile(frontend_splash, STATIC_DIR / "splash.png") except Exception as e: logging.error(f"An error occurred: {e}") -else: - logging.warning(f"Frontend splash not found at {frontend_splash}") + +frontend_loader = FRONTEND_BUILD_DIR / "static" / "loader.js" + +if frontend_loader.exists(): + try: + shutil.copyfile(frontend_loader, STATIC_DIR / "loader.js") + except Exception as e: + logging.error(f"An error occurred: {e}") #################################### -# CUSTOM_NAME +# CUSTOM_NAME (Legacy) #################################### CUSTOM_NAME = os.environ.get("CUSTOM_NAME", "") @@ -650,6 +670,16 @@ if CUSTOM_NAME: pass +#################################### +# LICENSE_KEY +#################################### + +LICENSE_KEY = PersistentConfig( + "LICENSE_KEY", + "license.key", + os.environ.get("LICENSE_KEY", ""), +) + #################################### # STORAGE PROVIDER #################################### @@ -1347,6 +1377,39 @@ Responses from models: {{responses}}""" # Code Interpreter #################################### + +CODE_EXECUTION_ENGINE = PersistentConfig( + "CODE_EXECUTION_ENGINE", + "code_execution.engine", + os.environ.get("CODE_EXECUTION_ENGINE", "pyodide"), +) + +CODE_EXECUTION_JUPYTER_URL = PersistentConfig( + "CODE_EXECUTION_JUPYTER_URL", + "code_execution.jupyter.url", + os.environ.get("CODE_EXECUTION_JUPYTER_URL", ""), +) + +CODE_EXECUTION_JUPYTER_AUTH = PersistentConfig( + "CODE_EXECUTION_JUPYTER_AUTH", + "code_execution.jupyter.auth", + os.environ.get("CODE_EXECUTION_JUPYTER_AUTH", ""), +) + +CODE_EXECUTION_JUPYTER_AUTH_TOKEN = PersistentConfig( + "CODE_EXECUTION_JUPYTER_AUTH_TOKEN", + "code_execution.jupyter.auth_token", + os.environ.get("CODE_EXECUTION_JUPYTER_AUTH_TOKEN", ""), +) + + +CODE_EXECUTION_JUPYTER_AUTH_PASSWORD = PersistentConfig( + "CODE_EXECUTION_JUPYTER_AUTH_PASSWORD", + "code_execution.jupyter.auth_password", + os.environ.get("CODE_EXECUTION_JUPYTER_AUTH_PASSWORD", ""), +) + + ENABLE_CODE_INTERPRETER = PersistentConfig( "ENABLE_CODE_INTERPRETER", "code_interpreter.enable", @@ -1368,26 +1431,37 @@ CODE_INTERPRETER_PROMPT_TEMPLATE = PersistentConfig( CODE_INTERPRETER_JUPYTER_URL = PersistentConfig( "CODE_INTERPRETER_JUPYTER_URL", "code_interpreter.jupyter.url", - os.environ.get("CODE_INTERPRETER_JUPYTER_URL", ""), + os.environ.get( + "CODE_INTERPRETER_JUPYTER_URL", os.environ.get("CODE_EXECUTION_JUPYTER_URL", "") + ), ) CODE_INTERPRETER_JUPYTER_AUTH = PersistentConfig( "CODE_INTERPRETER_JUPYTER_AUTH", "code_interpreter.jupyter.auth", - os.environ.get("CODE_INTERPRETER_JUPYTER_AUTH", ""), + os.environ.get( + "CODE_INTERPRETER_JUPYTER_AUTH", + os.environ.get("CODE_EXECUTION_JUPYTER_AUTH", ""), + ), ) CODE_INTERPRETER_JUPYTER_AUTH_TOKEN = PersistentConfig( "CODE_INTERPRETER_JUPYTER_AUTH_TOKEN", "code_interpreter.jupyter.auth_token", - os.environ.get("CODE_INTERPRETER_JUPYTER_AUTH_TOKEN", ""), + os.environ.get( + "CODE_INTERPRETER_JUPYTER_AUTH_TOKEN", + os.environ.get("CODE_EXECUTION_JUPYTER_AUTH_TOKEN", ""), + ), ) CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD = PersistentConfig( "CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD", "code_interpreter.jupyter.auth_password", - os.environ.get("CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD", ""), + os.environ.get( + "CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD", + os.environ.get("CODE_EXECUTION_JUPYTER_AUTH_PASSWORD", ""), + ), ) @@ -1706,6 +1780,12 @@ RAG_WEB_SEARCH_ENGINE = PersistentConfig( os.getenv("RAG_WEB_SEARCH_ENGINE", ""), ) +RAG_WEB_SEARCH_FULL_CONTEXT = PersistentConfig( + "RAG_WEB_SEARCH_FULL_CONTEXT", + "rag.web.search.full_context", + os.getenv("RAG_WEB_SEARCH_FULL_CONTEXT", "False").lower() == "true", +) + # You can provide a list of your own websites to filter after performing a web search. # This ensures the highest level of safety and reliability of the information sources. RAG_WEB_SEARCH_DOMAIN_FILTER_LIST = PersistentConfig( @@ -1853,6 +1933,11 @@ RAG_WEB_SEARCH_CONCURRENT_REQUESTS = PersistentConfig( int(os.getenv("RAG_WEB_SEARCH_CONCURRENT_REQUESTS", "10")), ) +RAG_WEB_SEARCH_TRUST_ENV = PersistentConfig( + "RAG_WEB_SEARCH_TRUST_ENV", + "rag.web.search.trust_env", + os.getenv("RAG_WEB_SEARCH_TRUST_ENV", False), +) #################################### # Images diff --git a/backend/open_webui/env.py b/backend/open_webui/env.py index 0be3887f8..96e288d77 100644 --- a/backend/open_webui/env.py +++ b/backend/open_webui/env.py @@ -113,6 +113,7 @@ if WEBUI_NAME != "Open WebUI": WEBUI_FAVICON_URL = "https://openwebui.com/favicon.png" +TRUSTED_SIGNATURE_KEY = os.environ.get("TRUSTED_SIGNATURE_KEY", "") #################################### # ENV (dev,test,prod) diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index a36323151..dd0c2bf9f 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -88,6 +88,7 @@ from open_webui.models.models import Models from open_webui.models.users import UserModel, Users from open_webui.config import ( + LICENSE_KEY, # Ollama ENABLE_OLLAMA_API, OLLAMA_BASE_URLS, @@ -99,7 +100,12 @@ from open_webui.config import ( OPENAI_API_CONFIGS, # Direct Connections ENABLE_DIRECT_CONNECTIONS, - # Code Interpreter + # Code Execution + CODE_EXECUTION_ENGINE, + CODE_EXECUTION_JUPYTER_URL, + CODE_EXECUTION_JUPYTER_AUTH, + CODE_EXECUTION_JUPYTER_AUTH_TOKEN, + CODE_EXECUTION_JUPYTER_AUTH_PASSWORD, ENABLE_CODE_INTERPRETER, CODE_INTERPRETER_ENGINE, CODE_INTERPRETER_PROMPT_TEMPLATE, @@ -173,8 +179,10 @@ from open_webui.config import ( YOUTUBE_LOADER_PROXY_URL, # Retrieval (Web Search) RAG_WEB_SEARCH_ENGINE, + RAG_WEB_SEARCH_FULL_CONTEXT, RAG_WEB_SEARCH_RESULT_COUNT, RAG_WEB_SEARCH_CONCURRENT_REQUESTS, + RAG_WEB_SEARCH_TRUST_ENV, RAG_WEB_SEARCH_DOMAIN_FILTER_LIST, JINA_API_KEY, SEARCHAPI_API_KEY, @@ -313,15 +321,17 @@ from open_webui.utils.middleware import process_chat_payload, process_chat_respo from open_webui.utils.access_control import has_access from open_webui.utils.auth import ( + get_license_data, decode_token, get_admin_user, get_verified_user, ) -from open_webui.utils.oauth import oauth_manager +from open_webui.utils.oauth import OAuthManager from open_webui.utils.security_headers import SecurityHeadersMiddleware from open_webui.tasks import stop_task, list_tasks # Import from tasks.py + if SAFE_MODE: print("SAFE MODE ENABLED") Functions.deactivate_all_functions() @@ -348,12 +358,12 @@ class SPAStaticFiles(StaticFiles): print( rf""" - ___ __ __ _ _ _ ___ - / _ \ _ __ ___ _ __ \ \ / /__| |__ | | | |_ _| -| | | | '_ \ / _ \ '_ \ \ \ /\ / / _ \ '_ \| | | || | -| |_| | |_) | __/ | | | \ V V / __/ |_) | |_| || | - \___/| .__/ \___|_| |_| \_/\_/ \___|_.__/ \___/|___| - |_| + ██████╗ ██████╗ ███████╗███╗ ██╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗██╗ +██╔═══██╗██╔══██╗██╔════╝████╗ ██║ ██║ ██║██╔════╝██╔══██╗██║ ██║██║ +██║ ██║██████╔╝█████╗ ██╔██╗ ██║ ██║ █╗ ██║█████╗ ██████╔╝██║ ██║██║ +██║ ██║██╔═══╝ ██╔══╝ ██║╚██╗██║ ██║███╗██║██╔══╝ ██╔══██╗██║ ██║██║ +╚██████╔╝██║ ███████╗██║ ╚████║ ╚███╔███╔╝███████╗██████╔╝╚██████╔╝██║ + ╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝ ╚══╝╚══╝ ╚══════╝╚═════╝ ╚═════╝ ╚═╝ v{VERSION} - building the best open-source AI user interface. @@ -368,6 +378,9 @@ async def lifespan(app: FastAPI): if RESET_CONFIG_ON_START: reset_config() + if app.state.config.LICENSE_KEY: + get_license_data(app, app.state.config.LICENSE_KEY) + asyncio.create_task(periodic_usage_pool_cleanup()) yield @@ -379,8 +392,12 @@ app = FastAPI( lifespan=lifespan, ) +oauth_manager = OAuthManager(app) + app.state.config = AppConfig() +app.state.WEBUI_NAME = WEBUI_NAME +app.state.config.LICENSE_KEY = LICENSE_KEY ######################################## # @@ -482,10 +499,10 @@ app.state.config.LDAP_CIPHERS = LDAP_CIPHERS app.state.AUTH_TRUSTED_EMAIL_HEADER = WEBUI_AUTH_TRUSTED_EMAIL_HEADER app.state.AUTH_TRUSTED_NAME_HEADER = WEBUI_AUTH_TRUSTED_NAME_HEADER +app.state.USER_COUNT = None app.state.TOOLS = {} app.state.FUNCTIONS = {} - ######################################## # # RETRIEVAL @@ -532,6 +549,7 @@ app.state.config.YOUTUBE_LOADER_PROXY_URL = YOUTUBE_LOADER_PROXY_URL app.state.config.ENABLE_RAG_WEB_SEARCH = ENABLE_RAG_WEB_SEARCH app.state.config.RAG_WEB_SEARCH_ENGINE = RAG_WEB_SEARCH_ENGINE +app.state.config.RAG_WEB_SEARCH_FULL_CONTEXT = RAG_WEB_SEARCH_FULL_CONTEXT app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST = RAG_WEB_SEARCH_DOMAIN_FILTER_LIST app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION = ENABLE_GOOGLE_DRIVE_INTEGRATION @@ -558,6 +576,7 @@ app.state.config.EXA_API_KEY = EXA_API_KEY app.state.config.RAG_WEB_SEARCH_RESULT_COUNT = RAG_WEB_SEARCH_RESULT_COUNT app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS = RAG_WEB_SEARCH_CONCURRENT_REQUESTS +app.state.config.RAG_WEB_SEARCH_TRUST_ENV = RAG_WEB_SEARCH_TRUST_ENV app.state.EMBEDDING_FUNCTION = None app.state.ef = None @@ -601,10 +620,18 @@ app.state.EMBEDDING_FUNCTION = get_embedding_function( ######################################## # -# CODE INTERPRETER +# CODE EXECUTION # ######################################## +app.state.config.CODE_EXECUTION_ENGINE = CODE_EXECUTION_ENGINE +app.state.config.CODE_EXECUTION_JUPYTER_URL = CODE_EXECUTION_JUPYTER_URL +app.state.config.CODE_EXECUTION_JUPYTER_AUTH = CODE_EXECUTION_JUPYTER_AUTH +app.state.config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN = CODE_EXECUTION_JUPYTER_AUTH_TOKEN +app.state.config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD = ( + CODE_EXECUTION_JUPYTER_AUTH_PASSWORD +) + app.state.config.ENABLE_CODE_INTERPRETER = ENABLE_CODE_INTERPRETER app.state.config.CODE_INTERPRETER_ENGINE = CODE_INTERPRETER_ENGINE app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE = CODE_INTERPRETER_PROMPT_TEMPLATE @@ -1069,7 +1096,7 @@ async def get_app_config(request: Request): return { **({"onboarding": True} if onboarding else {}), "status": True, - "name": WEBUI_NAME, + "name": app.state.WEBUI_NAME, "version": VERSION, "default_locale": str(DEFAULT_LOCALE), "oauth": { @@ -1108,6 +1135,9 @@ async def get_app_config(request: Request): { "default_models": app.state.config.DEFAULT_MODELS, "default_prompt_suggestions": app.state.config.DEFAULT_PROMPT_SUGGESTIONS, + "code": { + "engine": app.state.config.CODE_EXECUTION_ENGINE, + }, "audio": { "tts": { "engine": app.state.config.TTS_ENGINE, @@ -1204,7 +1234,7 @@ if len(OAUTH_PROVIDERS) > 0: @app.get("/oauth/{provider}/login") async def oauth_login(provider: str, request: Request): - return await oauth_manager.handle_login(provider, request) + return await oauth_manager.handle_login(request, provider) # OAuth login logic is as follows: @@ -1215,14 +1245,14 @@ async def oauth_login(provider: str, request: Request): # - Email addresses are considered unique, so we fail registration if the email address is already taken @app.get("/oauth/{provider}/callback") async def oauth_callback(provider: str, request: Request, response: Response): - return await oauth_manager.handle_callback(provider, request, response) + return await oauth_manager.handle_callback(request, provider, response) @app.get("/manifest.json") async def get_manifest_json(): return { - "name": WEBUI_NAME, - "short_name": WEBUI_NAME, + "name": app.state.WEBUI_NAME, + "short_name": app.state.WEBUI_NAME, "description": "Open WebUI is an open, extensible, user-friendly interface for AI that adapts to your workflow.", "start_url": "/", "display": "standalone", @@ -1249,8 +1279,8 @@ async def get_manifest_json(): async def get_opensearch_xml(): xml_content = rf""" - {WEBUI_NAME} - Search {WEBUI_NAME} + {app.state.WEBUI_NAME} + Search {app.state.WEBUI_NAME} UTF-8 {app.state.config.WEBUI_URL}/static/favicon.png diff --git a/backend/open_webui/retrieval/utils.py b/backend/open_webui/retrieval/utils.py index b7da20ad5..437183369 100644 --- a/backend/open_webui/retrieval/utils.py +++ b/backend/open_webui/retrieval/utils.py @@ -304,7 +304,12 @@ def get_sources_from_files( relevant_contexts = [] for file in files: - if file.get("context") == "full": + if file.get("docs"): + context = { + "documents": [[doc.get("content") for doc in file.get("docs")]], + "metadatas": [[doc.get("metadata") for doc in file.get("docs")]], + } + elif file.get("context") == "full": context = { "documents": [[file.get("file").get("data", {}).get("content")]], "metadatas": [[{"file_id": file.get("id"), "name": file.get("name")}]], diff --git a/backend/open_webui/retrieval/web/duckduckgo.py b/backend/open_webui/retrieval/web/duckduckgo.py index 7c0c3f1c2..d95086671 100644 --- a/backend/open_webui/retrieval/web/duckduckgo.py +++ b/backend/open_webui/retrieval/web/duckduckgo.py @@ -32,19 +32,15 @@ def search_duckduckgo( # Convert the search results into a list search_results = [r for r in ddgs_gen] - # Create an empty list to store the SearchResult objects - results = [] - # Iterate over each search result - for result in search_results: - # Create a SearchResult object and append it to the results list - results.append( - SearchResult( - link=result["href"], - title=result.get("title"), - snippet=result.get("body"), - ) - ) if filter_list: - results = get_filtered_results(results, filter_list) + search_results = get_filtered_results(search_results, filter_list) + # Return the list of search results - return results + return [ + SearchResult( + link=result["href"], + title=result.get("title"), + snippet=result.get("body"), + ) + for result in search_results + ] diff --git a/backend/open_webui/retrieval/web/tavily.py b/backend/open_webui/retrieval/web/tavily.py index cc468725d..da70aa8e7 100644 --- a/backend/open_webui/retrieval/web/tavily.py +++ b/backend/open_webui/retrieval/web/tavily.py @@ -1,4 +1,5 @@ import logging +from typing import Optional import requests from open_webui.retrieval.web.main import SearchResult @@ -8,7 +9,13 @@ log = logging.getLogger(__name__) log.setLevel(SRC_LOG_LEVELS["RAG"]) -def search_tavily(api_key: str, query: str, count: int) -> list[SearchResult]: +def search_tavily( + api_key: str, + query: str, + count: int, + filter_list: Optional[list[str]] = None, + # **kwargs, +) -> list[SearchResult]: """Search using Tavily's Search API and return the results as a list of SearchResult objects. Args: @@ -20,7 +27,6 @@ def search_tavily(api_key: str, query: str, count: int) -> list[SearchResult]: """ url = "https://api.tavily.com/search" data = {"query": query, "api_key": api_key} - response = requests.post(url, json=data) response.raise_for_status() diff --git a/backend/open_webui/retrieval/web/utils.py b/backend/open_webui/retrieval/web/utils.py index 3a73444b3..145f1adbc 100644 --- a/backend/open_webui/retrieval/web/utils.py +++ b/backend/open_webui/retrieval/web/utils.py @@ -1,7 +1,10 @@ import socket +import aiohttp +import asyncio import urllib.parse import validators -from typing import Union, Sequence, Iterator +from typing import Any, AsyncIterator, Dict, Iterator, List, Sequence, Union + from langchain_community.document_loaders import ( WebBaseLoader, @@ -68,6 +71,70 @@ def resolve_hostname(hostname): class SafeWebBaseLoader(WebBaseLoader): """WebBaseLoader with enhanced error handling for URLs.""" + def __init__(self, trust_env: bool = False, *args, **kwargs): + """Initialize SafeWebBaseLoader + Args: + trust_env (bool, optional): set to True if using proxy to make web requests, for example + using http(s)_proxy environment variables. Defaults to False. + """ + super().__init__(*args, **kwargs) + self.trust_env = trust_env + + async def _fetch( + self, url: str, retries: int = 3, cooldown: int = 2, backoff: float = 1.5 + ) -> str: + async with aiohttp.ClientSession(trust_env=self.trust_env) as session: + for i in range(retries): + try: + kwargs: Dict = dict( + headers=self.session.headers, + cookies=self.session.cookies.get_dict(), + ) + if not self.session.verify: + kwargs["ssl"] = False + + async with session.get( + url, **(self.requests_kwargs | kwargs) + ) as response: + if self.raise_for_status: + response.raise_for_status() + return await response.text() + except aiohttp.ClientConnectionError as e: + if i == retries - 1: + raise + else: + log.warning( + f"Error fetching {url} with attempt " + f"{i + 1}/{retries}: {e}. Retrying..." + ) + await asyncio.sleep(cooldown * backoff**i) + raise ValueError("retry count exceeded") + + def _unpack_fetch_results( + self, results: Any, urls: List[str], parser: Union[str, None] = None + ) -> List[Any]: + """Unpack fetch results into BeautifulSoup objects.""" + from bs4 import BeautifulSoup + + final_results = [] + for i, result in enumerate(results): + url = urls[i] + if parser is None: + if url.endswith(".xml"): + parser = "xml" + else: + parser = self.default_parser + self._check_parser(parser) + final_results.append(BeautifulSoup(result, parser, **self.bs_kwargs)) + return final_results + + async def ascrape_all( + self, urls: List[str], parser: Union[str, None] = None + ) -> List[Any]: + """Async fetch all urls, then return soups for all results.""" + results = await self.fetch_all(urls) + return self._unpack_fetch_results(results, urls, parser=parser) + def lazy_load(self) -> Iterator[Document]: """Lazy load text from the url(s) in web_path with error handling.""" for path in self.web_paths: @@ -91,18 +158,40 @@ class SafeWebBaseLoader(WebBaseLoader): # Log the error and continue with the next URL log.error(f"Error loading {path}: {e}") + async def alazy_load(self) -> AsyncIterator[Document]: + """Async lazy load text from the url(s) in web_path.""" + results = await self.ascrape_all(self.web_paths) + for path, soup in zip(self.web_paths, results): + text = soup.get_text(**self.bs_get_text_kwargs) + metadata = {"source": path} + if title := soup.find("title"): + metadata["title"] = title.get_text() + if description := soup.find("meta", attrs={"name": "description"}): + metadata["description"] = description.get( + "content", "No description found." + ) + if html := soup.find("html"): + metadata["language"] = html.get("lang", "No language found.") + yield Document(page_content=text, metadata=metadata) + + async def aload(self) -> list[Document]: + """Load data into Document objects.""" + return [document async for document in self.alazy_load()] + def get_web_loader( urls: Union[str, Sequence[str]], verify_ssl: bool = True, requests_per_second: int = 2, + trust_env: bool = False, ): # Check if the URLs are valid safe_urls = safe_validate_urls([urls] if isinstance(urls, str) else urls) return SafeWebBaseLoader( - safe_urls, + web_path=safe_urls, verify_ssl=verify_ssl, requests_per_second=requests_per_second, continue_on_failure=True, + trust_env=trust_env, ) diff --git a/backend/open_webui/routers/auths.py b/backend/open_webui/routers/auths.py index b6a2c7562..a3f2e8b32 100644 --- a/backend/open_webui/routers/auths.py +++ b/backend/open_webui/routers/auths.py @@ -251,9 +251,19 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm): user = Users.get_user_by_email(mail) if not user: try: + user_count = Users.get_num_users() + if ( + request.app.state.USER_COUNT + and user_count >= request.app.state.USER_COUNT + ): + raise HTTPException( + status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + role = ( "admin" - if Users.get_num_users() == 0 + if user_count == 0 else request.app.state.config.DEFAULT_USER_ROLE ) @@ -413,6 +423,7 @@ async def signin(request: Request, response: Response, form_data: SigninForm): @router.post("/signup", response_model=SessionUserResponse) async def signup(request: Request, response: Response, form_data: SignupForm): + if WEBUI_AUTH: if ( not request.app.state.config.ENABLE_SIGNUP @@ -427,6 +438,12 @@ async def signup(request: Request, response: Response, form_data: SignupForm): status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED ) + user_count = Users.get_num_users() + if request.app.state.USER_COUNT and user_count >= request.app.state.USER_COUNT: + raise HTTPException( + status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED + ) + if not validate_email_format(form_data.email.lower()): raise HTTPException( status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.INVALID_EMAIL_FORMAT @@ -437,12 +454,10 @@ async def signup(request: Request, response: Response, form_data: SignupForm): try: role = ( - "admin" - if Users.get_num_users() == 0 - else request.app.state.config.DEFAULT_USER_ROLE + "admin" if user_count == 0 else request.app.state.config.DEFAULT_USER_ROLE ) - if Users.get_num_users() == 0: + if user_count == 0: # Disable signup after the first user is created request.app.state.config.ENABLE_SIGNUP = False @@ -484,6 +499,7 @@ async def signup(request: Request, response: Response, form_data: SignupForm): if request.app.state.config.WEBHOOK_URL: post_webhook( + request.app.state.WEBUI_NAME, request.app.state.config.WEBHOOK_URL, WEBHOOK_MESSAGES.USER_SIGNUP(user.name), { diff --git a/backend/open_webui/routers/channels.py b/backend/open_webui/routers/channels.py index da6a8d01f..6da3f04ce 100644 --- a/backend/open_webui/routers/channels.py +++ b/backend/open_webui/routers/channels.py @@ -192,7 +192,7 @@ async def get_channel_messages( ############################ -async def send_notification(webui_url, channel, message, active_user_ids): +async def send_notification(name, webui_url, channel, message, active_user_ids): users = get_users_with_access("read", channel.access_control) for user in users: @@ -206,6 +206,7 @@ async def send_notification(webui_url, channel, message, active_user_ids): if webhook_url: post_webhook( + name, webhook_url, f"#{channel.name} - {webui_url}/channels/{channel.id}\n\n{message.content}", { @@ -302,6 +303,7 @@ async def post_new_message( background_tasks.add_task( send_notification, + request.app.state.WEBUI_NAME, request.app.state.config.WEBUI_URL, channel, message, diff --git a/backend/open_webui/routers/configs.py b/backend/open_webui/routers/configs.py index 016075234..d460ae670 100644 --- a/backend/open_webui/routers/configs.py +++ b/backend/open_webui/routers/configs.py @@ -70,6 +70,11 @@ async def set_direct_connections_config( # CodeInterpreterConfig ############################ class CodeInterpreterConfigForm(BaseModel): + CODE_EXECUTION_ENGINE: str + CODE_EXECUTION_JUPYTER_URL: Optional[str] + CODE_EXECUTION_JUPYTER_AUTH: Optional[str] + CODE_EXECUTION_JUPYTER_AUTH_TOKEN: Optional[str] + CODE_EXECUTION_JUPYTER_AUTH_PASSWORD: Optional[str] ENABLE_CODE_INTERPRETER: bool CODE_INTERPRETER_ENGINE: str CODE_INTERPRETER_PROMPT_TEMPLATE: Optional[str] @@ -79,9 +84,14 @@ class CodeInterpreterConfigForm(BaseModel): CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD: Optional[str] -@router.get("/code_interpreter", response_model=CodeInterpreterConfigForm) -async def get_code_interpreter_config(request: Request, user=Depends(get_admin_user)): +@router.get("/code_execution", response_model=CodeInterpreterConfigForm) +async def get_code_execution_config(request: Request, user=Depends(get_admin_user)): return { + "CODE_EXECUTION_ENGINE": request.app.state.config.CODE_EXECUTION_ENGINE, + "CODE_EXECUTION_JUPYTER_URL": request.app.state.config.CODE_EXECUTION_JUPYTER_URL, + "CODE_EXECUTION_JUPYTER_AUTH": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH, + "CODE_EXECUTION_JUPYTER_AUTH_TOKEN": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN, + "CODE_EXECUTION_JUPYTER_AUTH_PASSWORD": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD, "ENABLE_CODE_INTERPRETER": request.app.state.config.ENABLE_CODE_INTERPRETER, "CODE_INTERPRETER_ENGINE": request.app.state.config.CODE_INTERPRETER_ENGINE, "CODE_INTERPRETER_PROMPT_TEMPLATE": request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE, @@ -92,10 +102,25 @@ async def get_code_interpreter_config(request: Request, user=Depends(get_admin_u } -@router.post("/code_interpreter", response_model=CodeInterpreterConfigForm) -async def set_code_interpreter_config( +@router.post("/code_execution", response_model=CodeInterpreterConfigForm) +async def set_code_execution_config( request: Request, form_data: CodeInterpreterConfigForm, user=Depends(get_admin_user) ): + + request.app.state.config.CODE_EXECUTION_ENGINE = form_data.CODE_EXECUTION_ENGINE + request.app.state.config.CODE_EXECUTION_JUPYTER_URL = ( + form_data.CODE_EXECUTION_JUPYTER_URL + ) + request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH = ( + form_data.CODE_EXECUTION_JUPYTER_AUTH + ) + request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN = ( + form_data.CODE_EXECUTION_JUPYTER_AUTH_TOKEN + ) + request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD = ( + form_data.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD + ) + request.app.state.config.ENABLE_CODE_INTERPRETER = form_data.ENABLE_CODE_INTERPRETER request.app.state.config.CODE_INTERPRETER_ENGINE = form_data.CODE_INTERPRETER_ENGINE request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE = ( @@ -118,6 +143,11 @@ async def set_code_interpreter_config( ) return { + "CODE_EXECUTION_ENGINE": request.app.state.config.CODE_EXECUTION_ENGINE, + "CODE_EXECUTION_JUPYTER_URL": request.app.state.config.CODE_EXECUTION_JUPYTER_URL, + "CODE_EXECUTION_JUPYTER_AUTH": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH, + "CODE_EXECUTION_JUPYTER_AUTH_TOKEN": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN, + "CODE_EXECUTION_JUPYTER_AUTH_PASSWORD": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD, "ENABLE_CODE_INTERPRETER": request.app.state.config.ENABLE_CODE_INTERPRETER, "CODE_INTERPRETER_ENGINE": request.app.state.config.CODE_INTERPRETER_ENGINE, "CODE_INTERPRETER_PROMPT_TEMPLATE": request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE, diff --git a/backend/open_webui/routers/ollama.py b/backend/open_webui/routers/ollama.py index 64373c616..4fca10e1f 100644 --- a/backend/open_webui/routers/ollama.py +++ b/backend/open_webui/routers/ollama.py @@ -944,7 +944,7 @@ class ChatMessage(BaseModel): class GenerateChatCompletionForm(BaseModel): model: str messages: list[ChatMessage] - format: Optional[dict] = None + format: Optional[Union[dict, str]] = None options: Optional[dict] = None template: Optional[str] = None stream: Optional[bool] = True diff --git a/backend/open_webui/routers/pipelines.py b/backend/open_webui/routers/pipelines.py index 062663671..ad280b65c 100644 --- a/backend/open_webui/routers/pipelines.py +++ b/backend/open_webui/routers/pipelines.py @@ -9,6 +9,7 @@ from fastapi import ( status, APIRouter, ) +import aiohttp import os import logging import shutil @@ -56,96 +57,103 @@ def get_sorted_filters(model_id, models): return sorted_filters -def process_pipeline_inlet_filter(request, payload, user, models): +async def process_pipeline_inlet_filter(request, payload, user, models): user = {"id": user.id, "email": user.email, "name": user.name, "role": user.role} model_id = payload["model"] - sorted_filters = get_sorted_filters(model_id, models) model = models[model_id] if "pipeline" in model: sorted_filters.append(model) - for filter in sorted_filters: - r = None - try: - urlIdx = filter["urlIdx"] + async with aiohttp.ClientSession() as session: + for filter in sorted_filters: + urlIdx = filter.get("urlIdx") + if urlIdx is None: + continue url = request.app.state.config.OPENAI_API_BASE_URLS[urlIdx] key = request.app.state.config.OPENAI_API_KEYS[urlIdx] - if key == "": + if not key: continue headers = {"Authorization": f"Bearer {key}"} - r = requests.post( - f"{url}/{filter['id']}/filter/inlet", - headers=headers, - json={ - "user": user, - "body": payload, - }, - ) + request_data = { + "user": user, + "body": payload, + } - r.raise_for_status() - payload = r.json() - except Exception as e: - # Handle connection error here - print(f"Connection error: {e}") - - if r is not None: - res = r.json() + try: + async with session.post( + f"{url}/{filter['id']}/filter/inlet", + headers=headers, + json=request_data, + ) as response: + response.raise_for_status() + payload = await response.json() + except aiohttp.ClientResponseError as e: + res = ( + await response.json() + if response.content_type == "application/json" + else {} + ) if "detail" in res: - raise Exception(r.status_code, res["detail"]) + raise Exception(response.status, res["detail"]) + except Exception as e: + print(f"Connection error: {e}") return payload -def process_pipeline_outlet_filter(request, payload, user, models): +async def process_pipeline_outlet_filter(request, payload, user, models): user = {"id": user.id, "email": user.email, "name": user.name, "role": user.role} model_id = payload["model"] - sorted_filters = get_sorted_filters(model_id, models) model = models[model_id] if "pipeline" in model: sorted_filters = [model] + sorted_filters - for filter in sorted_filters: - r = None - try: - urlIdx = filter["urlIdx"] + async with aiohttp.ClientSession() as session: + for filter in sorted_filters: + urlIdx = filter.get("urlIdx") + if urlIdx is None: + continue url = request.app.state.config.OPENAI_API_BASE_URLS[urlIdx] key = request.app.state.config.OPENAI_API_KEYS[urlIdx] - if key != "": - r = requests.post( + if not key: + continue + + headers = {"Authorization": f"Bearer {key}"} + request_data = { + "user": user, + "body": payload, + } + + try: + async with session.post( f"{url}/{filter['id']}/filter/outlet", - headers={"Authorization": f"Bearer {key}"}, - json={ - "user": user, - "body": payload, - }, - ) - - r.raise_for_status() - data = r.json() - payload = data - except Exception as e: - # Handle connection error here - print(f"Connection error: {e}") - - if r is not None: + headers=headers, + json=request_data, + ) as response: + response.raise_for_status() + payload = await response.json() + except aiohttp.ClientResponseError as e: try: - res = r.json() + res = ( + await response.json() + if "application/json" in response.content_type + else {} + ) if "detail" in res: - return Exception(r.status_code, res) + raise Exception(response.status, res) except Exception: pass - - else: - pass + except Exception as e: + print(f"Connection error: {e}") return payload diff --git a/backend/open_webui/routers/retrieval.py b/backend/open_webui/routers/retrieval.py index 415d3bbb5..71eec6a68 100644 --- a/backend/open_webui/routers/retrieval.py +++ b/backend/open_webui/routers/retrieval.py @@ -21,6 +21,7 @@ from fastapi import ( APIRouter, ) from fastapi.middleware.cors import CORSMiddleware +from fastapi.concurrency import run_in_threadpool from pydantic import BaseModel import tiktoken @@ -370,7 +371,8 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)): "proxy_url": request.app.state.config.YOUTUBE_LOADER_PROXY_URL, }, "web": { - "web_loader_ssl_verification": request.app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION, + "ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION": request.app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION, + "RAG_WEB_SEARCH_FULL_CONTEXT": request.app.state.config.RAG_WEB_SEARCH_FULL_CONTEXT, "search": { "enabled": request.app.state.config.ENABLE_RAG_WEB_SEARCH, "drive": request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION, @@ -450,12 +452,14 @@ class WebSearchConfig(BaseModel): exa_api_key: Optional[str] = None result_count: Optional[int] = None concurrent_requests: Optional[int] = None + trust_env: Optional[bool] = None domain_filter_list: Optional[List[str]] = [] class WebConfig(BaseModel): search: WebSearchConfig - web_loader_ssl_verification: Optional[bool] = None + ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION: Optional[bool] = None + RAG_WEB_SEARCH_FULL_CONTEXT: Optional[bool] = None class ConfigUpdateForm(BaseModel): @@ -510,11 +514,16 @@ async def update_rag_config( if form_data.web is not None: request.app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = ( # Note: When UI "Bypass SSL verification for Websites"=True then ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION=False - form_data.web.web_loader_ssl_verification + form_data.web.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION ) request.app.state.config.ENABLE_RAG_WEB_SEARCH = form_data.web.search.enabled request.app.state.config.RAG_WEB_SEARCH_ENGINE = form_data.web.search.engine + + request.app.state.config.RAG_WEB_SEARCH_FULL_CONTEXT = ( + form_data.web.RAG_WEB_SEARCH_FULL_CONTEXT + ) + request.app.state.config.SEARXNG_QUERY_URL = ( form_data.web.search.searxng_query_url ) @@ -569,6 +578,9 @@ async def update_rag_config( request.app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS = ( form_data.web.search.concurrent_requests ) + request.app.state.config.RAG_WEB_SEARCH_TRUST_ENV = ( + form_data.web.search.trust_env + ) request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST = ( form_data.web.search.domain_filter_list ) @@ -595,7 +607,8 @@ async def update_rag_config( "translation": request.app.state.YOUTUBE_LOADER_TRANSLATION, }, "web": { - "web_loader_ssl_verification": request.app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION, + "ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION": request.app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION, + "RAG_WEB_SEARCH_FULL_CONTEXT": request.app.state.config.RAG_WEB_SEARCH_FULL_CONTEXT, "search": { "enabled": request.app.state.config.ENABLE_RAG_WEB_SEARCH, "engine": request.app.state.config.RAG_WEB_SEARCH_ENGINE, @@ -621,6 +634,7 @@ async def update_rag_config( "exa_api_key": request.app.state.config.EXA_API_KEY, "result_count": request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT, "concurrent_requests": request.app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS, + "trust_env": request.app.state.config.RAG_WEB_SEARCH_TRUST_ENV, "domain_filter_list": request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST, }, }, @@ -1256,6 +1270,7 @@ def search_web(request: Request, engine: str, query: str) -> list[SearchResult]: request.app.state.config.TAVILY_API_KEY, query, request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT, + request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST, ) else: raise Exception("No TAVILY_API_KEY found in environment variables") @@ -1308,7 +1323,7 @@ def search_web(request: Request, engine: str, query: str) -> list[SearchResult]: @router.post("/process/web/search") -def process_web_search( +async def process_web_search( request: Request, form_data: SearchForm, user=Depends(get_verified_user) ): try: @@ -1340,17 +1355,39 @@ def process_web_search( urls, verify_ssl=request.app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION, requests_per_second=request.app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS, + trust_env=request.app.state.config.RAG_WEB_SEARCH_TRUST_ENV, ) - docs = loader.load() - save_docs_to_vector_db( - request, docs, collection_name, overwrite=True, user=user - ) + docs = await loader.aload() - return { - "status": True, - "collection_name": collection_name, - "filenames": urls, - } + if request.app.state.config.RAG_WEB_SEARCH_FULL_CONTEXT: + return { + "status": True, + "docs": [ + { + "content": doc.page_content, + "metadata": doc.metadata, + } + for doc in docs + ], + "filenames": urls, + "loaded_count": len(docs), + } + else: + await run_in_threadpool( + save_docs_to_vector_db, + request, + docs, + collection_name, + overwrite=True, + user=user, + ) + + return { + "status": True, + "collection_name": collection_name, + "filenames": urls, + "loaded_count": len(docs), + } except Exception as e: log.exception(e) raise HTTPException( diff --git a/backend/open_webui/routers/tasks.py b/backend/open_webui/routers/tasks.py index 8b17c6c4b..0328cefe0 100644 --- a/backend/open_webui/routers/tasks.py +++ b/backend/open_webui/routers/tasks.py @@ -208,7 +208,7 @@ async def generate_title( "stream": False, **( {"max_tokens": 1000} - if models[task_model_id]["owned_by"] == "ollama" + if models[task_model_id].get("owned_by") == "ollama" else { "max_completion_tokens": 1000, } @@ -571,7 +571,7 @@ async def generate_emoji( "stream": False, **( {"max_tokens": 4} - if models[task_model_id]["owned_by"] == "ollama" + if models[task_model_id].get("owned_by") == "ollama" else { "max_completion_tokens": 4, } diff --git a/backend/open_webui/routers/utils.py b/backend/open_webui/routers/utils.py index ea73e9759..61863bda5 100644 --- a/backend/open_webui/routers/utils.py +++ b/backend/open_webui/routers/utils.py @@ -4,45 +4,75 @@ import markdown from open_webui.models.chats import ChatTitleMessagesForm from open_webui.config import DATA_DIR, ENABLE_ADMIN_EXPORT from open_webui.constants import ERROR_MESSAGES -from fastapi import APIRouter, Depends, HTTPException, Response, status +from fastapi import APIRouter, Depends, HTTPException, Request, Response, status from pydantic import BaseModel from starlette.responses import FileResponse + + from open_webui.utils.misc import get_gravatar_url from open_webui.utils.pdf_generator import PDFGenerator -from open_webui.utils.auth import get_admin_user +from open_webui.utils.auth import get_admin_user, get_verified_user +from open_webui.utils.code_interpreter import execute_code_jupyter + router = APIRouter() @router.get("/gravatar") -async def get_gravatar( - email: str, -): +async def get_gravatar(email: str, user=Depends(get_verified_user)): return get_gravatar_url(email) -class CodeFormatRequest(BaseModel): +class CodeForm(BaseModel): code: str @router.post("/code/format") -async def format_code(request: CodeFormatRequest): +async def format_code(form_data: CodeForm, user=Depends(get_verified_user)): try: - formatted_code = black.format_str(request.code, mode=black.Mode()) + formatted_code = black.format_str(form_data.code, mode=black.Mode()) return {"code": formatted_code} except black.NothingChanged: - return {"code": request.code} + return {"code": form_data.code} except Exception as e: raise HTTPException(status_code=400, detail=str(e)) +@router.post("/code/execute") +async def execute_code( + request: Request, form_data: CodeForm, user=Depends(get_verified_user) +): + if request.app.state.config.CODE_EXECUTION_ENGINE == "jupyter": + output = await execute_code_jupyter( + request.app.state.config.CODE_EXECUTION_JUPYTER_URL, + form_data.code, + ( + request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN + if request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH == "token" + else None + ), + ( + request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD + if request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH == "password" + else None + ), + ) + + return output + else: + raise HTTPException( + status_code=400, + detail="Code execution engine not supported", + ) + + class MarkdownForm(BaseModel): md: str @router.post("/markdown") async def get_html_from_markdown( - form_data: MarkdownForm, + form_data: MarkdownForm, user=Depends(get_verified_user) ): return {"html": markdown.markdown(form_data.md)} @@ -54,7 +84,7 @@ class ChatForm(BaseModel): @router.post("/pdf") async def download_chat_as_pdf( - form_data: ChatTitleMessagesForm, + form_data: ChatTitleMessagesForm, user=Depends(get_verified_user) ): try: pdf_bytes = PDFGenerator(form_data).generate_chat_pdf() diff --git a/backend/open_webui/static/loader.js b/backend/open_webui/static/loader.js new file mode 100644 index 000000000..e69de29bb diff --git a/backend/open_webui/static/swagger-ui/swagger-ui.css b/backend/open_webui/static/swagger-ui/swagger-ui.css index 749dba8c9..4efa5efee 100644 --- a/backend/open_webui/static/swagger-ui/swagger-ui.css +++ b/backend/open_webui/static/swagger-ui/swagger-ui.css @@ -9308,5 +9308,3 @@ .json-schema-2020-12__title:first-of-type { font-size: 16px; } - -/*# sourceMappingURL=swagger-ui.css.map*/ diff --git a/backend/open_webui/utils/auth.py b/backend/open_webui/utils/auth.py index 3a0490960..e4ad27c84 100644 --- a/backend/open_webui/utils/auth.py +++ b/backend/open_webui/utils/auth.py @@ -1,6 +1,11 @@ import logging import uuid import jwt +import base64 +import hmac +import hashlib +import requests + from datetime import UTC, datetime, timedelta from typing import Optional, Union, List, Dict @@ -8,7 +13,8 @@ from typing import Optional, Union, List, Dict from open_webui.models.users import Users from open_webui.constants import ERROR_MESSAGES -from open_webui.env import WEBUI_SECRET_KEY +from open_webui.config import override_static +from open_webui.env import WEBUI_SECRET_KEY, TRUSTED_SIGNATURE_KEY from fastapi import Depends, HTTPException, Request, Response, status from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer @@ -24,6 +30,53 @@ ALGORITHM = "HS256" # Auth Utils ############## + +def verify_signature(payload: str, signature: str) -> bool: + """ + Verifies the HMAC signature of the received payload. + """ + try: + expected_signature = base64.b64encode( + hmac.new(TRUSTED_SIGNATURE_KEY, payload.encode(), hashlib.sha256).digest() + ).decode() + + # Compare securely to prevent timing attacks + return hmac.compare_digest(expected_signature, signature) + + except Exception: + return False + + +def get_license_data(app, key): + if key: + try: + res = requests.post( + "https://api.openwebui.com/api/v1/license", + json={"key": key, "version": "1"}, + timeout=5, + ) + + if getattr(res, "ok", False): + payload = getattr(res, "json", lambda: {})() + for k, v in payload.items(): + if k == "resources": + for p, c in v.items(): + globals().get("override_static", lambda a, b: None)(p, c) + elif k == "user_count": + setattr(app.state, "USER_COUNT", v) + elif k == "webui_name": + setattr(app.state, "WEBUI_NAME", v) + + return True + else: + print( + f"License: retrieval issue: {getattr(res, 'text', 'unknown error')}" + ) + except Exception as ex: + print(f"License: Uncaught Exception: {ex}") + return False + + bearer_security = HTTPBearer(auto_error=False) pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") diff --git a/backend/open_webui/utils/chat.py b/backend/open_webui/utils/chat.py index 569bcad85..209fb02dc 100644 --- a/backend/open_webui/utils/chat.py +++ b/backend/open_webui/utils/chat.py @@ -186,12 +186,6 @@ async def generate_chat_completion( if model_id not in models: raise Exception("Model not found") - # Process the form_data through the pipeline - try: - form_data = process_pipeline_inlet_filter(request, form_data, user, models) - except Exception as e: - raise e - model = models[model_id] if getattr(request.state, "direct", False): @@ -206,7 +200,7 @@ async def generate_chat_completion( except Exception as e: raise e - if model["owned_by"] == "arena": + if model.get("owned_by") == "arena": model_ids = model.get("info", {}).get("meta", {}).get("model_ids") filter_mode = model.get("info", {}).get("meta", {}).get("filter_mode") if model_ids and filter_mode == "exclude": @@ -259,7 +253,7 @@ async def generate_chat_completion( return await generate_function_chat_completion( request, form_data, user=user, models=models ) - if model["owned_by"] == "ollama": + if model.get("owned_by") == "ollama": # Using /ollama/api/chat endpoint form_data = convert_payload_openai_to_ollama(form_data) response = await generate_ollama_chat_completion( @@ -308,7 +302,7 @@ async def chat_completed(request: Request, form_data: dict, user: Any): model = models[model_id] try: - data = process_pipeline_outlet_filter(request, data, user, models) + data = await process_pipeline_outlet_filter(request, data, user, models) except Exception as e: return Exception(f"Error: {e}") diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index 4e4ba8d30..93edc8f72 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -39,7 +39,10 @@ from open_webui.routers.tasks import ( ) from open_webui.routers.retrieval import process_web_search, SearchForm from open_webui.routers.images import image_generations, GenerateImageForm - +from open_webui.routers.pipelines import ( + process_pipeline_inlet_filter, + process_pipeline_outlet_filter, +) from open_webui.utils.webhook import post_webhook @@ -334,21 +337,15 @@ async def chat_web_search_handler( try: - # Offload process_web_search to a separate thread - loop = asyncio.get_running_loop() - with ThreadPoolExecutor() as executor: - results = await loop.run_in_executor( - executor, - lambda: process_web_search( - request, - SearchForm( - **{ - "query": searchQuery, - } - ), - user, - ), - ) + results = await process_web_search( + request, + SearchForm( + **{ + "query": searchQuery, + } + ), + user, + ) if results: await event_emitter( @@ -365,14 +362,25 @@ async def chat_web_search_handler( ) files = form_data.get("files", []) - files.append( - { - "collection_name": results["collection_name"], - "name": searchQuery, - "type": "web_search_results", - "urls": results["filenames"], - } - ) + + if request.app.state.config.RAG_WEB_SEARCH_FULL_CONTEXT: + files.append( + { + "docs": results.get("docs", []), + "name": searchQuery, + "type": "web_search_docs", + "urls": results["filenames"], + } + ) + else: + files.append( + { + "collection_name": results["collection_name"], + "name": searchQuery, + "type": "web_search_results", + "urls": results["filenames"], + } + ) form_data["files"] = files else: await event_emitter( @@ -682,6 +690,25 @@ async def process_chat_payload(request, form_data, metadata, user, model): variables = form_data.pop("variables", None) + # Process the form_data through the pipeline + try: + form_data = await process_pipeline_inlet_filter( + request, form_data, user, models + ) + except Exception as e: + raise e + + try: + form_data, flags = await process_filter_functions( + request=request, + filter_ids=get_sorted_filter_ids(model), + filter_type="inlet", + form_data=form_data, + extra_params=extra_params, + ) + except Exception as e: + raise Exception(f"Error: {e}") + features = form_data.pop("features", None) if features: if "web_search" in features and features["web_search"]: @@ -704,17 +731,6 @@ async def process_chat_payload(request, form_data, metadata, user, model): form_data["messages"], ) - try: - form_data, flags = await process_filter_functions( - request=request, - filter_ids=get_sorted_filter_ids(model), - filter_type="inlet", - form_data=form_data, - extra_params=extra_params, - ) - except Exception as e: - raise Exception(f"Error: {e}") - tool_ids = form_data.pop("tool_ids", None) files = form_data.pop("files", None) # Remove files duplicates @@ -778,7 +794,7 @@ async def process_chat_payload(request, form_data, metadata, user, model): if "document" in source: for doc_idx, doc_context in enumerate(source["document"]): - context_string += f"{doc_idx}{doc_context}\n" + context_string += f"{source_idx}{doc_context}\n" context_string = context_string.strip() prompt = get_last_user_message(form_data["messages"]) @@ -795,7 +811,7 @@ async def process_chat_payload(request, form_data, metadata, user, model): # Workaround for Ollama 2.0+ system prompt issue # TODO: replace with add_or_update_system_message - if model["owned_by"] == "ollama": + if model.get("owned_by") == "ollama": form_data["messages"] = prepend_to_first_user_message_content( rag_template( request.app.state.config.RAG_TEMPLATE, context_string, prompt @@ -1003,6 +1019,7 @@ async def process_chat_response( webhook_url = Users.get_user_webhook_url_by_id(user.id) if webhook_url: post_webhook( + request.app.state.WEBUI_NAME, webhook_url, f"{title} - {request.app.state.config.WEBUI_URL}/c/{metadata['chat_id']}\n\n{content}", { @@ -1341,7 +1358,14 @@ async def process_chat_response( ) tool_calls = [] - content = message.get("content", "") if message else "" + + last_assistant_message = get_last_assistant_message(form_data["messages"]) + content = ( + message.get("content", "") + if message + else last_assistant_message if last_assistant_message else "" + ) + content_blocks = [ { "type": "text", @@ -1868,6 +1892,7 @@ async def process_chat_response( webhook_url = Users.get_user_webhook_url_by_id(user.id) if webhook_url: post_webhook( + request.app.state.WEBUI_NAME, webhook_url, f"{title} - {request.app.state.config.WEBUI_URL}/c/{metadata['chat_id']}\n\n{content}", { diff --git a/backend/open_webui/utils/models.py b/backend/open_webui/utils/models.py index 975f8cb09..a2c0eadca 100644 --- a/backend/open_webui/utils/models.py +++ b/backend/open_webui/utils/models.py @@ -142,7 +142,7 @@ async def get_all_models(request): custom_model.base_model_id == model["id"] or custom_model.base_model_id == model["id"].split(":")[0] ): - owned_by = model["owned_by"] + owned_by = model.get("owned_by", "unknown owner") if "pipe" in model: pipe = model["pipe"] break diff --git a/backend/open_webui/utils/oauth.py b/backend/open_webui/utils/oauth.py index 463f67adc..a635853d6 100644 --- a/backend/open_webui/utils/oauth.py +++ b/backend/open_webui/utils/oauth.py @@ -36,7 +36,11 @@ from open_webui.config import ( AppConfig, ) from open_webui.constants import ERROR_MESSAGES, WEBHOOK_MESSAGES -from open_webui.env import WEBUI_AUTH_COOKIE_SAME_SITE, WEBUI_AUTH_COOKIE_SECURE +from open_webui.env import ( + WEBUI_NAME, + WEBUI_AUTH_COOKIE_SAME_SITE, + WEBUI_AUTH_COOKIE_SECURE, +) from open_webui.utils.misc import parse_duration from open_webui.utils.auth import get_password_hash, create_token from open_webui.utils.webhook import post_webhook @@ -66,8 +70,9 @@ auth_manager_config.JWT_EXPIRES_IN = JWT_EXPIRES_IN class OAuthManager: - def __init__(self): + def __init__(self, app): self.oauth = OAuth() + self.app = app for _, provider_config in OAUTH_PROVIDERS.items(): provider_config["register"](self.oauth) @@ -200,7 +205,7 @@ class OAuthManager: id=group_model.id, form_data=update_form, overwrite=False ) - async def handle_login(self, provider, request): + async def handle_login(self, request, provider): if provider not in OAUTH_PROVIDERS: raise HTTPException(404) # If the provider has a custom redirect URL, use that, otherwise automatically generate one @@ -212,7 +217,7 @@ class OAuthManager: raise HTTPException(404) return await client.authorize_redirect(request, redirect_uri) - async def handle_callback(self, provider, request, response): + async def handle_callback(self, request, provider, response): if provider not in OAUTH_PROVIDERS: raise HTTPException(404) client = self.get_client(provider) @@ -266,6 +271,17 @@ class OAuthManager: Users.update_user_role_by_id(user.id, determined_role) if not user: + user_count = Users.get_num_users() + + if ( + request.app.state.USER_COUNT + and user_count >= request.app.state.USER_COUNT + ): + raise HTTPException( + 403, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + # If the user does not exist, check if signups are enabled if auth_manager_config.ENABLE_OAUTH_SIGNUP: # Check if an existing user with the same email already exists @@ -334,6 +350,7 @@ class OAuthManager: if auth_manager_config.WEBHOOK_URL: post_webhook( + WEBUI_NAME, auth_manager_config.WEBHOOK_URL, WEBHOOK_MESSAGES.USER_SIGNUP(user.name), { @@ -380,6 +397,3 @@ class OAuthManager: # Redirect back to the frontend with the JWT token redirect_url = f"{request.base_url}auth#token={jwt_token}" return RedirectResponse(url=redirect_url, headers=response.headers) - - -oauth_manager = OAuthManager() diff --git a/backend/open_webui/utils/task.py b/backend/open_webui/utils/task.py index 3d8c05d45..5663ce2ac 100644 --- a/backend/open_webui/utils/task.py +++ b/backend/open_webui/utils/task.py @@ -22,7 +22,7 @@ def get_task_model_id( # Set the task model task_model_id = default_model_id # Check if the user has a custom task model and use that model - if models[task_model_id]["owned_by"] == "ollama": + if models[task_model_id].get("owned_by") == "ollama": if task_model and task_model in models: task_model_id = task_model else: diff --git a/backend/open_webui/utils/webhook.py b/backend/open_webui/utils/webhook.py index d59244dd3..bf0b334d8 100644 --- a/backend/open_webui/utils/webhook.py +++ b/backend/open_webui/utils/webhook.py @@ -2,14 +2,14 @@ import json import logging import requests -from open_webui.config import WEBUI_FAVICON_URL, WEBUI_NAME +from open_webui.config import WEBUI_FAVICON_URL from open_webui.env import SRC_LOG_LEVELS, VERSION log = logging.getLogger(__name__) log.setLevel(SRC_LOG_LEVELS["WEBHOOK"]) -def post_webhook(url: str, message: str, event_data: dict) -> bool: +def post_webhook(name: str, url: str, message: str, event_data: dict) -> bool: try: log.debug(f"post_webhook: {url}, {message}, {event_data}") payload = {} @@ -39,7 +39,7 @@ def post_webhook(url: str, message: str, event_data: dict) -> bool: "sections": [ { "activityTitle": message, - "activitySubtitle": f"{WEBUI_NAME} ({VERSION}) - {action}", + "activitySubtitle": f"{name} ({VERSION}) - {action}", "activityImage": WEBUI_FAVICON_URL, "facts": facts, "markdown": True, diff --git a/package-lock.json b/package-lock.json index 56a76c09c..c3e867d2c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "open-webui", - "version": "0.5.12", + "version": "0.5.13", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "open-webui", - "version": "0.5.12", + "version": "0.5.13", "dependencies": { "@codemirror/lang-javascript": "^6.2.2", "@codemirror/lang-python": "^6.1.6", @@ -72,10 +72,10 @@ "@sveltejs/kit": "^2.5.20", "@sveltejs/vite-plugin-svelte": "^3.1.1", "@tailwindcss/container-queries": "^0.1.1", + "@tailwindcss/postcss": "^4.0.0", "@tailwindcss/typography": "^0.5.13", "@typescript-eslint/eslint-plugin": "^6.17.0", "@typescript-eslint/parser": "^6.17.0", - "autoprefixer": "^10.4.16", "cypress": "^13.15.0", "eslint": "^8.56.0", "eslint-config-prettier": "^9.1.0", @@ -89,7 +89,7 @@ "svelte": "^4.2.18", "svelte-check": "^3.8.5", "svelte-confetti": "^1.3.2", - "tailwindcss": "^3.3.3", + "tailwindcss": "^4.0.0", "tslib": "^2.4.1", "typescript": "^5.5.4", "vite": "^5.4.14", @@ -2368,6 +2368,260 @@ "tailwindcss": ">=3.2.0" } }, + "node_modules/@tailwindcss/node": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.0.6.tgz", + "integrity": "sha512-jb6E0WeSq7OQbVYcIJ6LxnZTeC4HjMvbzFBMCrQff4R50HBlo/obmYNk6V2GCUXDeqiXtvtrQgcIbT+/boB03Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "enhanced-resolve": "^5.18.0", + "jiti": "^2.4.2", + "tailwindcss": "4.0.6" + } + }, + "node_modules/@tailwindcss/node/node_modules/jiti": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", + "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/@tailwindcss/node/node_modules/tailwindcss": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.6.tgz", + "integrity": "sha512-mysewHYJKaXgNOW6pp5xon/emCsfAMnO8WMaGKZZ35fomnR/T5gYnRg2/yRTTrtXiEl1tiVkeRt0eMO6HxEZqw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.0.6.tgz", + "integrity": "sha512-lVyKV2y58UE9CeKVcYykULe9QaE1dtKdxDEdrTPIdbzRgBk6bdxHNAoDqvcqXbIGXubn3VOl1O/CFF77v/EqSA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.0.6", + "@tailwindcss/oxide-darwin-arm64": "4.0.6", + "@tailwindcss/oxide-darwin-x64": "4.0.6", + "@tailwindcss/oxide-freebsd-x64": "4.0.6", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.0.6", + "@tailwindcss/oxide-linux-arm64-gnu": "4.0.6", + "@tailwindcss/oxide-linux-arm64-musl": "4.0.6", + "@tailwindcss/oxide-linux-x64-gnu": "4.0.6", + "@tailwindcss/oxide-linux-x64-musl": "4.0.6", + "@tailwindcss/oxide-win32-arm64-msvc": "4.0.6", + "@tailwindcss/oxide-win32-x64-msvc": "4.0.6" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.0.6.tgz", + "integrity": "sha512-xDbym6bDPW3D2XqQqX3PjqW3CKGe1KXH7Fdkc60sX5ZLVUbzPkFeunQaoP+BuYlLc2cC1FoClrIRYnRzof9Sow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.0.6.tgz", + "integrity": "sha512-1f71/ju/tvyGl5c2bDkchZHy8p8EK/tDHCxlpYJ1hGNvsYihZNurxVpZ0DefpN7cNc9RTT8DjrRoV8xXZKKRjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.0.6.tgz", + "integrity": "sha512-s/hg/ZPgxFIrGMb0kqyeaqZt505P891buUkSezmrDY6lxv2ixIELAlOcUVTkVh245SeaeEiUVUPiUN37cwoL2g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.0.6.tgz", + "integrity": "sha512-Z3Wo8FWZnmio8+xlcbb7JUo/hqRMSmhQw8IGIRoRJ7GmLR0C+25Wq+bEX/135xe/yEle2lFkhu9JBHd4wZYiig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.0.6.tgz", + "integrity": "sha512-SNSwkkim1myAgmnbHs4EjXsPL7rQbVGtjcok5EaIzkHkCAVK9QBQsWeP2Jm2/JJhq4wdx8tZB9Y7psMzHYWCkA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.0.6.tgz", + "integrity": "sha512-tJ+mevtSDMQhKlwCCuhsFEFg058kBiSy4TkoeBG921EfrHKmexOaCyFKYhVXy4JtkaeeOcjJnCLasEeqml4i+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.0.6.tgz", + "integrity": "sha512-IoArz1vfuTR4rALXMUXI/GWWfx2EaO4gFNtBNkDNOYhlTD4NVEwE45nbBoojYiTulajI4c2XH8UmVEVJTOJKxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.0.6.tgz", + "integrity": "sha512-QtsUfLkEAeWAC3Owx9Kg+7JdzE+k9drPhwTAXbXugYB9RZUnEWWx5x3q/au6TvUYcL+n0RBqDEO2gucZRvRFgQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.0.6.tgz", + "integrity": "sha512-QthvJqIji2KlGNwLcK/PPYo7w1Wsi/8NK0wAtRGbv4eOPdZHkQ9KUk+oCoP20oPO7i2a6X1aBAFQEL7i08nNMA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.0.6.tgz", + "integrity": "sha512-+oka+dYX8jy9iP00DJ9Y100XsqvbqR5s0yfMZJuPR1H/lDVtDfsZiSix1UFBQ3X1HWxoEEl6iXNJHWd56TocVw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.0.6.tgz", + "integrity": "sha512-+o+juAkik4p8Ue/0LiflQXPmVatl6Av3LEZXpBTfg4qkMIbZdhCGWFzHdt2NjoMiLOJCFDddoV6GYaimvK1Olw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.0.0.tgz", + "integrity": "sha512-lI2bPk4TvwavHdehjr5WiC6HnZ59hacM6ySEo4RM/H7tsjWd8JpqiNW9ThH7rO/yKtrn4mGBoXshpvn8clXjPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "^4.0.0", + "@tailwindcss/oxide": "^4.0.0", + "lightningcss": "^1.29.1", + "postcss": "^8.4.41", + "tailwindcss": "4.0.0" + } + }, "node_modules/@tailwindcss/typography": { "version": "0.5.13", "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.13.tgz", @@ -3428,12 +3682,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true - }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -3466,12 +3714,6 @@ } ] }, - "node_modules/arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "dev": true - }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -3551,43 +3793,6 @@ "node": ">= 4.0.0" } }, - "node_modules/autoprefixer": { - "version": "10.4.19", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", - "integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "browserslist": "^4.23.0", - "caniuse-lite": "^1.0.30001599", - "fraction.js": "^4.3.7", - "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, "node_modules/aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", @@ -3827,38 +4032,6 @@ "node": "10.* || >= 12.*" } }, - "node_modules/browserslist": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", - "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "caniuse-lite": "^1.0.30001587", - "electron-to-chromium": "^1.4.668", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.13" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, "node_modules/buffer": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", @@ -3957,35 +4130,6 @@ "node": ">=6" } }, - "node_modules/camelcase-css": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001600", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001600.tgz", - "integrity": "sha512-+2S9/2JFhYmYaDpZvo0lKkfvuKIglrx68MwOBqMGHhQsNkLjB5xtc/TGoEPs+MxjSyN/72qer2g97nzR641mOQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ] - }, "node_modules/caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", @@ -5261,12 +5405,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true - }, "node_modules/diff": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", @@ -5297,12 +5435,6 @@ "node": ">=8" } }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true - }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -5390,12 +5522,6 @@ "safer-buffer": "^2.1.0" } }, - "node_modules/electron-to-chromium": { - "version": "1.4.715", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.715.tgz", - "integrity": "sha512-XzWNH4ZSa9BwVUQSDorPWAUQ5WGuYz7zJUNpNif40zFCiCl20t8zgylmreNmn26h5kiyw2lg7RfTmeMBsDklqg==", - "dev": true - }, "node_modules/elkjs": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.9.3.tgz", @@ -5435,6 +5561,20 @@ "node": ">=10.0.0" } }, + "node_modules/enhanced-resolve": { + "version": "5.18.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", + "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/enquirer": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", @@ -5536,15 +5676,6 @@ "@esbuild/win32-x64": "0.20.2" } }, - "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -6114,19 +6245,6 @@ "node": ">= 6" } }, - "node_modules/fraction.js": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", - "dev": true, - "engines": { - "node": "*" - }, - "funding": { - "type": "patreon", - "url": "https://github.com/sponsors/rawify" - } - }, "node_modules/fs-extra": { "version": "11.2.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", @@ -7024,33 +7142,6 @@ "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", "dev": true }, - "node_modules/jackspeak": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", - "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", - "dev": true, - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/jiti": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", - "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", - "dev": true, - "bin": { - "jiti": "bin/jiti.js" - } - }, "node_modules/js-sha256": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.10.1.tgz", @@ -7234,6 +7325,248 @@ "node": ">= 0.8.0" } }, + "node_modules/lightningcss": { + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.1.tgz", + "integrity": "sha512-FmGoeD4S05ewj+AkhTY+D+myDvXI6eL27FjHIjoyUkO/uw7WZD1fBVs0QxeYWa7E17CUHJaYX/RUGISCtcrG4Q==", + "devOptional": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^1.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.29.1", + "lightningcss-darwin-x64": "1.29.1", + "lightningcss-freebsd-x64": "1.29.1", + "lightningcss-linux-arm-gnueabihf": "1.29.1", + "lightningcss-linux-arm64-gnu": "1.29.1", + "lightningcss-linux-arm64-musl": "1.29.1", + "lightningcss-linux-x64-gnu": "1.29.1", + "lightningcss-linux-x64-musl": "1.29.1", + "lightningcss-win32-arm64-msvc": "1.29.1", + "lightningcss-win32-x64-msvc": "1.29.1" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.29.1.tgz", + "integrity": "sha512-HtR5XJ5A0lvCqYAoSv2QdZZyoHNttBpa5EP9aNuzBQeKGfbyH5+UipLWvVzpP4Uml5ej4BYs5I9Lco9u1fECqw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.29.1.tgz", + "integrity": "sha512-k33G9IzKUpHy/J/3+9MCO4e+PzaFblsgBjSGlpAaFikeBFm8B/CkO3cKU9oI4g+fjS2KlkLM/Bza9K/aw8wsNA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.29.1.tgz", + "integrity": "sha512-0SUW22fv/8kln2LnIdOCmSuXnxgxVC276W5KLTwoehiO0hxkacBxjHOL5EtHD8BAXg2BvuhsJPmVMasvby3LiQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.29.1.tgz", + "integrity": "sha512-sD32pFvlR0kDlqsOZmYqH/68SqUMPNj+0pucGxToXZi4XZgZmqeX/NkxNKCPsswAXU3UeYgDSpGhu05eAufjDg==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.29.1.tgz", + "integrity": "sha512-0+vClRIZ6mmJl/dxGuRsE197o1HDEeeRk6nzycSy2GofC2JsY4ifCRnvUWf/CUBQmlrvMzt6SMQNMSEu22csWQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.29.1.tgz", + "integrity": "sha512-UKMFrG4rL/uHNgelBsDwJcBqVpzNJbzsKkbI3Ja5fg00sgQnHw/VrzUTEc4jhZ+AN2BvQYz/tkHu4vt1kLuJyw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.29.1.tgz", + "integrity": "sha512-u1S+xdODy/eEtjADqirA774y3jLcm8RPtYztwReEXoZKdzgsHYPl0s5V52Tst+GKzqjebkULT86XMSxejzfISw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.29.1.tgz", + "integrity": "sha512-L0Tx0DtaNUTzXv0lbGCLB/c/qEADanHbu4QdcNOXLIe1i8i22rZRpbT3gpWYsCh9aSL9zFujY/WmEXIatWvXbw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.29.1.tgz", + "integrity": "sha512-QoOVnkIEFfbW4xPi+dpdft/zAKmgLgsRHfJalEPYuJDOWf7cLQzYg0DEh8/sn737FaeMJxHZRc1oBreiwZCjog==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.29.1.tgz", + "integrity": "sha512-NygcbThNBe4JElP+olyTI/doBNGJvLs3bFCRPdvuCcxZCcCZ71B858IHpdm7L1btZex0FvCmM17FK98Y9MRy1Q==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss/node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/lilconfig": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz", @@ -7246,12 +7579,6 @@ "url": "https://github.com/sponsors/antonk52" } }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true - }, "node_modules/linkify-it": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", @@ -8305,17 +8632,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, "node_modules/nanoid": { "version": "5.0.9", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.9.tgz", @@ -8345,12 +8661,6 @@ "resolved": "https://registry.npmjs.org/ngraph.events/-/ngraph.events-1.2.2.tgz", "integrity": "sha512-JsUbEOzANskax+WSYiAPETemLWYXmixuPAlmZmhIbIj6FH/WDgEGCGnRwUQBK0GjOnVm8Ui+e5IJ+5VZ4e32eQ==" }, - "node_modules/node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", - "dev": true - }, "node_modules/non-layered-tidy-tree-layout": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/non-layered-tidy-tree-layout/-/non-layered-tidy-tree-layout-2.0.2.tgz", @@ -8364,15 +8674,6 @@ "node": ">=0.10.0" } }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/now-and-later": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-3.0.0.tgz", @@ -8418,15 +8719,6 @@ "node": ">=0.10.0" } }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, "node_modules/object-inspect": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", @@ -8795,15 +9087,6 @@ "node": ">=0.10.0" } }, - "node_modules/pirates": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", - "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, "node_modules/pkg-types": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.1.1.tgz", @@ -8871,42 +9154,6 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/postcss-import": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", - "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-js": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", - "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", - "dev": true, - "dependencies": { - "camelcase-css": "^2.0.1" - }, - "engines": { - "node": "^12 || ^14 || >= 16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": "^8.4.21" - } - }, "node_modules/postcss-load-config": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", @@ -8945,38 +9192,6 @@ "node": ">=10" } }, - "node_modules/postcss-nested": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", - "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", - "dev": true, - "dependencies": { - "postcss-selector-parser": "^6.0.11" - }, - "engines": { - "node": ">=12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": "^8.2.14" - } - }, - "node_modules/postcss-nested/node_modules/postcss-selector-parser": { - "version": "6.0.16", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz", - "integrity": "sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==", - "dev": true, - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/postcss-safe-parser": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz", @@ -9032,12 +9247,6 @@ "node": ">=4" } }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, "node_modules/postcss/node_modules/nanoid": { "version": "3.3.8", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", @@ -9552,15 +9761,6 @@ "dev": true, "license": "MIT" }, - "node_modules/read-cache": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", - "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "dev": true, - "dependencies": { - "pify": "^2.3.0" - } - }, "node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", @@ -10764,59 +10964,6 @@ "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz", "integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==" }, - "node_modules/sucrase": { - "version": "3.35.0", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", - "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", - "dev": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "glob": "^10.3.10", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/sucrase/node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/sucrase/node_modules/glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", - "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -11053,121 +11200,20 @@ "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==" }, "node_modules/tailwindcss": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.1.tgz", - "integrity": "sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.0.tgz", + "integrity": "sha512-ULRPI3A+e39T7pSaf1xoi58AqqJxVCLg8F/uM5A3FadUbnyDTgltVnXJvdkTjwCOGA6NazqHVcwPJC5h2vRYVQ==", "dev": true, - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.5.3", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.3.0", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.19.1", - "lilconfig": "^2.1.0", - "micromatch": "^4.0.5", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.0.0", - "postcss": "^8.4.23", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.1", - "postcss-nested": "^6.0.1", - "postcss-selector-parser": "^6.0.11", - "resolve": "^1.22.2", - "sucrase": "^3.32.0" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } + "license": "MIT" }, - "node_modules/tailwindcss/node_modules/lilconfig": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", - "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", "dev": true, + "license": "MIT", "engines": { - "node": ">=10" - } - }, - "node_modules/tailwindcss/node_modules/postcss-load-config": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", - "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "lilconfig": "^3.0.0", - "yaml": "^2.3.4" - }, - "engines": { - "node": ">= 14" - }, - "peerDependencies": { - "postcss": ">=8.0.9", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "postcss": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/tailwindcss/node_modules/postcss-load-config/node_modules/lilconfig": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz", - "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==", - "dev": true, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, - "node_modules/tailwindcss/node_modules/postcss-selector-parser": { - "version": "6.0.16", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz", - "integrity": "sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==", - "dev": true, - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/tailwindcss/node_modules/yaml": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.1.tgz", - "integrity": "sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==", - "dev": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14" + "node": ">=6" } }, "node_modules/tar": { @@ -11217,27 +11263,6 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dev": true, - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dev": true, - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, "node_modules/throttleit": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.1.tgz", @@ -11395,12 +11420,6 @@ "node": ">=6.10" } }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "dev": true - }, "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", @@ -11543,36 +11562,6 @@ "node": ">=8" } }, - "node_modules/update-browserslist-db": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", - "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", diff --git a/package.json b/package.json index c1a76fd78..353a1b9f4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "open-webui", - "version": "0.5.12", + "version": "0.5.13", "private": true, "scripts": { "dev": "npm run pyodide:fetch && vite dev --host", @@ -26,10 +26,10 @@ "@sveltejs/kit": "^2.5.20", "@sveltejs/vite-plugin-svelte": "^3.1.1", "@tailwindcss/container-queries": "^0.1.1", + "@tailwindcss/postcss": "^4.0.0", "@tailwindcss/typography": "^0.5.13", "@typescript-eslint/eslint-plugin": "^6.17.0", "@typescript-eslint/parser": "^6.17.0", - "autoprefixer": "^10.4.16", "cypress": "^13.15.0", "eslint": "^8.56.0", "eslint-config-prettier": "^9.1.0", @@ -43,7 +43,7 @@ "svelte": "^4.2.18", "svelte-check": "^3.8.5", "svelte-confetti": "^1.3.2", - "tailwindcss": "^3.3.3", + "tailwindcss": "^4.0.0", "tslib": "^2.4.1", "typescript": "^5.5.4", "vite": "^5.4.14", diff --git a/postcss.config.js b/postcss.config.js index 0f7721681..85b958cb5 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -1,6 +1,5 @@ export default { plugins: { - tailwindcss: {}, - autoprefixer: {} + '@tailwindcss/postcss': {} } }; diff --git a/src/app.css b/src/app.css index dadfda78f..d324175b5 100644 --- a/src/app.css +++ b/src/app.css @@ -1,3 +1,5 @@ +@reference "./tailwind.css"; + @font-face { font-family: 'Inter'; src: url('/assets/fonts/Inter-Variable.ttf'); @@ -53,11 +55,11 @@ math { } .markdown-prose { - @apply prose dark:prose-invert prose-blockquote:border-gray-100 prose-blockquote:dark:border-gray-800 prose-blockquote:border-l-2 prose-blockquote:not-italic prose-blockquote:font-normal prose-headings:font-semibold prose-hr:my-4 prose-hr:border-gray-100 prose-hr:dark:border-gray-800 prose-p:my-0 prose-img:my-1 prose-headings:my-1 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-ul:-my-0 prose-ol:-my-0 prose-li:-my-0 whitespace-pre-line; + @apply prose dark:prose-invert prose-blockquote:border-s-gray-100 prose-blockquote:dark:border-gray-800 prose-blockquote:border-s-2 prose-blockquote:not-italic prose-blockquote:font-normal prose-headings:font-semibold prose-hr:my-4 prose-hr:border-gray-100 prose-hr:dark:border-gray-800 prose-p:my-0 prose-img:my-1 prose-headings:my-1 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-ul:-my-0 prose-ol:-my-0 prose-li:-my-0 whitespace-pre-line; } .markdown-prose-xs { - @apply text-xs prose dark:prose-invert prose-blockquote:border-gray-100 prose-blockquote:dark:border-gray-800 prose-blockquote:border-l-2 prose-blockquote:not-italic prose-blockquote:font-normal prose-headings:font-semibold prose-hr:my-0 prose-hr:border-gray-100 prose-hr:dark:border-gray-800 prose-p:my-0 prose-img:my-1 prose-headings:my-1 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-ul:-my-0 prose-ol:-my-0 prose-li:-my-0 whitespace-pre-line; + @apply text-xs prose dark:prose-invert prose-blockquote:border-s-gray-100 prose-blockquote:dark:border-gray-800 prose-blockquote:border-s-2 prose-blockquote:not-italic prose-blockquote:font-normal prose-headings:font-semibold prose-hr:my-0 prose-hr:border-gray-100 prose-hr:dark:border-gray-800 prose-p:my-0 prose-img:my-1 prose-headings:my-1 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-ul:-my-0 prose-ol:-my-0 prose-li:-my-0 whitespace-pre-line; } .markdown a { @@ -217,8 +219,18 @@ input[type='number'] { width: 100%; } -.cm-scroller { - @apply scrollbar-hidden; +.cm-scroller:active::-webkit-scrollbar-thumb, +.cm-scroller:focus::-webkit-scrollbar-thumb, +.cm-scroller:hover::-webkit-scrollbar-thumb { + visibility: visible; +} + +.cm-scroller::-webkit-scrollbar-thumb { + visibility: hidden; +} + +.cm-scroller::-webkit-scrollbar-corner { + display: none; } .cm-editor.cm-focused { diff --git a/src/app.html b/src/app.html index 537e28dbe..c8fbfca72 100644 --- a/src/app.html +++ b/src/app.html @@ -21,6 +21,7 @@ title="Open WebUI" href="/opensearch.xml" /> +