diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e67c9fbd5..0316bb1035 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,66 @@ 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.6.29] - 2025-09-17 + +### Added + +- 🎨 The chat input menu has been completely overhauled with a revolutionary new design, consolidating attachments under a unified '+' button, organizing integrations into a streamlined options menu, and introducing powerful, interactive selectors for attaching chats, notes, and knowledge base items. [Commit](https://github.com/open-webui/open-webui/commit/a68342d5a887e36695e21f8c2aec593b159654ff), [Commit](https://github.com/open-webui/open-webui/commit/96b8aaf83ff341fef432649366bc5155bac6cf20), [Commit](https://github.com/open-webui/open-webui/commit/4977e6d50f7b931372c96dd5979ca635d58aeb78), [Commit](https://github.com/open-webui/open-webui/commit/d973db829f7ec98b8f8fe7d3b2822d588e79f94e), [Commit](https://github.com/open-webui/open-webui/commit/d4c628de09654df76653ad9bce9cb3263e2f27c8), [Commit](https://github.com/open-webui/open-webui/commit/cd740f436db4ea308dbede14ef7ff56e8126f51b), [Commit](https://github.com/open-webui/open-webui/commit/5c2db102d06b5c18beb248d795682ff422e9b6d1), [Commit](https://github.com/open-webui/open-webui/commit/031cf38655a1a2973194d2eaa0fbbd17aca8ee92), [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/3ed0a6d11fea1a054e0bc8aa8dfbe417c7c53e51), [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/eadec9e86e01bc8f9fb90dfe7a7ae4fc3bfa6420), [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/c03ca7270e64e3a002d321237160c0ddaf2bb129), [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/b53ddfbd19aa94e9cbf7210acb31c3cfafafa5fe), [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/c923461882fcde30ae297a95e91176c95b9b72e1) +- 🤖 AI models can now be mentioned in channels to automatically generate responses, enabling multi-model conversations where mentioned models participate directly in threaded discussions with full context awareness. [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/4fe97d8794ee18e087790caab9e5d82886006145) +- 💬 The Channels feature now utilizes the modern rich text editor, including support for '/', '@', and '#' command suggestions. [Commit](https://github.com/open-webui/open-webui/commit/06c1426e14ac0dfaf723485dbbc9723a4d89aba9), [Commit](https://github.com/open-webui/open-webui/commit/02f7c3258b62970ce79716f75d15467a96565054) +- 📎 Channel message input now supports direct paste functionality for images and files from the clipboard, streamlining content sharing workflows. [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/6549fc839f86c40c26c2ef4dedcaf763a9304418) +- ⚙️ Models can now be configured with default features (Web Search, Image Generation) and filters that automatically activate when a user selects the model. [Commit](https://github.com/open-webui/open-webui/commit/9a555478273355a5177bfc7f7211c64778e4c8de), [Commit](https://github.com/open-webui/open-webui/commit/384a53b339820068e92f7eaea0d9f3e0536c19c2), [Commit](https://github.com/open-webui/open-webui/commit/d7f43bfc1a30c065def8c50d77c2579c1a3c5c67), [Commit](https://github.com/open-webui/open-webui/commit/6a67a2217cc5946ad771e479e3a37ac213210748) +- 💬 The ability to reference other chats as context within a conversation was added via the attachment menu. [Commit](https://github.com/open-webui/open-webui/commit/e097bbdf11ae4975c622e086df00d054291cdeb3), [Commit](https://github.com/open-webui/open-webui/commit/f3cd2ffb18e7dedbe88430f9ae7caa6b3cfd79d0), [Commit](https://github.com/open-webui/open-webui/commit/74263c872c5d574a9bb0944d7984f748dc772dba), [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/aa8ab349ed2fcb46d1cf994b9c0de2ec2ea35d0d), [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/025eef754f0d46789981defd473d001e3b1d0ca2) +- 🎨 The command suggestion UI for prompts ('/'), models ('@'), and knowledge ('#') was completely overhauled with a more responsive and keyboard-navigable interface. [Commit](https://github.com/open-webui/open-webui/commit/6b69c4da0fb9329ccf7024483960e070cf52ccab), [Commit](https://github.com/open-webui/open-webui/commit/06a6855f844456eceaa4d410c93379460e208202), [Commit](https://github.com/open-webui/open-webui/commit/c55f5578280b936cf581a743df3703e3db1afd54), [Commit](https://github.com/open-webui/open-webui/commit/f68d1ba394d4423d369f827894cde99d760b2402) +- 👥 User and channel suggestions were added to the mention system, enabling '@' mentions for users and models, and '#' mentions for channels with searchable user lookup and clickable navigation. [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/bbd1d2b58c89b35daea234f1fc9208f2af840899), [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/aef1e06f0bb72065a25579c982dd49157e320268), [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/779db74d7e9b7b00d099b7d65cfbc8a831e74690) +- 📁 Folder functionality was enhanced with custom background image support, improved drag-and-drop capabilities for moving folders to root level, and better menu interactions. [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/2a234829f5dfdfde27fdfd30591caa908340efb4), [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/2b1ee8b0dc5f7c0caaafdd218f20705059fa72e2), [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/b1e5bc8e490745f701909c19b6a444b67c04660e), [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/3e584132686372dfeef187596a7c557aa5f48308) +- ☁️ OneDrive integration configuration now supports selecting between personal and work/school account types via ENABLE_ONEDRIVE_PERSONAL and ENABLE_ONEDRIVE_BUSINESS environment variables. [#17354](https://github.com/open-webui/open-webui/pull/17354), [Commit](https://github.com/open-webui/open-webui/commit/e1e3009a30f9808ce06582d81a60e391f5ca09ec), [Docs:#697](https://github.com/open-webui/docs/pull/697) +- ⚡ Mermaid.js is now dynamically loaded on demand, significantly reducing first-screen loading time and improving initial page performance. [#17476](https://github.com/open-webui/open-webui/issues/17476), [#17477](https://github.com/open-webui/open-webui/pull/17477) +- ⚡ Azure MSAL browser library is now dynamically loaded on demand, reducing initial bundle size by 730KB and improving first-screen loading speed. [#17479](https://github.com/open-webui/open-webui/pull/17479) +- ⚡ CodeEditor component is now dynamically loaded on demand, reducing initial bundle size by 1MB and improving first-screen loading speed. [#17498](https://github.com/open-webui/open-webui/pull/17498) +- ⚡ Hugging Face Transformers library is now dynamically loaded on demand, reducing initial bundle size by 1.9MB and improving first-screen loading speed. [#17499](https://github.com/open-webui/open-webui/pull/17499) +- ⚡ jsPDF and html2canvas-pro libraries are now dynamically loaded on demand, reducing initial bundle size by 980KB and improving first-screen loading speed. [#17502](https://github.com/open-webui/open-webui/pull/17502) +- ⚡ Leaflet mapping library is now dynamically loaded on demand, reducing initial bundle size by 454KB and improving first-screen loading speed. [#17503](https://github.com/open-webui/open-webui/pull/17503) +- 📊 OpenTelemetry metrics collection was enhanced to properly handle HTTP 500 errors and ensure metrics are recorded even during exceptions. [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/b14617a653c6bdcfd3102c12f971924fd1faf572) +- 🔒 OAuth token retrieval logic was refactored, improving the reliability and consistency of authentication handling across the backend. [Commit](https://github.com/open-webui/open-webui/commit/6c0a5fa91cdbf6ffb74667ee61ca96bebfdfbc50) +- 💻 Code block output processing was improved to handle Python execution results more reliably, along with refined visual styling and button layouts. [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/0e5320c39e308ff97f2ca9e289618af12479eb6e) +- ⚡ Message input processing was optimized to skip unnecessary text variable handling when input is empty, improving performance. [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/e1386fe80b77126a12dabc4ad058abe9b024b275) +- 📄 Individual chat PDF export was added to the sidebar chat menu, allowing users to export single conversations as PDF documents with both stylized and plain text options. [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/d041d58bb619689cd04a391b4f8191b23941ca62) +- 🛠️ Function validation was enhanced with improved valve validation and better error handling during function loading and synchronization. [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/e66e0526ed6a116323285f79f44237538b6c75e6), [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/8edfd29102e0a61777b23d3575eaa30be37b59a5) +- 🔔 Notification toast interaction was enhanced with drag detection to prevent accidental clicks and added keyboard support for accessibility. [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/621e7679c427b6f0efa85f95235319238bf171ad) +- 🗓️ Improved date and time formatting dynamically adapts to the selected language, ensuring consistent localization across the UI. [#17409](https://github.com/open-webui/open-webui/pull/17409), [Commit](https://github.com/open-webui/open-webui/commit/2227f24bd6d861b1fad8d2cabacf7d62ce137d0c) +- 🔒 Feishu SSO integration was added, allowing users to authenticate via Feishu. [#17284](https://github.com/open-webui/open-webui/pull/17284), [Docs:#685](https://github.com/open-webui/docs/pull/685) +- 🔠 Toggle filters in the chat input options menu are now sorted alphabetically for easier navigation. [Commit](https://github.com/open-webui/open-webui/commit/ca853ca4656180487afcd84230d214f91db52533) +- 🎨 Long chat titles in the sidebar are now truncated to prevent text overflow and maintain a clean layout. [#17356](https://github.com/open-webui/open-webui/pull/17356) +- 🎨 Temporary chat interface design was refined with improved layout and visual consistency. [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/67549dcadd670285d491bd41daf3d081a70fd094), [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/2ca34217e68f3b439899c75881dfb050f49c9eb2), [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/fb02ec52a5df3f58b53db4ab3a995c15f83503cd) +- 🎨 Download icon consistency was improved across the entire interface by standardizing the icon component used in menus, functions, tools, and export features. [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/596be451ece7e11b5cd25465d49670c27a1cb33f) +- 🎨 Settings interface was enhanced with improved iconography and reorganized the 'Chats' section into 'Data Controls' for better clarity. [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/8bf0b40fdd978b5af6548a6e1fb3aabd90bcd5cd) +- 🔄 Various improvements were implemented across the frontend and backend to enhance performance, stability, and security. +- 🌐 Translations for Finnish, German, Kabyle, Portuguese (Brazil), Simplified Chinese, Spanish (Spain), and Traditional Chinese (Taiwan) were enhanced and expanded. + +### Fixed + +- 📚 Knowledge base permission logic was corrected to ensure private collection owners can access their own content when embedding bypass is enabled. [#17432](https://github.com/open-webui/open-webui/issues/17432), [Commit](https://github.com/open-webui/open-webui/commit/a51f0c30ec1472d71487eab3e15d0351a2716b12) +- ⚙️ Connection URL editing in Admin Settings now properly saves changes instead of reverting to original values, fixing issues with both Ollama and OpenAI-compatible endpoints. [#17435](https://github.com/open-webui/open-webui/issues/17435), [Commit](https://github.com/open-webui/open-webui/commit/e4c864de7eb0d577843a80688677ce3659d1f81f) +- 📊 Usage information collection from Google models was corrected to handle providers that send usage data alongside content chunks instead of separately. [#17421](https://github.com/open-webui/open-webui/pull/17421), [Commit](https://github.com/open-webui/open-webui/commit/c2f98a4cd29ed738f395fef09c42ab8e73cd46a0) +- ⚙️ Settings modal scrolling issue was resolved by moving image compression controls to a dedicated modal, preventing the main settings from becoming scrollable out of view. [#17474](https://github.com/open-webui/open-webui/issues/17474), [Commit](https://github.com/open-webui/open-webui/commit/fed5615c19b0045a55b0be426b468a57bfda4b66) +- 📁 Folder click behavior was improved to prevent accidental actions by implementing proper double-click detection and timing delays for folder expansion and selection. [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/19e3214997170eea6ee92452e8c778e04a28e396) +- 🔐 Access control component reliability was improved with better null checking and error handling for group permissions and private access scenarios. [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/c8780a7f934c5e49a21b438f2f30232f83cf75d2), [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/32015c392dbc6b7367a6a91d9e173e675ea3402c) +- 🔗 The citation modal now correctly displays and links to external web page sources in addition to internal documents. [Commit](https://github.com/open-webui/open-webui/commit/9208a84185a7e59524f00a7576667d493c3ac7d4) +- 🔗 Web and YouTube attachment handling was fixed, ensuring their content is now reliably processed and included in the chat context for retrieval. [Commit](https://github.com/open-webui/open-webui/commit/210197fd438b52080cda5d6ce3d47b92cdc264c8) +- 📂 Large file upload failures are resolved by correcting the processing logic for scenarios where document embedding is bypassed. [Commit](https://github.com/open-webui/open-webui/commit/051b6daa8299fd332503bd584563556e2ae6adab) +- 🌐 Rich text input placeholder text now correctly updates when the interface language is switched, ensuring proper localization. [#17473](https://github.com/open-webui/open-webui/pull/17473), [Commit](https://github.com/open-webui/open-webui/commit/77358031f5077e6efe5cc08d8d4e5831c7cd1cd9) +- 📊 Llama.cpp server timing metrics are now correctly parsed and displayed by fixing a typo in the response handling. [#17350](https://github.com/open-webui/open-webui/issues/17350), [Commit](https://github.com/open-webui/open-webui/commit/cf72f5503f39834b9da44ebbb426a3674dad0caa) +- 🛠️ Filter functions with file_handler configuration now properly handle messages without file attachments, preventing runtime errors. [#17423](https://github.com/open-webui/open-webui/pull/17423) +- 🔔 Channel notification delivery was fixed to properly handle background task execution and user access checking. [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/1077b2ac8b96e49c2ad2620e76eb65bbb2a3a1f3) + +### Changed + +- 📝 Prompt template variables are now optional by default instead of being forced as required, allowing flexible workflows with optional metadata fields. [#17447](https://github.com/open-webui/open-webui/issues/17447), [Commit](https://github.com/open-webui/open-webui/commit/d5824b1b495fcf86e57171769bcec2a0f698b070), [Docs:#696](https://github.com/open-webui/docs/pull/696) +- 🛠️ Direct external tool servers now require explicit user selection from the input interface instead of being automatically included in conversations, providing better control over tool usage. [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/0f04227c34ca32746c43a9323e2df32299fcb6af), [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/99bba12de279dd55c55ded35b2e4f819af1c9ab5) +- 📺 Widescreen mode option was removed from Channels interface, with all channel layouts now using full-width display. [Commit](https://github.com/open-webui/open-webui/pull/17420/commits/d46b7b8f1b99a8054b55031fe935c8a16d5ec956) +- 🎛️ The plain textarea input option was deprecated, and the custom text editor is now the standard for all chat inputs. [Commit](https://github.com/open-webui/open-webui/commit/153afd832ccd12a1e5fd99b085008d080872c161) + ## [0.6.28] - 2025-09-10 ### Added diff --git a/backend/dev.sh b/backend/dev.sh index 504b8f7554..042fbd9efa 100755 --- a/backend/dev.sh +++ b/backend/dev.sh @@ -1,3 +1,3 @@ -export CORS_ALLOW_ORIGIN="http://localhost:5173" +export CORS_ALLOW_ORIGIN="http://localhost:5173;http://localhost:8080" PORT="${PORT:-8080}" uvicorn open_webui.main:app --port $PORT --host 0.0.0.0 --forwarded-allow-ips '*' --reload diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index 11698d87af..ca090efa22 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -513,6 +513,30 @@ OAUTH_GROUPS_CLAIM = PersistentConfig( os.environ.get("OAUTH_GROUPS_CLAIM", os.environ.get("OAUTH_GROUP_CLAIM", "groups")), ) +FEISHU_CLIENT_ID = PersistentConfig( + "FEISHU_CLIENT_ID", + "oauth.feishu.client_id", + os.environ.get("FEISHU_CLIENT_ID", ""), +) + +FEISHU_CLIENT_SECRET = PersistentConfig( + "FEISHU_CLIENT_SECRET", + "oauth.feishu.client_secret", + os.environ.get("FEISHU_CLIENT_SECRET", ""), +) + +FEISHU_OAUTH_SCOPE = PersistentConfig( + "FEISHU_OAUTH_SCOPE", + "oauth.feishu.scope", + os.environ.get("FEISHU_OAUTH_SCOPE", "contact:user.base:readonly"), +) + +FEISHU_REDIRECT_URI = PersistentConfig( + "FEISHU_REDIRECT_URI", + "oauth.feishu.redirect_uri", + os.environ.get("FEISHU_REDIRECT_URI", ""), +) + ENABLE_OAUTH_ROLE_MANAGEMENT = PersistentConfig( "ENABLE_OAUTH_ROLE_MANAGEMENT", "oauth.enable_role_mapping", @@ -705,6 +729,33 @@ def load_oauth_providers(): "register": oidc_oauth_register, } + if FEISHU_CLIENT_ID.value and FEISHU_CLIENT_SECRET.value: + + def feishu_oauth_register(client: OAuth): + client.register( + name="feishu", + client_id=FEISHU_CLIENT_ID.value, + client_secret=FEISHU_CLIENT_SECRET.value, + access_token_url="https://open.feishu.cn/open-apis/authen/v2/oauth/token", + authorize_url="https://accounts.feishu.cn/open-apis/authen/v1/authorize", + api_base_url="https://open.feishu.cn/open-apis", + userinfo_endpoint="https://open.feishu.cn/open-apis/authen/v1/user_info", + client_kwargs={ + "scope": FEISHU_OAUTH_SCOPE.value, + **( + {"timeout": int(OAUTH_TIMEOUT.value)} + if OAUTH_TIMEOUT.value + else {} + ), + }, + redirect_uri=FEISHU_REDIRECT_URI.value, + ) + + OAUTH_PROVIDERS["feishu"] = { + "register": feishu_oauth_register, + "sub_claim": "user_id", + } + configured_providers = [] if GOOGLE_CLIENT_ID.value: configured_providers.append("Google") @@ -712,6 +763,8 @@ def load_oauth_providers(): configured_providers.append("Microsoft") if GITHUB_CLIENT_ID.value: configured_providers.append("GitHub") + if FEISHU_CLIENT_ID.value: + configured_providers.append("Feishu") if configured_providers and not OPENID_PROVIDER_URL.value: provider_list = ", ".join(configured_providers) @@ -2115,6 +2168,12 @@ ENABLE_ONEDRIVE_INTEGRATION = PersistentConfig( "onedrive.enable", os.getenv("ENABLE_ONEDRIVE_INTEGRATION", "False").lower() == "true", ) +ENABLE_ONEDRIVE_PERSONAL = ( + os.environ.get("ENABLE_ONEDRIVE_PERSONAL", "True").lower() == "true" +) +ENABLE_ONEDRIVE_BUSINESS = ( + os.environ.get("ENABLE_ONEDRIVE_BUSINESS", "True").lower() == "true" +) ONEDRIVE_CLIENT_ID = PersistentConfig( "ONEDRIVE_CLIENT_ID", diff --git a/backend/open_webui/functions.py b/backend/open_webui/functions.py index 4122cbbe0d..af6dd1ce1a 100644 --- a/backend/open_webui/functions.py +++ b/backend/open_webui/functions.py @@ -19,6 +19,7 @@ from fastapi import ( from starlette.responses import Response, StreamingResponse +from open_webui.constants import ERROR_MESSAGES from open_webui.socket.main import ( get_event_call, get_event_emitter, @@ -60,8 +61,20 @@ def get_function_module_by_id(request: Request, pipe_id: str): function_module, _, _ = get_function_module_from_cache(request, pipe_id) if hasattr(function_module, "valves") and hasattr(function_module, "Valves"): + Valves = function_module.Valves valves = Functions.get_function_valves_by_id(pipe_id) - function_module.valves = function_module.Valves(**(valves if valves else {})) + + if valves: + try: + function_module.valves = Valves( + **{k: v for k, v in valves.items() if v is not None} + ) + except Exception as e: + log.exception(f"Error loading valves for function {pipe_id}: {e}") + raise e + else: + function_module.valves = Valves() + return function_module @@ -70,65 +83,69 @@ async def get_function_models(request): pipe_models = [] for pipe in pipes: - function_module = get_function_module_by_id(request, pipe.id) + try: + function_module = get_function_module_by_id(request, pipe.id) - # Check if function is a manifold - if hasattr(function_module, "pipes"): - sub_pipes = [] - - # Handle pipes being a list, sync function, or async function - try: - if callable(function_module.pipes): - if asyncio.iscoroutinefunction(function_module.pipes): - sub_pipes = await function_module.pipes() - else: - sub_pipes = function_module.pipes() - else: - sub_pipes = function_module.pipes - except Exception as e: - log.exception(e) + # Check if function is a manifold + if hasattr(function_module, "pipes"): sub_pipes = [] - log.debug( - f"get_function_models: function '{pipe.id}' is a manifold of {sub_pipes}" - ) + # Handle pipes being a list, sync function, or async function + try: + if callable(function_module.pipes): + if asyncio.iscoroutinefunction(function_module.pipes): + sub_pipes = await function_module.pipes() + else: + sub_pipes = function_module.pipes() + else: + sub_pipes = function_module.pipes + except Exception as e: + log.exception(e) + sub_pipes = [] - for p in sub_pipes: - sub_pipe_id = f'{pipe.id}.{p["id"]}' - sub_pipe_name = p["name"] + log.debug( + f"get_function_models: function '{pipe.id}' is a manifold of {sub_pipes}" + ) - if hasattr(function_module, "name"): - sub_pipe_name = f"{function_module.name}{sub_pipe_name}" + for p in sub_pipes: + sub_pipe_id = f'{pipe.id}.{p["id"]}' + sub_pipe_name = p["name"] - pipe_flag = {"type": pipe.type} + if hasattr(function_module, "name"): + sub_pipe_name = f"{function_module.name}{sub_pipe_name}" + + pipe_flag = {"type": pipe.type} + + pipe_models.append( + { + "id": sub_pipe_id, + "name": sub_pipe_name, + "object": "model", + "created": pipe.created_at, + "owned_by": "openai", + "pipe": pipe_flag, + } + ) + else: + pipe_flag = {"type": "pipe"} + + log.debug( + f"get_function_models: function '{pipe.id}' is a single pipe {{ 'id': {pipe.id}, 'name': {pipe.name} }}" + ) pipe_models.append( { - "id": sub_pipe_id, - "name": sub_pipe_name, + "id": pipe.id, + "name": pipe.name, "object": "model", "created": pipe.created_at, "owned_by": "openai", "pipe": pipe_flag, } ) - else: - pipe_flag = {"type": "pipe"} - - log.debug( - f"get_function_models: function '{pipe.id}' is a single pipe {{ 'id': {pipe.id}, 'name': {pipe.name} }}" - ) - - pipe_models.append( - { - "id": pipe.id, - "name": pipe.name, - "object": "model", - "created": pipe.created_at, - "owned_by": "openai", - "pipe": pipe_flag, - } - ) + except Exception as e: + log.exception(e) + continue return pipe_models @@ -221,10 +238,11 @@ async def generate_function_chat_completion( oauth_token = None try: - oauth_token = request.app.state.oauth_manager.get_oauth_token( - user.id, - request.cookies.get("oauth_session_id", None), - ) + if request.cookies.get("oauth_session_id", None): + oauth_token = request.app.state.oauth_manager.get_oauth_token( + user.id, + request.cookies.get("oauth_session_id", None), + ) except Exception as e: log.error(f"Error getting OAuth token: {e}") diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index a5d55f75ab..bf25c91cd0 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -448,6 +448,7 @@ from open_webui.utils.models import ( get_all_models, get_all_base_models, check_model_access, + get_filtered_models, ) from open_webui.utils.chat import ( generate_chat_completion as chat_completion_handler, @@ -1291,33 +1292,6 @@ if audit_level != AuditLevel.NONE: async def get_models( request: Request, refresh: bool = False, user=Depends(get_verified_user) ): - def get_filtered_models(models, user): - filtered_models = [] - for model in models: - if model.get("arena"): - if has_access( - user.id, - type="read", - access_control=model.get("info", {}) - .get("meta", {}) - .get("access_control", {}), - ): - filtered_models.append(model) - continue - - model_info = Models.get_model_by_id(model["id"]) - if model_info: - if ( - (user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL) - or user.id == model_info.user_id - or has_access( - user.id, type="read", access_control=model_info.access_control - ) - ): - filtered_models.append(model) - - return filtered_models - all_models = await get_all_models(request, refresh=refresh, user=user) models = [] @@ -1353,12 +1327,7 @@ async def get_models( ) ) - # Filter out models that the user does not have access to - if ( - user.role == "user" - or (user.role == "admin" and not BYPASS_ADMIN_ACCESS_CONTROL) - ) and not BYPASS_MODEL_ACCESS_CONTROL: - models = get_filtered_models(models, user) + models = get_filtered_models(models, user) log.debug( f"/api/models returned filtered models accessible to the user: {json.dumps([model.get('id') for model in models])}" @@ -1418,14 +1387,6 @@ async def chat_completion( model_item = form_data.pop("model_item", {}) tasks = form_data.pop("background_tasks", None) - oauth_token = None - try: - oauth_token = request.app.state.oauth_manager.get_oauth_token( - user.id, request.cookies.get("oauth_session_id", None) - ) - except Exception as e: - log.error(f"Error getting OAuth token: {e}") - metadata = {} try: if not model_item.get("direct", False): @@ -1738,6 +1699,14 @@ async def get_app_config(request: Request): "enable_admin_chat_access": ENABLE_ADMIN_CHAT_ACCESS, "enable_google_drive_integration": app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION, "enable_onedrive_integration": app.state.config.ENABLE_ONEDRIVE_INTEGRATION, + **( + { + "enable_onedrive_personal": app.state.config.ENABLE_ONEDRIVE_PERSONAL, + "enable_onedrive_business": app.state.config.ENABLE_ONEDRIVE_BUSINESS, + } + if app.state.config.ENABLE_ONEDRIVE_INTEGRATION + else {} + ), } if user is not None else {} diff --git a/backend/open_webui/models/chats.py b/backend/open_webui/models/chats.py index 56f992806a..cadb5a3a79 100644 --- a/backend/open_webui/models/chats.py +++ b/backend/open_webui/models/chats.py @@ -236,7 +236,7 @@ class ChatTable: return chat.chat.get("title", "New Chat") - def get_messages_by_chat_id(self, id: str) -> Optional[dict]: + def get_messages_map_by_chat_id(self, id: str) -> Optional[dict]: chat = self.get_chat_by_id(id) if chat is None: return None diff --git a/backend/open_webui/models/functions.py b/backend/open_webui/models/functions.py index 2bb6d60889..e8ce3aa811 100644 --- a/backend/open_webui/models/functions.py +++ b/backend/open_webui/models/functions.py @@ -37,6 +37,7 @@ class Function(Base): class FunctionMeta(BaseModel): description: Optional[str] = None manifest: Optional[dict] = {} + model_config = ConfigDict(extra="allow") class FunctionModel(BaseModel): @@ -260,6 +261,29 @@ class FunctionsTable: except Exception: return None + def update_function_metadata_by_id( + self, id: str, metadata: dict + ) -> Optional[FunctionModel]: + with get_db() as db: + try: + function = db.get(Function, id) + + if function: + if function.meta: + function.meta = {**function.meta, **metadata} + else: + function.meta = metadata + + function.updated_at = int(time.time()) + db.commit() + db.refresh(function) + return self.get_function_by_id(id) + else: + return None + except Exception as e: + log.exception(f"Error updating function metadata by id {id}: {e}") + return None + def get_user_valves_by_id_and_user_id( self, id: str, user_id: str ) -> Optional[dict]: diff --git a/backend/open_webui/models/messages.py b/backend/open_webui/models/messages.py index a27ae52519..ff4553ee9d 100644 --- a/backend/open_webui/models/messages.py +++ b/backend/open_webui/models/messages.py @@ -201,8 +201,14 @@ class MessageTable: with get_db() as db: message = db.get(Message, id) message.content = form_data.content - message.data = form_data.data - message.meta = form_data.meta + message.data = { + **(message.data if message.data else {}), + **(form_data.data if form_data.data else {}), + } + message.meta = { + **(message.meta if message.meta else {}), + **(form_data.meta if form_data.meta else {}), + } message.updated_at = int(time.time_ns()) db.commit() db.refresh(message) diff --git a/backend/open_webui/models/notes.py b/backend/open_webui/models/notes.py index c720ff80a4..b61e820eae 100644 --- a/backend/open_webui/models/notes.py +++ b/backend/open_webui/models/notes.py @@ -97,15 +97,26 @@ class NoteTable: db.commit() return note - def get_notes(self) -> list[NoteModel]: + def get_notes( + self, skip: Optional[int] = None, limit: Optional[int] = None + ) -> list[NoteModel]: with get_db() as db: - notes = db.query(Note).order_by(Note.updated_at.desc()).all() + query = db.query(Note).order_by(Note.updated_at.desc()) + if skip is not None: + query = query.offset(skip) + if limit is not None: + query = query.limit(limit) + notes = query.all() return [NoteModel.model_validate(note) for note in notes] def get_notes_by_user_id( - self, user_id: str, permission: str = "write" + self, + user_id: str, + permission: str = "write", + skip: Optional[int] = None, + limit: Optional[int] = None, ) -> list[NoteModel]: - notes = self.get_notes() + notes = self.get_notes(skip=skip, limit=limit) user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user_id)} return [ note diff --git a/backend/open_webui/models/users.py b/backend/open_webui/models/users.py index 620a746eed..05000744dd 100644 --- a/backend/open_webui/models/users.py +++ b/backend/open_webui/models/users.py @@ -107,11 +107,21 @@ class UserInfoResponse(BaseModel): role: str +class UserIdNameResponse(BaseModel): + id: str + name: str + + class UserInfoListResponse(BaseModel): users: list[UserInfoResponse] total: int +class UserIdNameListResponse(BaseModel): + users: list[UserIdNameResponse] + total: int + + class UserResponse(BaseModel): id: str name: str @@ -210,7 +220,7 @@ class UsersTable: filter: Optional[dict] = None, skip: Optional[int] = None, limit: Optional[int] = None, - ) -> UserListResponse: + ) -> dict: with get_db() as db: query = db.query(User) diff --git a/backend/open_webui/retrieval/utils.py b/backend/open_webui/retrieval/utils.py index dead8458cb..aec8de6846 100644 --- a/backend/open_webui/retrieval/utils.py +++ b/backend/open_webui/retrieval/utils.py @@ -19,10 +19,13 @@ from open_webui.retrieval.vector.factory import VECTOR_DB_CLIENT from open_webui.models.users import UserModel from open_webui.models.files import Files from open_webui.models.knowledge import Knowledges + +from open_webui.models.chats import Chats from open_webui.models.notes import Notes from open_webui.retrieval.vector.main import GetResult from open_webui.utils.access_control import has_access +from open_webui.utils.misc import get_message_list from open_webui.env import ( @@ -432,13 +435,14 @@ def get_embedding_function( if isinstance(query, list): embeddings = [] for i in range(0, len(query), embedding_batch_size): - embeddings.extend( - func( - query[i : i + embedding_batch_size], - prefix=prefix, - user=user, - ) + batch_embeddings = func( + query[i : i + embedding_batch_size], + prefix=prefix, + user=user, ) + + if isinstance(batch_embeddings, list): + embeddings.extend(batch_embeddings) return embeddings else: return func(query, prefix, user) @@ -490,25 +494,37 @@ def get_sources_from_items( # Raw Text # Used during temporary chat file uploads or web page & youtube attachements - if item.get("collection_name"): - # If item has a collection name, use it - collection_names.append(item.get("collection_name")) - elif item.get("file"): - # if item has file data, use it - query_result = { - "documents": [ - [item.get("file", {}).get("data", {}).get("content")] - ], - "metadatas": [[item.get("file", {}).get("meta", {})]], - } - else: - # Fallback to item content - query_result = { - "documents": [[item.get("content")]], - "metadatas": [ - [{"file_id": item.get("id"), "name": item.get("name")}] - ], - } + if item.get("context") == "full": + if item.get("file"): + # if item has file data, use it + query_result = { + "documents": [ + [item.get("file", {}).get("data", {}).get("content")] + ], + "metadatas": [[item.get("file", {}).get("meta", {})]], + } + + if query_result is None: + # Fallback + if item.get("collection_name"): + # If item has a collection name, use it + collection_names.append(item.get("collection_name")) + elif item.get("file"): + # If item has file data, use it + query_result = { + "documents": [ + [item.get("file", {}).get("data", {}).get("content")] + ], + "metadatas": [[item.get("file", {}).get("meta", {})]], + } + else: + # Fallback to item content + query_result = { + "documents": [[item.get("content")]], + "metadatas": [ + [{"file_id": item.get("id"), "name": item.get("name")}] + ], + } elif item.get("type") == "note": # Note Attached @@ -525,6 +541,30 @@ def get_sources_from_items( "metadatas": [[{"file_id": note.id, "name": note.title}]], } + elif item.get("type") == "chat": + # Chat Attached + chat = Chats.get_chat_by_id(item.get("id")) + + if chat and (user.role == "admin" or chat.user_id == user.id): + messages_map = chat.chat.get("history", {}).get("messages", {}) + message_id = chat.chat.get("history", {}).get("currentId") + + if messages_map and message_id: + # Reconstruct the message list in order + message_list = get_message_list(messages_map, message_id) + message_history = "\n".join( + [ + f"#### {m.get('role', 'user').capitalize()}\n{m.get('content')}\n" + for m in message_list + ] + ) + + # User has access to the chat + query_result = { + "documents": [[message_history]], + "metadatas": [[{"file_id": chat.id, "name": chat.title}]], + } + elif item.get("type") == "file": if ( item.get("context") == "full" @@ -581,6 +621,7 @@ def get_sources_from_items( if knowledge_base and ( user.role == "admin" + or knowledge_base.user_id == user.id or has_access(user.id, "read", knowledge_base.access_control) ): diff --git a/backend/open_webui/routers/audio.py b/backend/open_webui/routers/audio.py index c4a187b50d..100610a83a 100644 --- a/backend/open_webui/routers/audio.py +++ b/backend/open_webui/routers/audio.py @@ -550,7 +550,7 @@ def transcription_handler(request, file_path, metadata): metadata = metadata or {} languages = [ - metadata.get("language", None) if WHISPER_LANGUAGE == "" else WHISPER_LANGUAGE, + metadata.get("language", None) if not WHISPER_LANGUAGE else WHISPER_LANGUAGE, None, # Always fallback to None in case transcription fails ] diff --git a/backend/open_webui/routers/channels.py b/backend/open_webui/routers/channels.py index cf3603c6ff..da52be6e79 100644 --- a/backend/open_webui/routers/channels.py +++ b/backend/open_webui/routers/channels.py @@ -24,9 +24,17 @@ from open_webui.constants import ERROR_MESSAGES from open_webui.env import SRC_LOG_LEVELS +from open_webui.utils.models import ( + get_all_models, + get_filtered_models, +) +from open_webui.utils.chat import generate_chat_completion + + from open_webui.utils.auth import get_admin_user, get_verified_user from open_webui.utils.access_control import has_access, get_users_with_access from open_webui.utils.webhook import post_webhook +from open_webui.utils.channels import extract_mentions, replace_mentions log = logging.getLogger(__name__) log.setLevel(SRC_LOG_LEVELS["MODELS"]) @@ -200,14 +208,11 @@ 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: - if user.id in active_user_ids: - continue - else: + if user.id not in active_user_ids: if user.settings: webhook_url = user.settings.ui.get("notifications", {}).get( "webhook_url", None ) - if webhook_url: await post_webhook( name, @@ -221,14 +226,134 @@ async def send_notification(name, webui_url, channel, message, active_user_ids): }, ) + return True -@router.post("/{id}/messages/post", response_model=Optional[MessageModel]) -async def post_new_message( - request: Request, - id: str, - form_data: MessageForm, - background_tasks: BackgroundTasks, - user=Depends(get_verified_user), + +async def model_response_handler(request, channel, message, user): + MODELS = { + model["id"]: model + for model in get_filtered_models(await get_all_models(request, user=user), user) + } + + mentions = extract_mentions(message.content) + message_content = replace_mentions(message.content) + + # check if any of the mentions are models + model_mentions = [mention for mention in mentions if mention["id_type"] == "M"] + if not model_mentions: + return False + + for mention in model_mentions: + model_id = mention["id"] + model = MODELS.get(model_id, None) + + if model: + try: + # reverse to get in chronological order + thread_messages = Messages.get_messages_by_parent_id( + channel.id, + message.parent_id if message.parent_id else message.id, + )[::-1] + + response_message, channel = await new_message_handler( + request, + channel.id, + MessageForm( + **{ + "parent_id": ( + message.parent_id if message.parent_id else message.id + ), + "content": f"", + "data": {}, + "meta": { + "model_id": model_id, + "model_name": model.get("name", model_id), + }, + } + ), + user, + ) + + thread_history = [] + message_users = {} + + for thread_message in thread_messages: + message_user = None + if thread_message.user_id not in message_users: + message_user = Users.get_user_by_id(thread_message.user_id) + message_users[thread_message.user_id] = message_user + else: + message_user = message_users[thread_message.user_id] + + if thread_message.meta and thread_message.meta.get( + "model_id", None + ): + # If the message was sent by a model, use the model name + message_model_id = thread_message.meta.get("model_id", None) + message_model = MODELS.get(message_model_id, None) + username = ( + message_model.get("name", message_model_id) + if message_model + else message_model_id + ) + else: + username = message_user.name if message_user else "Unknown" + + thread_history.append( + f"{username}: {replace_mentions(thread_message.content)}" + ) + + system_message = { + "role": "system", + "content": f"You are {model.get('name', model_id)}, an AI assistant participating in a threaded conversation. Be helpful, concise, and conversational." + + ( + f"Here's the thread history:\n\n{''.join([f'{msg}' for msg in thread_history])}\n\nContinue the conversation naturally, addressing the most recent message while being aware of the full context." + if thread_history + else "" + ), + } + + form_data = { + "model": model_id, + "messages": [ + system_message, + { + "role": "user", + "content": f"{user.name if user else 'User'}: {message_content}", + }, + ], + "stream": False, + } + + res = await generate_chat_completion( + request, + form_data=form_data, + user=user, + ) + + if res: + await update_message_by_id( + channel.id, + response_message.id, + MessageForm( + **{ + "content": res["choices"][0]["message"]["content"], + "meta": { + "done": True, + }, + } + ), + user, + ) + except Exception as e: + log.info(e) + pass + + return True + + +async def new_message_handler( + request: Request, id: str, form_data: MessageForm, user=Depends(get_verified_user) ): channel = Channels.get_channel_by_id(id) if not channel: @@ -302,11 +427,30 @@ async def post_new_message( }, to=f"channel:{channel.id}", ) + return MessageModel(**message.model_dump()), channel + except Exception as e: + log.exception(e) + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT() + ) - active_user_ids = get_user_ids_from_room(f"channel:{channel.id}") - background_tasks.add_task( - send_notification, +@router.post("/{id}/messages/post", response_model=Optional[MessageModel]) +async def post_new_message( + request: Request, + id: str, + form_data: MessageForm, + background_tasks: BackgroundTasks, + user=Depends(get_verified_user), +): + + try: + message, channel = await new_message_handler(request, id, form_data, user) + active_user_ids = get_user_ids_from_room(f"channel:{channel.id}") + + async def background_handler(): + await model_response_handler(request, channel, message, user) + await send_notification( request.app.state.WEBUI_NAME, request.app.state.config.WEBUI_URL, channel, @@ -314,7 +458,12 @@ async def post_new_message( active_user_ids, ) - return MessageModel(**message.model_dump()) + background_tasks.add_task(background_handler) + + return message + + except HTTPException as e: + raise e except Exception as e: log.exception(e) raise HTTPException( diff --git a/backend/open_webui/routers/chats.py b/backend/open_webui/routers/chats.py index 6f853ab266..847368412e 100644 --- a/backend/open_webui/routers/chats.py +++ b/backend/open_webui/routers/chats.py @@ -166,7 +166,7 @@ async def import_chat(form_data: ChatImportForm, user=Depends(get_verified_user) @router.get("/search", response_model=list[ChatTitleIdResponse]) -async def search_user_chats( +def search_user_chats( text: str, page: Optional[int] = None, user=Depends(get_verified_user) ): if page is None: diff --git a/backend/open_webui/routers/files.py b/backend/open_webui/routers/files.py index 778fbdec27..84d8f841cf 100644 --- a/backend/open_webui/routers/files.py +++ b/backend/open_webui/routers/files.py @@ -120,11 +120,6 @@ def process_uploaded_file(request, file, file_path, file_item, file_metadata, us f"File type {file.content_type} is not provided, but trying to process anyway" ) process_file(request, ProcessFileForm(file_id=file_item.id), user=user) - - Files.update_file_data_by_id( - file_item.id, - {"status": "completed"}, - ) except Exception as e: log.error(f"Error processing file: {file_item.id}") Files.update_file_data_by_id( diff --git a/backend/open_webui/routers/functions.py b/backend/open_webui/routers/functions.py index 9ef6915709..202aa74ca4 100644 --- a/backend/open_webui/routers/functions.py +++ b/backend/open_webui/routers/functions.py @@ -148,6 +148,18 @@ async def sync_functions( content=function.content, ) + if hasattr(function_module, "Valves") and function.valves: + Valves = function_module.Valves + try: + Valves( + **{k: v for k, v in function.valves.items() if v is not None} + ) + except Exception as e: + log.exception( + f"Error validating valves for function {function.id}: {e}" + ) + raise e + return Functions.sync_functions(user.id, form_data.functions) except Exception as e: log.exception(f"Failed to load a function: {e}") @@ -192,6 +204,9 @@ async def create_new_function( function_cache_dir = CACHE_DIR / "functions" / form_data.id function_cache_dir.mkdir(parents=True, exist_ok=True) + if function_type == "filter" and getattr(function_module, "toggle", None): + Functions.update_function_metadata_by_id(id, {"toggle": True}) + if function: return function else: @@ -308,6 +323,9 @@ async def update_function_by_id( function = Functions.update_function_by_id(id, updated) + if function_type == "filter" and getattr(function_module, "toggle", None): + Functions.update_function_metadata_by_id(id, {"toggle": True}) + if function: return function else: diff --git a/backend/open_webui/routers/models.py b/backend/open_webui/routers/models.py index a4d4e3668e..05d7c68006 100644 --- a/backend/open_webui/routers/models.py +++ b/backend/open_webui/routers/models.py @@ -1,4 +1,6 @@ from typing import Optional +import io +import base64 from open_webui.models.models import ( ModelForm, @@ -10,12 +12,13 @@ from open_webui.models.models import ( from pydantic import BaseModel from open_webui.constants import ERROR_MESSAGES -from fastapi import APIRouter, Depends, HTTPException, Request, status +from fastapi import APIRouter, Depends, HTTPException, Request, status, Response +from fastapi.responses import FileResponse, StreamingResponse from open_webui.utils.auth import get_admin_user, get_verified_user from open_webui.utils.access_control import has_access, has_permission -from open_webui.config import BYPASS_ADMIN_ACCESS_CONTROL +from open_webui.config import BYPASS_ADMIN_ACCESS_CONTROL, STATIC_DIR router = APIRouter() @@ -129,6 +132,39 @@ async def get_model_by_id(id: str, user=Depends(get_verified_user)): ) +########################### +# GetModelById +########################### + + +@router.get("/model/profile/image") +async def get_model_profile_image(id: str, user=Depends(get_verified_user)): + model = Models.get_model_by_id(id) + if model: + if model.meta.profile_image_url: + if model.meta.profile_image_url.startswith("http"): + return Response( + status_code=status.HTTP_302_FOUND, + headers={"Location": model.meta.profile_image_url}, + ) + elif model.meta.profile_image_url.startswith("data:image"): + try: + header, base64_data = model.meta.profile_image_url.split(",", 1) + image_data = base64.b64decode(base64_data) + image_buffer = io.BytesIO(image_data) + + return StreamingResponse( + image_buffer, + media_type="image/png", + headers={"Content-Disposition": "inline; filename=image.png"}, + ) + except Exception as e: + pass + return FileResponse(f"{STATIC_DIR}/favicon.png") + else: + return FileResponse(f"{STATIC_DIR}/favicon.png") + + ############################ # ToggleModelById ############################ diff --git a/backend/open_webui/routers/notes.py b/backend/open_webui/routers/notes.py index 375f59ff6c..dff7bc2e7f 100644 --- a/backend/open_webui/routers/notes.py +++ b/backend/open_webui/routers/notes.py @@ -62,8 +62,9 @@ class NoteTitleIdResponse(BaseModel): @router.get("/list", response_model=list[NoteTitleIdResponse]) -async def get_note_list(request: Request, user=Depends(get_verified_user)): - +async def get_note_list( + request: Request, page: Optional[int] = None, user=Depends(get_verified_user) +): if user.role != "admin" and not has_permission( user.id, "features.notes", request.app.state.config.USER_PERMISSIONS ): @@ -72,9 +73,15 @@ async def get_note_list(request: Request, user=Depends(get_verified_user)): detail=ERROR_MESSAGES.UNAUTHORIZED, ) + limit = None + skip = None + if page is not None: + limit = 60 + skip = (page - 1) * limit + notes = [ NoteTitleIdResponse(**note.model_dump()) - for note in Notes.get_notes_by_user_id(user.id, "write") + for note in Notes.get_notes_by_user_id(user.id, "write", skip=skip, limit=limit) ] return notes diff --git a/backend/open_webui/routers/openai.py b/backend/open_webui/routers/openai.py index 184f47038d..3154be2ee6 100644 --- a/backend/open_webui/routers/openai.py +++ b/backend/open_webui/routers/openai.py @@ -9,6 +9,8 @@ from aiocache import cached import requests from urllib.parse import quote +from azure.identity import DefaultAzureCredential, get_bearer_token_provider + from fastapi import Depends, HTTPException, Request, APIRouter from fastapi.responses import ( FileResponse, @@ -171,22 +173,41 @@ def get_headers_and_cookies( oauth_token = None try: - oauth_token = request.app.state.oauth_manager.get_oauth_token( - user.id, - request.cookies.get("oauth_session_id", None), - ) + if request.cookies.get("oauth_session_id", None): + oauth_token = request.app.state.oauth_manager.get_oauth_token( + user.id, + request.cookies.get("oauth_session_id", None), + ) except Exception as e: log.error(f"Error getting OAuth token: {e}") if oauth_token: token = f"{oauth_token.get('access_token', '')}" + elif auth_type in ("azure_ad", "microsoft_entra_id"): + token = get_microsoft_entra_id_access_token() + if token: headers["Authorization"] = f"Bearer {token}" return headers, cookies +def get_microsoft_entra_id_access_token(): + """ + Get Microsoft Entra ID access token using DefaultAzureCredential for Azure OpenAI. + Returns the token string or None if authentication fails. + """ + try: + token_provider = get_bearer_token_provider( + DefaultAzureCredential(), "https://cognitiveservices.azure.com/.default" + ) + return token_provider() + except Exception as e: + log.error(f"Error getting Microsoft Entra ID access token: {e}") + return None + + ########################################## # # API routes @@ -640,9 +661,12 @@ async def verify_connection( ) if api_config.get("azure", False): - headers["api-key"] = key - api_version = api_config.get("api_version", "") or "2023-03-15-preview" + # Only set api-key header if not using Azure Entra ID authentication + auth_type = api_config.get("auth_type", "bearer") + if auth_type not in ("azure_ad", "microsoft_entra_id"): + headers["api-key"] = key + api_version = api_config.get("api_version", "") or "2023-03-15-preview" async with session.get( url=f"{url}/openai/models?api-version={api_version}", headers=headers, @@ -884,7 +908,12 @@ async def generate_chat_completion( if api_config.get("azure", False): api_version = api_config.get("api_version", "2023-03-15-preview") request_url, payload = convert_to_azure_payload(url, payload, api_version) - headers["api-key"] = key + + # Only set api-key header if not using Azure Entra ID authentication + auth_type = api_config.get("auth_type", "bearer") + if auth_type not in ("azure_ad", "microsoft_entra_id"): + headers["api-key"] = key + headers["api-version"] = api_version request_url = f"{request_url}/chat/completions?api-version={api_version}" else: @@ -1057,7 +1086,12 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)): if api_config.get("azure", False): api_version = api_config.get("api_version", "2023-03-15-preview") - headers["api-key"] = key + + # Only set api-key header if not using Azure Entra ID authentication + auth_type = api_config.get("auth_type", "bearer") + if auth_type not in ("azure_ad", "microsoft_entra_id"): + headers["api-key"] = key + headers["api-version"] = api_version payload = json.loads(body) diff --git a/backend/open_webui/routers/retrieval.py b/backend/open_webui/routers/retrieval.py index dd5e2d5bc4..0ddf824efa 100644 --- a/backend/open_webui/routers/retrieval.py +++ b/backend/open_webui/routers/retrieval.py @@ -1334,7 +1334,7 @@ def save_docs_to_vector_db( ) return True - log.info(f"adding to collection {collection_name}") + log.info(f"generating embeddings for {collection_name}") embedding_function = get_embedding_function( request.app.state.config.RAG_EMBEDDING_ENGINE, request.app.state.config.RAG_EMBEDDING_MODEL, @@ -1370,6 +1370,7 @@ def save_docs_to_vector_db( prefix=RAG_EMBEDDING_CONTENT_PREFIX, user=user, ) + log.info(f"embeddings generated {len(embeddings)} for {len(texts)} items") items = [ { @@ -1381,11 +1382,13 @@ def save_docs_to_vector_db( for idx, text in enumerate(texts) ] + log.info(f"adding to collection {collection_name}") VECTOR_DB_CLIENT.insert( collection_name=collection_name, items=items, ) + log.info(f"added {len(items)} items to collection {collection_name}") return True except Exception as e: log.exception(e) @@ -1544,13 +1547,20 @@ def process_file( log.debug(f"text_content: {text_content}") Files.update_file_data_by_id( file.id, - {"status": "completed", "content": text_content}, + {"content": text_content}, ) - hash = calculate_sha256_string(text_content) Files.update_file_hash_by_id(file.id, hash) - if not request.app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL: + if request.app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL: + Files.update_file_data_by_id(file.id, {"status": "completed"}) + return { + "status": True, + "collection_name": None, + "filename": file.filename, + "content": text_content, + } + else: try: result = save_docs_to_vector_db( request, @@ -1564,6 +1574,7 @@ def process_file( add=(True if form_data.collection_name else False), user=user, ) + log.info(f"added {len(docs)} items to collection {collection_name}") if result: Files.update_file_metadata_by_id( @@ -1573,21 +1584,21 @@ def process_file( }, ) + Files.update_file_data_by_id( + file.id, + {"status": "completed"}, + ) + return { "status": True, "collection_name": collection_name, "filename": file.filename, "content": text_content, } + else: + raise Exception("Error saving document to vector database") except Exception as e: raise e - else: - return { - "status": True, - "collection_name": None, - "filename": file.filename, - "content": text_content, - } except Exception as e: log.exception(e) diff --git a/backend/open_webui/routers/users.py b/backend/open_webui/routers/users.py index 5b331dce73..9a0f8c6aaf 100644 --- a/backend/open_webui/routers/users.py +++ b/backend/open_webui/routers/users.py @@ -18,6 +18,7 @@ from open_webui.models.users import ( UserModel, UserListResponse, UserInfoListResponse, + UserIdNameListResponse, UserRoleUpdateForm, Users, UserSettings, @@ -100,6 +101,23 @@ async def get_all_users( return Users.get_users() +@router.get("/search", response_model=UserIdNameListResponse) +async def search_users( + query: Optional[str] = None, + user=Depends(get_verified_user), +): + limit = PAGE_ITEM_COUNT + + page = 1 # Always return the first page for search + skip = (page - 1) * limit + + filter = {} + if query: + filter["query"] = query + + return Users.get_users(filter=filter, skip=skip, limit=limit) + + ############################ # User Groups ############################ diff --git a/backend/open_webui/utils/access_control.py b/backend/open_webui/utils/access_control.py index 1529773c44..6215a6ac22 100644 --- a/backend/open_webui/utils/access_control.py +++ b/backend/open_webui/utils/access_control.py @@ -130,9 +130,10 @@ def has_access( # Get all users with access to a resource def get_users_with_access( type: str = "write", access_control: Optional[dict] = None -) -> List[UserModel]: +) -> list[UserModel]: if access_control is None: - return Users.get_users() + result = Users.get_users() + return result.get("users", []) permission_access = access_control.get(type, {}) permitted_group_ids = permission_access.get("group_ids", []) diff --git a/backend/open_webui/utils/channels.py b/backend/open_webui/utils/channels.py new file mode 100644 index 0000000000..312b5ea24c --- /dev/null +++ b/backend/open_webui/utils/channels.py @@ -0,0 +1,31 @@ +import re + + +def extract_mentions(message: str, triggerChar: str = "@"): + # Escape triggerChar in case it's a regex special character + triggerChar = re.escape(triggerChar) + pattern = rf"<{triggerChar}([A-Z]):([^|>]+)" + + matches = re.findall(pattern, message) + return [{"id_type": id_type, "id": id_value} for id_type, id_value in matches] + + +def replace_mentions(message: str, triggerChar: str = "@", use_label: bool = True): + """ + Replace mentions in the message with either their label (after the pipe `|`) + or their id if no label exists. + + Example: + "<@M:gpt-4.1|GPT-4>" -> "GPT-4" (if use_label=True) + "<@M:gpt-4.1|GPT-4>" -> "gpt-4.1" (if use_label=False) + """ + # Escape triggerChar + triggerChar = re.escape(triggerChar) + + def replacer(match): + id_type, id_value, label = match.groups() + return label if use_label and label else id_value + + # Regex captures: idType, id, optional label + pattern = rf"<{triggerChar}([A-Z]):([^|>]+)(?:\|([^>]+))?>" + return re.sub(pattern, replacer, message) diff --git a/backend/open_webui/utils/filter.py b/backend/open_webui/utils/filter.py index 1986e55b64..663b4e3fb7 100644 --- a/backend/open_webui/utils/filter.py +++ b/backend/open_webui/utils/filter.py @@ -127,8 +127,10 @@ async def process_filter_functions( raise e # Handle file cleanup for inlet - if skip_files and "files" in form_data.get("metadata", {}): - del form_data["files"] - del form_data["metadata"]["files"] + if skip_files: + if "files" in form_data.get("metadata", {}): + del form_data["metadata"]["files"] + if "files" in form_data: + del form_data["files"] return form_data, {} diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index ae2c96c6da..97f19dcded 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -817,10 +817,11 @@ async def process_chat_payload(request, form_data, user, metadata, model): oauth_token = None try: - oauth_token = request.app.state.oauth_manager.get_oauth_token( - user.id, - request.cookies.get("oauth_session_id", None), - ) + if request.cookies.get("oauth_session_id", None): + oauth_token = request.app.state.oauth_manager.get_oauth_token( + user.id, + request.cookies.get("oauth_session_id", None), + ) except Exception as e: log.error(f"Error getting OAuth token: {e}") @@ -1130,11 +1131,11 @@ async def process_chat_response( request, response, form_data, user, metadata, model, events, tasks ): async def background_tasks_handler(): - message_map = Chats.get_messages_by_chat_id(metadata["chat_id"]) - message = message_map.get(metadata["message_id"]) if message_map else None + messages_map = Chats.get_messages_map_by_chat_id(metadata["chat_id"]) + message = messages_map.get(metadata["message_id"]) if messages_map else None if message: - message_list = get_message_list(message_map, metadata["message_id"]) + message_list = get_message_list(messages_map, metadata["message_id"]) # Remove details tags and files from the messages. # as get_message_list creates a new list, it does not affect @@ -1496,10 +1497,11 @@ async def process_chat_response( oauth_token = None try: - oauth_token = request.app.state.oauth_manager.get_oauth_token( - user.id, - request.cookies.get("oauth_session_id", None), - ) + if request.cookies.get("oauth_session_id", None): + oauth_token = request.app.state.oauth_manager.get_oauth_token( + user.id, + request.cookies.get("oauth_session_id", None), + ) except Exception as e: log.error(f"Error getting OAuth token: {e}") @@ -2029,6 +2031,20 @@ async def process_chat_response( ) else: choices = data.get("choices", []) + + # 17421 + usage = data.get("usage", {}) or {} + usage.update(data.get("timings", {})) # llama.cpp + if usage: + await event_emitter( + { + "type": "chat:completion", + "data": { + "usage": usage, + }, + } + ) + if not choices: error = data.get("error", {}) if error: @@ -2040,20 +2056,6 @@ async def process_chat_response( }, } ) - usage = data.get("usage", {}) - usage.update( - data.get("timing", {}) - ) # llama.cpp - - if usage: - await event_emitter( - { - "type": "chat:completion", - "data": { - "usage": usage, - }, - } - ) continue delta = choices[0].get("delta", {}) diff --git a/backend/open_webui/utils/misc.py b/backend/open_webui/utils/misc.py index 82729c34e0..370cf26c48 100644 --- a/backend/open_webui/utils/misc.py +++ b/backend/open_webui/utils/misc.py @@ -26,7 +26,7 @@ def deep_update(d, u): return d -def get_message_list(messages, message_id): +def get_message_list(messages_map, message_id): """ Reconstructs a list of messages in order up to the specified message_id. @@ -36,11 +36,11 @@ def get_message_list(messages, message_id): """ # Handle case where messages is None - if not messages: + if not messages_map: return [] # Return empty list instead of None to prevent iteration errors # Find the message by its id - current_message = messages.get(message_id) + current_message = messages_map.get(message_id) if not current_message: return [] # Return empty list instead of None to prevent iteration errors @@ -53,7 +53,7 @@ def get_message_list(messages, message_id): 0, current_message ) # Insert the message at the beginning of the list parent_id = current_message.get("parentId") # Use .get() for safety - current_message = messages.get(parent_id) if parent_id else None + current_message = messages_map.get(parent_id) if parent_id else None return message_list diff --git a/backend/open_webui/utils/models.py b/backend/open_webui/utils/models.py index b713b84307..7e69661f56 100644 --- a/backend/open_webui/utils/models.py +++ b/backend/open_webui/utils/models.py @@ -22,10 +22,11 @@ from open_webui.utils.access_control import has_access from open_webui.config import ( + BYPASS_ADMIN_ACCESS_CONTROL, DEFAULT_ARENA_MODEL, ) -from open_webui.env import SRC_LOG_LEVELS, GLOBAL_LOG_LEVEL +from open_webui.env import BYPASS_MODEL_ACCESS_CONTROL, SRC_LOG_LEVELS, GLOBAL_LOG_LEVEL from open_webui.models.users import UserModel @@ -332,3 +333,40 @@ def check_model_access(user, model): ) ): raise Exception("Model not found") + + +def get_filtered_models(models, user): + # Filter out models that the user does not have access to + if ( + user.role == "user" + or (user.role == "admin" and not BYPASS_ADMIN_ACCESS_CONTROL) + ) and not BYPASS_MODEL_ACCESS_CONTROL: + filtered_models = [] + for model in models: + if model.get("arena"): + if has_access( + user.id, + type="read", + access_control=model.get("info", {}) + .get("meta", {}) + .get("access_control", {}), + ): + filtered_models.append(model) + continue + + model_info = Models.get_model_by_id(model["id"]) + if model_info: + if ( + (user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL) + or user.id == model_info.user_id + or has_access( + user.id, + type="read", + access_control=model_info.access_control, + ) + ): + filtered_models.append(model) + + return filtered_models + else: + return models diff --git a/backend/open_webui/utils/oauth.py b/backend/open_webui/utils/oauth.py index 7eedc30c31..9090c38ce5 100644 --- a/backend/open_webui/utils/oauth.py +++ b/backend/open_webui/utils/oauth.py @@ -602,6 +602,12 @@ class OAuthManager: or (auth_manager_config.OAUTH_USERNAME_CLAIM not in user_data) ): user_data: UserInfo = await client.userinfo(token=token) + if ( + provider == "feishu" + and isinstance(user_data, dict) + and "data" in user_data + ): + user_data = user_data["data"] if not user_data: log.warning(f"OAuth callback failed, user data is missing: {token}") raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED) diff --git a/backend/open_webui/utils/telemetry/metrics.py b/backend/open_webui/utils/telemetry/metrics.py index 75c13ccc0a..c7b47c0231 100644 --- a/backend/open_webui/utils/telemetry/metrics.py +++ b/backend/open_webui/utils/telemetry/metrics.py @@ -163,20 +163,27 @@ def setup_metrics(app: FastAPI, resource: Resource) -> None: @app.middleware("http") async def _metrics_middleware(request: Request, call_next): start_time = time.perf_counter() - response = await call_next(request) - elapsed_ms = (time.perf_counter() - start_time) * 1000.0 - # Route template e.g. "/items/{item_id}" instead of real path. - route = request.scope.get("route") - route_path = getattr(route, "path", request.url.path) + status_code = None + try: + response = await call_next(request) + status_code = getattr(response, "status_code", 500) + return response + except Exception: + status_code = 500 + raise + finally: + elapsed_ms = (time.perf_counter() - start_time) * 1000.0 - attrs: Dict[str, str | int] = { - "http.method": request.method, - "http.route": route_path, - "http.status_code": response.status_code, - } + # Route template e.g. "/items/{item_id}" instead of real path. + route = request.scope.get("route") + route_path = getattr(route, "path", request.url.path) - request_counter.add(1, attrs) - duration_histogram.record(elapsed_ms, attrs) + attrs: Dict[str, str | int] = { + "http.method": request.method, + "http.route": route_path, + "http.status_code": status_code, + } - return response + request_counter.add(1, attrs) + duration_histogram.record(elapsed_ms, attrs) diff --git a/package-lock.json b/package-lock.json index 48122812b8..b9b9be7a2e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "open-webui", - "version": "0.6.28", + "version": "0.6.29", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "open-webui", - "version": "0.6.28", + "version": "0.6.29", "dependencies": { "@azure/msal-browser": "^4.5.0", "@codemirror/lang-javascript": "^6.2.2", @@ -37,6 +37,7 @@ "@tiptap/extensions": "^3.0.7", "@tiptap/pm": "^3.0.7", "@tiptap/starter-kit": "^3.0.7", + "@tiptap/suggestion": "^3.4.2", "@xyflow/svelte": "^0.1.19", "async": "^3.2.5", "bits-ui": "^0.21.15", @@ -86,7 +87,6 @@ "socket.io-client": "^4.2.0", "sortablejs": "^1.15.6", "svelte-sonner": "^0.3.19", - "svelte-tiptap": "^3.0.0", "tippy.js": "^6.3.7", "turndown": "^7.2.0", "turndown-plugin-gfm": "^1.0.2", @@ -3856,18 +3856,17 @@ } }, "node_modules/@tiptap/suggestion": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@tiptap/suggestion/-/suggestion-3.0.9.tgz", - "integrity": "sha512-irthqfUybezo3IwR6AXvyyTOtkzwfvvst58VXZtTnR1nN6NEcrs3TQoY3bGKGbN83bdiquKh6aU2nLnZfAhoXg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@tiptap/suggestion/-/suggestion-3.4.2.tgz", + "integrity": "sha512-sljtfiDtdAsbPOwrXrFGf64D6sXUjeU3Iz5v3TvN7TVJKozkZ/gaMkPRl+WC1CGwC6BnzQVDBEEa1e+aApV0mA==", "license": "MIT", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^3.0.9", - "@tiptap/pm": "^3.0.9" + "@tiptap/core": "^3.4.2", + "@tiptap/pm": "^3.4.2" } }, "node_modules/@tiptap/y-tiptap": { @@ -12503,26 +12502,6 @@ "svelte": "^3.0.0 || ^4.0.0 || ^5.0.0-next.1" } }, - "node_modules/svelte-tiptap": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/svelte-tiptap/-/svelte-tiptap-3.0.0.tgz", - "integrity": "sha512-digFHOJe16RX0HIU+u8hOaCS9sIgktTpYHSF9yJ6dgxPv/JWJdYCdwoX65lcHitFhhCG7xnolJng6PJa9M9h3w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "peerDependencies": { - "@floating-ui/dom": "^1.0.0", - "@tiptap/core": "^3.0.0", - "@tiptap/extension-bubble-menu": "^3.0.0", - "@tiptap/extension-floating-menu": "^3.0.0", - "@tiptap/pm": "^3.0.0", - "svelte": "^5.0.0" - } - }, "node_modules/svelte/node_modules/estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", diff --git a/package.json b/package.json index 054baf39c6..c5068f0366 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "open-webui", - "version": "0.6.28", + "version": "0.6.29", "private": true, "scripts": { "dev": "npm run pyodide:fetch && vite dev --host", @@ -81,6 +81,7 @@ "@tiptap/extensions": "^3.0.7", "@tiptap/pm": "^3.0.7", "@tiptap/starter-kit": "^3.0.7", + "@tiptap/suggestion": "^3.4.2", "@xyflow/svelte": "^0.1.19", "async": "^3.2.5", "bits-ui": "^0.21.15", @@ -130,7 +131,6 @@ "socket.io-client": "^4.2.0", "sortablejs": "^1.15.6", "svelte-sonner": "^0.3.19", - "svelte-tiptap": "^3.0.0", "tippy.js": "^6.3.7", "turndown": "^7.2.0", "turndown-plugin-gfm": "^1.0.2", diff --git a/pyproject.toml b/pyproject.toml index a0e7e4a095..4bbaf85b6b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,18 +56,11 @@ dependencies = [ "fake-useragent==2.2.0", "chromadb==1.0.20", - "pymilvus==2.5.0", - "qdrant-client==1.14.3", "opensearch-py==2.8.0", - "playwright==1.49.1", - "elasticsearch==9.1.0", - "pinecone==6.0.2", - "oracledb==3.2.0", - + "transformers", "sentence-transformers==4.1.0", "accelerate", - "colbert-ai==0.2.21", "pyarrow==20.0.0", "einops==0.8.1", @@ -154,6 +147,15 @@ all = [ "docker~=7.1.0", "pytest~=8.3.2", "pytest-docker~=3.1.1", + "playwright==1.49.1", + "elasticsearch==9.1.0", + + "qdrant-client==1.14.3", + "pymilvus==2.5.0", + "pinecone==6.0.2", + "oracledb==3.2.0", + + "colbert-ai==0.2.21", ] [project.scripts] diff --git a/src/app.css b/src/app.css index c48914febf..af030d8350 100644 --- a/src/app.css +++ b/src/app.css @@ -70,23 +70,23 @@ textarea::placeholder { } .input-prose { - @apply prose dark:prose-invert prose-headings:font-semibold prose-hr:my-4 prose-hr:border-gray-100 prose-hr:dark:border-gray-800 prose-p:my-1 prose-img:my-1 prose-headings:my-2 prose-pre:my-0 prose-table:my-1 prose-blockquote:my-0 prose-ul:my-1 prose-ol:my-1 prose-li:my-0.5 whitespace-pre-line; + @apply prose dark:prose-invert prose-headings:font-semibold prose-hr:my-4 prose-hr:border-gray-50 prose-hr:dark:border-gray-850 prose-p:my-1 prose-img:my-1 prose-headings:my-2 prose-pre:my-0 prose-table:my-1 prose-blockquote:my-0 prose-ul:my-1 prose-ol:my-1 prose-li:my-0.5 whitespace-pre-line; } .input-prose-sm { - @apply prose dark:prose-invert prose-headings:font-medium prose-h1:text-2xl prose-h2:text-xl prose-h3:text-lg prose-hr:my-4 prose-hr:border-gray-100 prose-hr:dark:border-gray-800 prose-p:my-1 prose-img:my-1 prose-headings:my-2 prose-pre:my-0 prose-table:my-1 prose-blockquote:my-0 prose-ul:my-1 prose-ol:my-1 prose-li:my-1 whitespace-pre-line text-sm; + @apply prose dark:prose-invert prose-headings:font-medium prose-h1:text-2xl prose-h2:text-xl prose-h3:text-lg prose-hr:my-4 prose-hr:border-gray-50 prose-hr:dark:border-gray-850 prose-p:my-1 prose-img:my-1 prose-headings:my-2 prose-pre:my-0 prose-table:my-1 prose-blockquote:my-0 prose-ul:my-1 prose-ol:my-1 prose-li:my-1 whitespace-pre-line text-sm; } .markdown-prose { - @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; + @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-50 prose-hr:dark:border-gray-850 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-sm { - @apply text-sm 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-2 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-sm 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-2 prose-hr:border-gray-50 prose-hr:dark:border-gray-850 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-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.5 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.5 prose-hr:border-gray-50 prose-hr:dark:border-gray-850 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 { @@ -409,17 +409,33 @@ input[type='number'] { } } -.tiptap .mention { +.mention { border-radius: 0.4rem; box-decoration-break: clone; padding: 0.1rem 0.3rem; - @apply text-blue-900 dark:text-blue-100 bg-blue-300/20 dark:bg-blue-500/20; + @apply text-sky-800 dark:text-sky-200 bg-sky-300/15 dark:bg-sky-500/15; } -.tiptap .mention::after { +.mention::after { content: '\200B'; } +.tiptap .suggestion { + border-radius: 0.4rem; + box-decoration-break: clone; + padding: 0.1rem 0.3rem; + @apply text-sky-800 dark:text-sky-200 bg-sky-300/15 dark:bg-sky-500/15; +} + +.tiptap .suggestion::after { + content: '\200B'; +} + +.tiptap .suggestion.is-empty::after { + content: '\00A0'; + border-bottom: 1px dotted rgba(31, 41, 55, 0.12); +} + .input-prose .tiptap ul[data-type='taskList'] { list-style: none; margin-left: 0; diff --git a/src/app.html b/src/app.html index f7167d42f2..6c1c362005 100644 --- a/src/app.html +++ b/src/app.html @@ -23,8 +23,6 @@ href="/static/apple-touch-icon.png" crossorigin="use-credentials" /> - - - - + diff --git a/src/lib/apis/notes/index.ts b/src/lib/apis/notes/index.ts index 965c4217ed..61794f6766 100644 --- a/src/lib/apis/notes/index.ts +++ b/src/lib/apis/notes/index.ts @@ -91,10 +91,15 @@ export const getNotes = async (token: string = '', raw: boolean = false) => { return grouped; }; -export const getNoteList = async (token: string = '') => { +export const getNoteList = async (token: string = '', page: number | null = null) => { let error = null; + const searchParams = new URLSearchParams(); - const res = await fetch(`${WEBUI_API_BASE_URL}/notes/list`, { + if (page !== null) { + searchParams.append('page', `${page}`); + } + + const res = await fetch(`${WEBUI_API_BASE_URL}/notes/list?${searchParams.toString()}`, { method: 'GET', headers: { Accept: 'application/json', diff --git a/src/lib/apis/users/index.ts b/src/lib/apis/users/index.ts index bdb44f2627..ac057359a5 100644 --- a/src/lib/apis/users/index.ts +++ b/src/lib/apis/users/index.ts @@ -194,6 +194,34 @@ export const getAllUsers = async (token: string) => { return res; }; +export const searchUsers = async (token: string, query: string) => { + let error = null; + let res = null; + + res = await fetch(`${WEBUI_API_BASE_URL}/users/search?query=${encodeURIComponent(query)}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.error(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + export const getUserSettings = async (token: string) => { let error = null; const res = await fetch(`${WEBUI_API_BASE_URL}/users/user/settings`, { diff --git a/src/lib/components/AddConnectionModal.svelte b/src/lib/components/AddConnectionModal.svelte index fb4da3175f..240df839a8 100644 --- a/src/lib/components/AddConnectionModal.svelte +++ b/src/lib/components/AddConnectionModal.svelte @@ -122,7 +122,7 @@ return; } - if (!key) { + if (!key && !['azure_ad', 'microsoft_entra_id'].includes(auth_type)) { loading = false; toast.error($i18n.t('Key is required')); @@ -331,6 +331,9 @@ {#if !direct} + {#if azure} + + {/if} {/if} {/if} @@ -361,6 +364,12 @@ > {$i18n.t('Forwards system user OAuth access token to authenticate')} + {:else if ['azure_ad', 'microsoft_entra_id'].includes(auth_type)} +
+ {$i18n.t('Uses DefaultAzureCredential to authenticate')} +
{/if} @@ -443,7 +452,7 @@ {/if} -
+
-
-
-
- -
+
-
📄
-
+
{#if title} {title} {:else} @@ -17,7 +16,7 @@
+ >
{#if content} {content} {:else} diff --git a/src/lib/components/NotificationToast.svelte b/src/lib/components/NotificationToast.svelte index bfb1ff691f..1b8d9fae8b 100644 --- a/src/lib/components/NotificationToast.svelte +++ b/src/lib/components/NotificationToast.svelte @@ -12,6 +12,43 @@ export let title: string = 'HI'; export let content: string; + let startX = 0, + startY = 0; + let moved = false; + const DRAG_THRESHOLD_PX = 6; + + const clickHandler = () => { + onClick(); + dispatch('closeToast'); + }; + + function onPointerDown(e: PointerEvent) { + startX = e.clientX; + startY = e.clientY; + moved = false; + // Ensure we continue to get events even if the toast moves under the pointer. + (e.currentTarget as HTMLElement).setPointerCapture?.(e.pointerId); + } + + function onPointerMove(e: PointerEvent) { + if (moved) return; + const dx = e.clientX - startX; + const dy = e.clientY - startY; + if (dx * dx + dy * dy > DRAG_THRESHOLD_PX * DRAG_THRESHOLD_PX) { + moved = true; + } + } + + function onPointerUp(e: PointerEvent) { + // Release capture if taken + (e.currentTarget as HTMLElement).releasePointerCapture?.(e.pointerId); + + // Only treat as a click if there wasn't a drag + if (!moved) { + clickHandler(); + } + } + onMount(() => { if (!navigator.userActivation.hasBeenActive) { return; @@ -31,24 +68,33 @@ }); - +
diff --git a/src/lib/components/admin/Evaluations.svelte b/src/lib/components/admin/Evaluations.svelte index d223db57ce..d29dee746c 100644 --- a/src/lib/components/admin/Evaluations.svelte +++ b/src/lib/components/admin/Evaluations.svelte @@ -56,7 +56,7 @@
-
+
{#if selectedTab === 'leaderboard'} {:else if selectedTab === 'feedbacks'} diff --git a/src/lib/components/admin/Evaluations/FeedbackMenu.svelte b/src/lib/components/admin/Evaluations/FeedbackMenu.svelte index fa24467a47..515408e463 100644 --- a/src/lib/components/admin/Evaluations/FeedbackMenu.svelte +++ b/src/lib/components/admin/Evaluations/FeedbackMenu.svelte @@ -13,7 +13,7 @@ import GarbageBin from '$lib/components/icons/GarbageBin.svelte'; import Pencil from '$lib/components/icons/Pencil.svelte'; import Tooltip from '$lib/components/common/Tooltip.svelte'; - import Download from '$lib/components/icons/ArrowDownTray.svelte'; + import Download from '$lib/components/icons/Download.svelte'; let show = false; @@ -25,7 +25,7 @@
-
+
{$i18n.t('Feedback History')} @@ -187,31 +187,25 @@ exportHandler(); }} > - +
{/if}
-
+
{#if (feedbacks ?? []).length === 0}
{$i18n.t('No feedbacks found')}
{:else} - - - +
+ + - + diff --git a/src/lib/components/admin/Evaluations/Leaderboard.svelte b/src/lib/components/admin/Evaluations/Leaderboard.svelte index ce6c526638..db46729b36 100644 --- a/src/lib/components/admin/Evaluations/Leaderboard.svelte +++ b/src/lib/components/admin/Evaluations/Leaderboard.svelte @@ -1,9 +1,4 @@ - +{#if loaded} + -{#if acceptFiles} - { - if (inputFiles && inputFiles.length > 0) { - inputFilesHandler(Array.from(inputFiles)); - } else { - toast.error($i18n.t(`File not found.`)); - } + {#if acceptFiles} + { + if (inputFiles && inputFiles.length > 0) { + inputFilesHandler(Array.from(inputFiles)); + } else { + toast.error($i18n.t(`File not found.`)); + } - filesInputElement.value = ''; - }} + filesInputElement.value = ''; + }} + /> + {/if} + + -{/if} - { - inputVariableValues = { ...inputVariableValues, ...variableValues }; - replaceVariables(inputVariableValues); - }} -/> - -
-
-
-
-
- {#if scrollEnd === false} -
- -
- {/if} -
- -
-
- {#if typingUsers.length > 0} -
- - {typingUsers.map((user) => user.name).join(', ')} - - {$i18n.t('is typing...')} + + + +
{/if}
- + {#if typingUsers.length > 0} +
+
+ + +
+ + {typingUsers.map((user) => user.name).join(', ')} + + {$i18n.t('is typing...')} +
+
+
+ {/if}
-
-
- {#if recording} - { - recording = false; +
+ {#if recording} + { + recording = false; - await tick(); + await tick(); - if (chatInputElement) { - chatInputElement.focus(); - } - }} - onConfirm={async (data) => { - const { text, filename } = data; - recording = false; + if (chatInputElement) { + chatInputElement.focus(); + } + }} + onConfirm={async (data) => { + const { text, filename } = data; + recording = false; - await tick(); - insertTextAtCursor(text); + await tick(); + insertTextAtCursor(text); - await tick(); + await tick(); - if (chatInputElement) { - chatInputElement.focus(); - } - }} - /> - {:else} -
{ - submitHandler(); - }} - > -
+ {:else} + { + submitHandler(); + }} > - {#if files.length > 0} -
- {#each files as file, fileIdx} - {#if file.type === 'image'} -
-
- +
+ {#if files.length > 0} +
+ {#each files as file, fileIdx} + {#if file.type === 'image'} +
+
+ +
+
+ +
-
+ {:else} + { + files.splice(fileIdx, 1); + files = files; + }} + on:click={() => { + console.log(file); + }} + /> + {/if} + {/each} +
+ {/if} + +
+
+ {#key $settings?.richTextInput} + 0 || + navigator.msMaxTouchPoints > 0 + ))} + largeTextAsFile={$settings?.largeTextAsFile ?? false} + floatingMenuPlacement={'top-start'} + {suggestions} + onChange={(e) => { + const { md } = e; + content = md; + command = getCommand(); + }} + on:keydown={async (e) => { + e = e.detail.event; + const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac + + const suggestionsContainerElement = + document.getElementById('suggestions-container'); + + if (!suggestionsContainerElement) { + if ( + !$mobile || + !( + 'ontouchstart' in window || + navigator.maxTouchPoints > 0 || + navigator.msMaxTouchPoints > 0 + ) + ) { + // Prevent Enter key from creating a new line + // Uses keyCode '13' for Enter key for chinese/japanese keyboards + if (e.keyCode === 13 && !e.shiftKey) { + e.preventDefault(); + } + + // Submit the content when Enter key is pressed + if (content !== '' && e.keyCode === 13 && !e.shiftKey) { + submitHandler(); + } + } + } + + if (e.key === 'Escape') { + console.info('Escape'); + } + }} + on:paste={async (e) => { + e = e.detail.event; + console.log(e); + + const clipboardData = e.clipboardData || window.clipboardData; + + if (clipboardData && clipboardData.items) { + for (const item of clipboardData.items) { + if (item.type.indexOf('image') !== -1) { + const blob = item.getAsFile(); + const reader = new FileReader(); + + reader.onload = function (e) { + files = [ + ...files, + { + type: 'image', + url: `${e.target.result}` + } + ]; + }; + + reader.readAsDataURL(blob); + } else if (item?.kind === 'file') { + const file = item.getAsFile(); + if (file) { + const _files = [file]; + await inputFilesHandler(_files); + e.preventDefault(); + } + } + } + } + }} + /> + {/key} +
+
+ +
+
+ + {#if acceptFiles} + { + filesInputElement.click(); + }} + > -
-
- {:else} - { - files.splice(fileIdx, 1); - files = files; - }} - on:click={() => { - console.log(file); - }} - /> - {/if} - {/each} -
- {/if} + + {/if} + +
-
-
- 0 || - navigator.msMaxTouchPoints > 0 - ))} - largeTextAsFile={$settings?.largeTextAsFile ?? false} - floatingMenuPlacement={'top-start'} - onChange={(e) => { - const { md } = e; - content = md; - command = getCommand(); - }} - on:keydown={async (e) => { - e = e.detail.event; - const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac - - const commandsContainerElement = document.getElementById('commands-container'); - - if (commandsContainerElement) { - if (commandsContainerElement && e.key === 'ArrowUp') { - e.preventDefault(); - commandsElement.selectUp(); - - const commandOptionButton = [ - ...document.getElementsByClassName('selected-command-option-button') - ]?.at(-1); - commandOptionButton.scrollIntoView({ block: 'center' }); - } - - if (commandsContainerElement && e.key === 'ArrowDown') { - e.preventDefault(); - commandsElement.selectDown(); - - const commandOptionButton = [ - ...document.getElementsByClassName('selected-command-option-button') - ]?.at(-1); - commandOptionButton.scrollIntoView({ block: 'center' }); - } - - if (commandsContainerElement && e.key === 'Tab') { - e.preventDefault(); - - const commandOptionButton = [ - ...document.getElementsByClassName('selected-command-option-button') - ]?.at(-1); - - commandOptionButton?.click(); - } - - if (commandsContainerElement && e.key === 'Enter') { - e.preventDefault(); - - const commandOptionButton = [ - ...document.getElementsByClassName('selected-command-option-button') - ]?.at(-1); - - if (commandOptionButton) { - commandOptionButton?.click(); - } else { - document.getElementById('send-message-button')?.click(); - } - } - } else { - if ( - !$mobile || - !( - 'ontouchstart' in window || - navigator.maxTouchPoints > 0 || - navigator.msMaxTouchPoints > 0 - ) - ) { - // Prevent Enter key from creating a new line - // Uses keyCode '13' for Enter key for chinese/japanese keyboards - if (e.keyCode === 13 && !e.shiftKey) { - e.preventDefault(); - } - - // Submit the content when Enter key is pressed - if (content !== '' && e.keyCode === 13 && !e.shiftKey) { - submitHandler(); - } - } - } - - if (e.key === 'Escape') { - console.info('Escape'); - } - }} - on:paste={async (e) => { - e = e.detail.event; - console.info(e); - }} - /> -
-
- -
-
- - {#if acceptFiles} - { - filesInputElement.click(); - }} - > +
+ {#if content === ''} + - + {/if} - -
-
- {#if content === ''} - - - - {/if} - -
- {#if inputLoading && onStop} -
- - - -
- {:else} -
- - + +
+ {:else} +
+ + - -
- {/if} + + + + + +
+ {/if} +
-
- - {/if} + + {/if} +
-
+{/if} diff --git a/src/lib/components/channel/MessageInput/InputMenu.svelte b/src/lib/components/channel/MessageInput/InputMenu.svelte index 7226c34cb9..939b79ce3c 100644 --- a/src/lib/components/channel/MessageInput/InputMenu.svelte +++ b/src/lib/components/channel/MessageInput/InputMenu.svelte @@ -13,6 +13,8 @@ import GlobeAltSolid from '$lib/components/icons/GlobeAltSolid.svelte'; import WrenchSolid from '$lib/components/icons/WrenchSolid.svelte'; import CameraSolid from '$lib/components/icons/CameraSolid.svelte'; + import Camera from '$lib/components/icons/Camera.svelte'; + import Clip from '$lib/components/icons/Clip.svelte'; const i18n = getContext('i18n'); @@ -44,34 +46,32 @@
- {#if !$mobile} - { - screenCaptureHandler(); - }} - > - -
{$i18n.t('Capture')}
-
- {/if} - { uploadFilesHandler(); }} > - +
{$i18n.t('Upload Files')}
+ + { + screenCaptureHandler(); + }} + > + +
{$i18n.t('Capture')}
+
diff --git a/src/lib/components/channel/MessageInput/MentionList.svelte b/src/lib/components/channel/MessageInput/MentionList.svelte new file mode 100644 index 0000000000..30ba8f7513 --- /dev/null +++ b/src/lib/components/channel/MessageInput/MentionList.svelte @@ -0,0 +1,205 @@ + + +{#if filteredItems.length} +
+
+ {#each filteredItems as item, i} + {#if i === 0 || item?.type !== filteredItems[i - 1]?.type} +
+ {#if item?.type === 'user'} + {$i18n.t('Users')} + {:else if item?.type === 'model'} + {$i18n.t('Models')} + {:else if item?.type === 'channel'} + {$i18n.t('Channels')} + {/if} +
+ {/if} + + + + + {/each} +
+
+{/if} diff --git a/src/lib/components/channel/Messages.svelte b/src/lib/components/channel/Messages.svelte index e95a6e100d..540891b500 100644 --- a/src/lib/components/channel/Messages.svelte +++ b/src/lib/components/channel/Messages.svelte @@ -63,11 +63,7 @@
{:else if !thread} -
+
{#if channel}
{channel.name}
@@ -99,7 +95,8 @@ {message} {thread} showUserProfile={messageIdx === 0 || - messageList.at(messageIdx - 1)?.user_id !== message.user_id} + messageList.at(messageIdx - 1)?.user_id !== message.user_id || + messageList.at(messageIdx - 1)?.meta?.model_id !== message?.meta?.model_id} onDelete={() => { messages = messages.filter((m) => m.id !== message.id); diff --git a/src/lib/components/channel/Messages/Message.svelte b/src/lib/components/channel/Messages/Message.svelte index 649529a6f9..4ea6a67aea 100644 --- a/src/lib/components/channel/Messages/Message.svelte +++ b/src/lib/components/channel/Messages/Message.svelte @@ -15,7 +15,7 @@ import { settings, user, shortCodesToEmojis } from '$lib/stores'; - import { WEBUI_BASE_URL } from '$lib/constants'; + import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants'; import Markdown from '$lib/components/chat/Messages/Markdown.svelte'; import ProfileImage from '$lib/components/chat/Messages/ProfileImage.svelte'; @@ -34,6 +34,8 @@ import ChevronRight from '$lib/components/icons/ChevronRight.svelte'; import { formatDate } from '$lib/utils'; import Emoji from '$lib/components/common/Emoji.svelte'; + import { t } from 'i18next'; + import Skeleton from '$lib/components/chat/Messages/Skeleton.svelte'; export let message; export let showUserProfile = true; @@ -64,9 +66,7 @@
{#if !edit}
-
+
{#if showUserProfile} - - - + {:else} + + + + {/if} {:else} @@ -170,7 +173,11 @@ {#if showUserProfile}
- {message?.user?.name} + {#if message?.meta?.model_id} + {message?.meta?.model_name ?? message?.meta?.model_id} + {:else} + {message?.user?.name} + {/if}
{#if message.created_at} @@ -178,7 +185,12 @@ class=" self-center text-xs invisible group-hover:visible text-gray-400 font-medium first-letter:capitalize ml-0.5 translate-y-[1px]" > - {formatDate(message.created_at / 1000000)} + + {$i18n.t(formatDate(message.created_at / 1000000), { + LOCALIZED_TIME: dayjs(message.created_at / 1000000).format('LT'), + LOCALIZED_DATE: dayjs(message.created_at / 1000000).format('L') + })} +
{/if} @@ -198,7 +210,7 @@ name={file.name} type={file.type} size={file?.size} - colorClassName="bg-white dark:bg-gray-850 " + small={true} /> {/if}
@@ -228,7 +240,7 @@
{:else}
- {#if message.created_at !== message.updated_at}(edited){/if} + {#if (message?.content ?? '').trim() === '' && message?.meta?.model_id} + + {:else} + {#if message.created_at !== message.updated_at && (message?.meta?.model_id ?? null) === null}({$i18n.t('edited')}){/if} + {/if}
{#if (message?.reactions ?? []).length > 0} diff --git a/src/lib/components/channel/Messages/Message/ProfilePreview.svelte b/src/lib/components/channel/Messages/Message/ProfilePreview.svelte index c4286db9a4..620905e5ff 100644 --- a/src/lib/components/channel/Messages/Message/ProfilePreview.svelte +++ b/src/lib/components/channel/Messages/Message/ProfilePreview.svelte @@ -1,101 +1,18 @@ - {}} - typeahead={false} -> - + + - + - - - {#if user} -
-
- profile -
- -
-
- {user.name} -
- -
- {#if active} -
- - - - -
- -
- {$i18n.t('Active')} -
- {:else} -
- - - -
- -
- {$i18n.t('Away')} -
- {/if} -
-
-
- {/if} -
-
-
+ + diff --git a/src/lib/components/channel/Messages/Message/UserStatus.svelte b/src/lib/components/channel/Messages/Message/UserStatus.svelte new file mode 100644 index 0000000000..689a4d5f54 --- /dev/null +++ b/src/lib/components/channel/Messages/Message/UserStatus.svelte @@ -0,0 +1,50 @@ + + +{#if user} +
+
+ profile +
+ +
+
+ {user.name} +
+ +
+ {#if user?.active} +
+ + + + +
+ + {$i18n.t('Active')} + {:else} +
+ + + +
+ + {$i18n.t('Away')} + {/if} +
+
+
+{/if} diff --git a/src/lib/components/channel/Messages/Message/UserStatusLinkPreview.svelte b/src/lib/components/channel/Messages/Message/UserStatusLinkPreview.svelte new file mode 100644 index 0000000000..0660548891 --- /dev/null +++ b/src/lib/components/channel/Messages/Message/UserStatusLinkPreview.svelte @@ -0,0 +1,37 @@ + + +{#if user} + + + +{/if} diff --git a/src/lib/components/channel/Thread.svelte b/src/lib/components/channel/Thread.svelte index b6ff4f42a7..dd20097fae 100644 --- a/src/lib/components/channel/Thread.svelte +++ b/src/lib/components/channel/Thread.svelte @@ -159,7 +159,7 @@ {#if channel}
-
+
{$i18n.t('Thread')}
@@ -174,7 +174,7 @@
-
+
-
- +
+
diff --git a/src/lib/components/chat/Artifacts.svelte b/src/lib/components/chat/Artifacts.svelte index bbe2132b90..848d81f635 100644 --- a/src/lib/components/chat/Artifacts.svelte +++ b/src/lib/components/chat/Artifacts.svelte @@ -12,7 +12,7 @@ import Tooltip from '../common/Tooltip.svelte'; import SvgPanZoom from '../common/SVGPanZoom.svelte'; import ArrowLeft from '../icons/ArrowLeft.svelte'; - import ArrowDownTray from '../icons/ArrowDownTray.svelte'; + import Download from '../icons/Download.svelte'; export let overlay = false; export let history; @@ -205,7 +205,7 @@
@@ -213,15 +213,6 @@
- -
@@ -294,7 +285,7 @@ class=" bg-none border-none text-xs bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-md p-0.5" on:click={downloadArtifact} > - + diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte index e0b9a0a3c0..e987733221 100644 --- a/src/lib/components/chat/Chat.svelte +++ b/src/lib/components/chat/Chat.svelte @@ -1,7 +1,6 @@ + +
+
+ {#if !loading} + {#if char === '/'} + { + const { type, data } = e; + + if (type === 'prompt') { + insertTextHandler(data.content); + } + }} + /> + {:else if char === '#'} + { + const { type, data } = e; + + if (type === 'knowledge') { + insertTextHandler(''); + + onUpload({ + type: 'file', + data: data + }); + } else if (type === 'youtube') { + insertTextHandler(''); + + onUpload({ + type: 'youtube', + data: data + }); + } else if (type === 'web') { + insertTextHandler(''); + + onUpload({ + type: 'web', + data: data + }); + } + }} + /> + {:else if char === '@'} + { + const { type, data } = e; + + if (type === 'model') { + insertTextHandler(''); + + onSelect({ + type: 'model', + data: data + }); + } + }} + /> + {/if} + {:else} +
+ +
+ {/if} +
+
diff --git a/src/lib/components/chat/MessageInput/Commands.svelte b/src/lib/components/chat/MessageInput/Commands.svelte deleted file mode 100644 index af71458522..0000000000 --- a/src/lib/components/chat/MessageInput/Commands.svelte +++ /dev/null @@ -1,129 +0,0 @@ - - -{#if show} - {#if !loading} - {#if command?.charAt(0) === '/'} - { - const { type, data } = e; - - if (type === 'prompt') { - insertTextHandler(data.content); - } - }} - /> - {:else if (command?.charAt(0) === '#' && command.startsWith('#') && !command.includes('# ')) || ('\\#' === command.slice(0, 2) && command.startsWith('#') && !command.includes('# '))} - { - const { type, data } = e; - - if (type === 'knowledge') { - insertTextHandler(''); - - onUpload({ - type: 'file', - data: data - }); - } else if (type === 'youtube') { - insertTextHandler(''); - - onUpload({ - type: 'youtube', - data: data - }); - } else if (type === 'web') { - insertTextHandler(''); - - onUpload({ - type: 'web', - data: data - }); - } - }} - /> - {:else if command?.charAt(0) === '@'} - { - const { type, data } = e; - - if (type === 'model') { - insertTextHandler(''); - - onSelect({ - type: 'model', - data: data - }); - } - }} - /> - {/if} - {:else} -
-
-
- -
-
-
- {/if} -{/if} diff --git a/src/lib/components/chat/MessageInput/Commands/Knowledge.svelte b/src/lib/components/chat/MessageInput/Commands/Knowledge.svelte index 781437e86e..5a6ce96cc4 100644 --- a/src/lib/components/chat/MessageInput/Commands/Knowledge.svelte +++ b/src/lib/components/chat/MessageInput/Commands/Knowledge.svelte @@ -8,29 +8,48 @@ import { tick, getContext, onMount, onDestroy } from 'svelte'; import { removeLastWordFromString, isValidHttpUrl } from '$lib/utils'; - import { knowledge } from '$lib/stores'; - import { getNoteList, getNotes } from '$lib/apis/notes'; + import Tooltip from '$lib/components/common/Tooltip.svelte'; + import DocumentPage from '$lib/components/icons/DocumentPage.svelte'; + import Database from '$lib/components/icons/Database.svelte'; + import GlobeAlt from '$lib/components/icons/GlobeAlt.svelte'; + import Youtube from '$lib/components/icons/Youtube.svelte'; const i18n = getContext('i18n'); - export let command = ''; + export let query = ''; export let onSelect = (e) => {}; + export let knowledge = []; + let selectedIdx = 0; let items = []; let fuse = null; - let filteredItems = []; + export let filteredItems = []; $: if (fuse) { - filteredItems = command.slice(1) - ? fuse.search(command).map((e) => { - return e.item; - }) - : items; + filteredItems = [ + ...(query + ? fuse.search(query).map((e) => { + return e.item; + }) + : items), + + ...(query.startsWith('http') + ? query.startsWith('https://www.youtube.com') || query.startsWith('https://youtu.be') + ? [{ type: 'youtube', name: query, description: query }] + : [ + { + type: 'web', + name: query, + description: query + } + ] + : []) + ]; } - $: if (command) { + $: if (query) { selectedIdx = 0; } @@ -42,32 +61,14 @@ selectedIdx = Math.min(selectedIdx + 1, filteredItems.length - 1); }; - let container; - let adjustHeightDebounce; - - const adjustHeight = () => { - if (container) { - if (adjustHeightDebounce) { - clearTimeout(adjustHeightDebounce); - } - - adjustHeightDebounce = setTimeout(() => { - if (!container) return; - - // Ensure the container is visible before adjusting height - const rect = container.getBoundingClientRect(); - container.style.maxHeight = Math.max(Math.min(240, rect.bottom - 100), 100) + 'px'; - }, 100); + export const select = async () => { + // find item with data-selected=true + const item = document.querySelector(`[data-selected="true"]`); + if (item) { + // click the item + item.click(); } }; - - const confirmSelect = async (type, data) => { - onSelect({ - type: type, - data: data - }); - }; - const decodeString = (str: string) => { try { return decodeURIComponent(str); @@ -77,22 +78,7 @@ }; onMount(async () => { - window.addEventListener('resize', adjustHeight); - - let notes = await getNoteList(localStorage.token).catch(() => { - return []; - }); - - notes = notes.map((note) => { - return { - ...note, - type: 'note', - name: note.title, - description: dayjs(note.updated_at / 1000000).fromNow() - }; - }); - - let legacy_documents = $knowledge + let legacy_documents = knowledge .filter((item) => item?.meta?.document) .map((item) => ({ ...item, @@ -127,16 +113,16 @@ ] : []; - let collections = $knowledge + let collections = knowledge .filter((item) => !item?.meta?.document) .map((item) => ({ ...item, type: 'collection' })); let collection_files = - $knowledge.length > 0 + knowledge.length > 0 ? [ - ...$knowledge + ...knowledge .reduce((a, item) => { return [ ...new Set([ @@ -158,196 +144,145 @@ ] : []; - items = [ - ...notes, - ...collections, - ...collection_files, - ...legacy_collections, - ...legacy_documents - ].map((item) => { - return { - ...item, - ...(item?.legacy || item?.meta?.legacy || item?.meta?.document ? { legacy: true } : {}) - }; - }); + items = [...collections, ...collection_files, ...legacy_collections, ...legacy_documents].map( + (item) => { + return { + ...item, + ...(item?.legacy || item?.meta?.legacy || item?.meta?.document ? { legacy: true } : {}) + }; + } + ); fuse = new Fuse(items, { keys: ['name', 'description'] }); await tick(); - adjustHeight(); + }); + + const onKeyDown = (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + select(); + } + }; + onMount(() => { + window.addEventListener('keydown', onKeyDown); }); onDestroy(() => { - window.removeEventListener('resize', adjustHeight); + window.removeEventListener('keydown', onKeyDown); }); -{#if filteredItems.length > 0 || command?.substring(1).startsWith('http')} -
-
-
-
- {#each filteredItems as item, idx} - + +
+ {decodeString(item?.name)} +
+
+
+ + {/if} + {/each} - - {/each} - - {#if command.substring(1).startsWith('https://www.youtube.com') || command - .substring(1) - .startsWith('https://youtu.be')} - - {:else if command.substring(1).startsWith('http')} - - {/if} +
+ {query}
-
-
+ + {:else if query.startsWith('http')} + + {/if} {/if} diff --git a/src/lib/components/chat/MessageInput/Commands/Models.svelte b/src/lib/components/chat/MessageInput/Commands/Models.svelte index 5163d850fb..0177e6fdf3 100644 --- a/src/lib/components/chat/MessageInput/Commands/Models.svelte +++ b/src/lib/components/chat/MessageInput/Commands/Models.svelte @@ -6,14 +6,15 @@ import { models } from '$lib/stores'; import { WEBUI_BASE_URL } from '$lib/constants'; + import Tooltip from '$lib/components/common/Tooltip.svelte'; const i18n = getContext('i18n'); - export let command = ''; + export let query = ''; export let onSelect = (e) => {}; let selectedIdx = 0; - let filteredItems = []; + export let filteredItems = []; let fuse = new Fuse( $models @@ -33,13 +34,13 @@ } ); - $: filteredItems = command.slice(1) - ? fuse.search(command.slice(1)).map((e) => { + $: filteredItems = query + ? fuse.search(query).map((e) => { return e.item; }) : $models.filter((model) => !model?.info?.meta?.hidden); - $: if (command) { + $: if (query) { selectedIdx = 0; } @@ -51,85 +52,46 @@ selectedIdx = Math.min(selectedIdx + 1, filteredItems.length - 1); }; - let container; - let adjustHeightDebounce; - - const adjustHeight = () => { - if (container) { - if (adjustHeightDebounce) { - clearTimeout(adjustHeightDebounce); - } - - adjustHeightDebounce = setTimeout(() => { - if (!container) return; - - // Ensure the container is visible before adjusting height - const rect = container.getBoundingClientRect(); - container.style.maxHeight = Math.max(Math.min(240, rect.bottom - 100), 100) + 'px'; - }, 100); + export const select = async () => { + const model = filteredItems[selectedIdx]; + if (model) { + onSelect({ type: 'model', data: model }); } }; - - const confirmSelect = async (model) => { - onSelect({ type: 'model', data: model }); - }; - - onMount(async () => { - window.addEventListener('resize', adjustHeight); - - await tick(); - const chatInputElement = document.getElementById('chat-input'); - await tick(); - chatInputElement?.focus(); - await tick(); - - adjustHeight(); - }); - - onDestroy(() => { - window.removeEventListener('resize', adjustHeight); - }); +
+ {$i18n.t('Models')} +
+ {#if filteredItems.length > 0} -
-
-
-
- {#each filteredItems as model, modelIdx} - - {/each} + {#each filteredItems as model, modelIdx} + +
-
-
+ + + {/each} {/if} diff --git a/src/lib/components/chat/MessageInput/Commands/Prompts.svelte b/src/lib/components/chat/MessageInput/Commands/Prompts.svelte index ffd02fbc41..5df3c4691b 100644 --- a/src/lib/components/chat/MessageInput/Commands/Prompts.svelte +++ b/src/lib/components/chat/MessageInput/Commands/Prompts.svelte @@ -1,140 +1,71 @@ -{#if filteredPrompts.length > 0} -
-
-
-
+ {$i18n.t('Prompts')} +
+ +{#if filteredItems.length > 0} +
+ {#each filteredItems as promptItem, promptIdx} + + - {/each} -
- -
-
- - - -
- -
- {$i18n.t( - 'Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.' - )} -
-
-
-
+ + {promptItem.title} + + + + {/each}
{/if} diff --git a/src/lib/components/chat/MessageInput/FilesOverlay.svelte b/src/lib/components/chat/MessageInput/FilesOverlay.svelte index 7c35ceb673..d8a09b0b1e 100644 --- a/src/lib/components/chat/MessageInput/FilesOverlay.svelte +++ b/src/lib/components/chat/MessageInput/FilesOverlay.svelte @@ -24,8 +24,10 @@ role="region" aria-label="Drag and Drop Container" > -
-
+
+
diff --git a/src/lib/components/chat/MessageInput/InputMenu.svelte b/src/lib/components/chat/MessageInput/InputMenu.svelte index 351c882388..977e18f91f 100644 --- a/src/lib/components/chat/MessageInput/InputMenu.svelte +++ b/src/lib/components/chat/MessageInput/InputMenu.svelte @@ -1,27 +1,33 @@ @@ -101,299 +102,390 @@
- {#if tools} - {#if Object.keys(tools).length > 0} -
- {#each Object.keys(tools) as toolId} + {#if tab === ''} +
+ + { + if (fileUploadEnabled) { + uploadFilesHandler(); + } + }} + > + + +
{$i18n.t('Upload Files')}
+
+
+ + + { + if (fileUploadEnabled) { + if (!detectMobile()) { + screenCaptureHandler(); + } else { + const cameraInputElement = document.getElementById('camera-input'); + + if (cameraInputElement) { + cameraInputElement.click(); + } + } + } + }} + > + +
{$i18n.t('Capture')}
+
+
+ + {#if $config?.features?.enable_notes ?? false} + - {/each} -
- {#if Object.keys(tools).length > 3} - + + {/if} + + + + + + {#if ($chats ?? []).length > 0} + + + + {/if} + + {#if fileUploadEnabled} + {#if $config?.features?.enable_google_drive_integration} + { + uploadGoogleDriveHandler(); + }} + > + + + + + + + + +
{$i18n.t('Google Drive')}
+
+ {/if} + + {#if $config?.features?.enable_onedrive_integration && ($config?.features?.enable_onedrive_personal || $config?.features?.enable_onedrive_business)} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{$i18n.t('Microsoft OneDrive')}
+
+ + {#if $config?.features?.enable_onedrive_personal} + { + uploadOneDriveHandler('personal'); + }} + > +
+
{$i18n.t('Microsoft OneDrive (personal)')}
+
+
+ {/if} + + {#if $config?.features?.enable_onedrive_business} + { + uploadOneDriveHandler('organizations'); + }} + > +
+
+ {$i18n.t('Microsoft OneDrive (work/school)')} +
+
{$i18n.t('Includes SharePoint')}
+
+
+ {/if} +
+
+ {/if} {/if} -
- {/if} - {:else} -
-
- -
- {/if} - - - { - if (fileUploadEnabled) { - if (!detectMobile()) { - screenCaptureHandler(); - } else { - const cameraInputElement = document.getElementById('camera-input'); - - if (cameraInputElement) { - cameraInputElement.click(); - } - } - } - }} - > - -
{$i18n.t('Capture')}
-
-
- - - { - if (fileUploadEnabled) { - uploadFilesHandler(); - } - }} - > - -
{$i18n.t('Upload Files')}
-
-
- - {#if fileUploadEnabled} - {#if $config?.features?.enable_google_drive_integration} - + + + +
+ {:else if tab === 'notes'} +
+ + + +
+ {:else if tab === 'chats'} +
+ + + +
{/if}
diff --git a/src/lib/components/chat/MessageInput/InputMenu/Chats.svelte b/src/lib/components/chat/MessageInput/InputMenu/Chats.svelte new file mode 100644 index 0000000000..b4ef49dce5 --- /dev/null +++ b/src/lib/components/chat/MessageInput/InputMenu/Chats.svelte @@ -0,0 +1,125 @@ + + +{#if loaded} + {#if items.length === 0} +
{$i18n.t('No chats found')}
+ {:else} +
+ {#each items as item, idx} + + {/each} + + {#if !allItemsLoaded} + { + if (!itemsLoading) { + loadMoreItems(); + } + }} + > +
+ +
{$i18n.t('Loading...')}
+
+
+ {/if} +
+ {/if} +{:else} +
+ +
+{/if} diff --git a/src/lib/components/chat/MessageInput/InputMenu/Knowledge.svelte b/src/lib/components/chat/MessageInput/InputMenu/Knowledge.svelte new file mode 100644 index 0000000000..df8d8aabab --- /dev/null +++ b/src/lib/components/chat/MessageInput/InputMenu/Knowledge.svelte @@ -0,0 +1,163 @@ + + +{#if loaded} +
+ {#each items as item, idx} + + {/each} +
+{:else} +
+ +
+{/if} diff --git a/src/lib/components/chat/MessageInput/InputMenu/Notes.svelte b/src/lib/components/chat/MessageInput/InputMenu/Notes.svelte new file mode 100644 index 0000000000..93c5bba180 --- /dev/null +++ b/src/lib/components/chat/MessageInput/InputMenu/Notes.svelte @@ -0,0 +1,128 @@ + + +{#if loaded} + {#if items.length === 0} +
{$i18n.t('No notes found')}
+ {:else} +
+ {#each items as item, idx} + + {/each} + + {#if !allItemsLoaded} + { + if (!itemsLoading) { + loadMoreItems(); + } + }} + > +
+ +
{$i18n.t('Loading...')}
+
+
+ {/if} +
+ {/if} +{:else} +
+ +
+{/if} diff --git a/src/lib/components/chat/MessageInput/InputVariablesModal.svelte b/src/lib/components/chat/MessageInput/InputVariablesModal.svelte index b507c3ff2d..4554ea72d3 100644 --- a/src/lib/components/chat/MessageInput/InputVariablesModal.svelte +++ b/src/lib/components/chat/MessageInput/InputVariablesModal.svelte @@ -84,8 +84,8 @@
{variable} - {#if variables[variable]?.required ?? true} - *required + {#if variables[variable]?.required ?? false} + *{$i18n.t('required')} {/if}
@@ -134,7 +134,7 @@ placeholder={$i18n.t('Enter value (true/false)')} bind:value={variableValues[variable]} autocomplete="off" - required + required={variables[variable]?.required ?? false} />
{:else if variables[variable]?.type === 'color'} @@ -159,7 +159,7 @@ placeholder={$i18n.t('Enter hex color (e.g. #FF0000)')} bind:value={variableValues[variable]} autocomplete="off" - required + required={variables[variable]?.required ?? false} />
{:else if variables[variable]?.type === 'date'} @@ -170,7 +170,7 @@ bind:value={variableValues[variable]} autocomplete="off" id="input-variable-{idx}" - required + required={variables[variable]?.required ?? false} {...variableAttributes} /> {:else if variables[variable]?.type === 'datetime-local'} @@ -181,7 +181,7 @@ bind:value={variableValues[variable]} autocomplete="off" id="input-variable-{idx}" - required + required={variables[variable]?.required ?? false} {...variableAttributes} /> {:else if variables[variable]?.type === 'email'} @@ -192,7 +192,7 @@ bind:value={variableValues[variable]} autocomplete="off" id="input-variable-{idx}" - required + required={variables[variable]?.required ?? false} {...variableAttributes} /> {:else if variables[variable]?.type === 'month'} @@ -203,7 +203,7 @@ bind:value={variableValues[variable]} autocomplete="off" id="input-variable-{idx}" - required + required={variables[variable]?.required ?? false} {...variableAttributes} /> {:else if variables[variable]?.type === 'number'} @@ -214,7 +214,7 @@ bind:value={variableValues[variable]} autocomplete="off" id="input-variable-{idx}" - required + required={variables[variable]?.required ?? false} {...variableAttributes} /> {:else if variables[variable]?.type === 'range'} @@ -235,7 +235,7 @@ placeholder={$i18n.t('Enter value')} bind:value={variableValues[variable]} autocomplete="off" - required + required={variables[variable]?.required ?? false} />
@@ -256,7 +256,7 @@ bind:value={variableValues[variable]} autocomplete="off" id="input-variable-{idx}" - required + required={variables[variable]?.required ?? false} {...variableAttributes} /> {:else if variables[variable]?.type === 'text'} @@ -267,7 +267,7 @@ bind:value={variableValues[variable]} autocomplete="off" id="input-variable-{idx}" - required + required={variables[variable]?.required ?? false} {...variableAttributes} /> {:else if variables[variable]?.type === 'time'} @@ -278,7 +278,7 @@ bind:value={variableValues[variable]} autocomplete="off" id="input-variable-{idx}" - required + required={variables[variable]?.required ?? false} {...variableAttributes} /> {:else if variables[variable]?.type === 'url'} @@ -289,7 +289,7 @@ bind:value={variableValues[variable]} autocomplete="off" id="input-variable-{idx}" - required + required={variables[variable]?.required ?? false} {...variableAttributes} /> {:else if variables[variable]?.type === 'map'} @@ -311,7 +311,7 @@ placeholder={$i18n.t('Enter coordinates (e.g. 51.505, -0.09)')} bind:value={variableValues[variable]} autocomplete="off" - required + required={variables[variable]?.required ?? false} />
{:else} @@ -321,7 +321,7 @@ bind:value={variableValues[variable]} autocomplete="off" id="input-variable-{idx}" - required + required={variables[variable]?.required ?? false} /> {/if}
diff --git a/src/lib/components/chat/MessageInput/IntegrationsMenu.svelte b/src/lib/components/chat/MessageInput/IntegrationsMenu.svelte new file mode 100644 index 0000000000..62258e8883 --- /dev/null +++ b/src/lib/components/chat/MessageInput/IntegrationsMenu.svelte @@ -0,0 +1,345 @@ + + + { + if (e.detail === false) { + onClose(); + } + }} +> + + + +
+ + {#if tab === ''} +
+ {#if tools} + {#if Object.keys(tools).length > 0} + + {/if} + {:else} +
+ +
+ {/if} + + {#if toggleFilters && toggleFilters.length > 0} + {#each toggleFilters.sort( (a, b) => a.name.localeCompare( b.name, undefined, { sensitivity: 'base' } ) ) as filter, filterIdx (filter.id)} + + + + {/each} + {/if} + + {#if showWebSearchButton} + + + + {/if} + + {#if showImageGenerationButton} + + + + {/if} + + {#if showCodeInterpreterButton} + + + + {/if} +
+ {:else if tab === 'tools' && tools} +
+ + + {#each Object.keys(tools) as toolId} + + {/each} +
+ {/if} +
+
+
diff --git a/src/lib/components/chat/Messages.svelte b/src/lib/components/chat/Messages.svelte index f7e7a8345d..784514679c 100644 --- a/src/lib/components/chat/Messages.svelte +++ b/src/lib/components/chat/Messages.svelte @@ -454,7 +454,7 @@ {/each} -
+
{#if bottomPadding}
{/if} diff --git a/src/lib/components/chat/Messages/Citations.svelte b/src/lib/components/chat/Messages/Citations.svelte index f234b52d4c..6ffdf4362d 100644 --- a/src/lib/components/chat/Messages/Citations.svelte +++ b/src/lib/components/chat/Messages/Citations.svelte @@ -1,6 +1,6 @@ - @@ -111,7 +119,7 @@
{/if} + +{#if showCitations} +
+
+ {#each citations as citation, idx} + + {/each} +
+
+{/if} diff --git a/src/lib/components/chat/Messages/Citations/CitationModal.svelte b/src/lib/components/chat/Messages/Citations/CitationModal.svelte index c6e460d964..114d4f48d2 100644 --- a/src/lib/components/chat/Messages/Citations/CitationModal.svelte +++ b/src/lib/components/chat/Messages/Citations/CitationModal.svelte @@ -60,19 +60,21 @@
-
-
+
+
{#if citation?.source?.name} {@const document = mergedDocuments?.[0]} {#if document?.metadata?.file_id || document.source?.url?.includes('http')}
-
+
{#each mergedDocuments as document, documentIdx}
diff --git a/src/lib/components/chat/Messages/CodeBlock.svelte b/src/lib/components/chat/Messages/CodeBlock.svelte index f3da6d8f75..01e512eff3 100644 --- a/src/lib/components/chat/Messages/CodeBlock.svelte +++ b/src/lib/components/chat/Messages/CodeBlock.svelte @@ -1,17 +1,12 @@
-
+
{#if lang === 'mermaid'} {#if mermaidHtml} @@ -428,16 +396,18 @@
{code}
{/if} {:else} -
+
{lang}
- {#if preview && ['html', 'svg'].includes(lang)} - - {/if} - {#if ($config?.features?.enable_code_execution ?? true) && (lang.toLowerCase() === 'python' || lang.toLowerCase() === 'py' || (lang === '' && checkPythonCode(code)))} {#if executing} -
+
{$i18n.t('Running')}
{:else if run} + + {#if preview && ['html', 'svg'].includes(lang)} + + {/if}
-
+
{#if !collapsed} {#if edit} - { - saveCode(); - }} - onChange={(value) => { - _code = value; - }} - /> + {#await import('$lib/components/common/CodeEditor.svelte') then { default: CodeEditor }} + { + saveCode(); + }} + onChange={(value) => { + _code = value; + }} + /> + {/await} {:else}
 						
 							{$i18n.t('{{COUNT}} hidden lines', {
@@ -561,7 +527,7 @@
 
 				{#if executing || stdout || stderr || result || files}
 					
{#if executing}
diff --git a/src/lib/components/chat/Messages/Markdown.svelte b/src/lib/components/chat/Messages/Markdown.svelte index 736c93cb0d..c33e452a6c 100644 --- a/src/lib/components/chat/Messages/Markdown.svelte +++ b/src/lib/components/chat/Messages/Markdown.svelte @@ -5,6 +5,7 @@ import markedExtension from '$lib/utils/marked/extension'; import markedKatexExtension from '$lib/utils/marked/katex-extension'; + import { mentionExtension } from '$lib/utils/marked/mention-extension'; import MarkdownTokens from './Markdown/MarkdownTokens.svelte'; @@ -37,6 +38,9 @@ marked.use(markedKatexExtension(options)); marked.use(markedExtension(options)); + marked.use({ + extensions: [mentionExtension({ triggerChar: '@' }), mentionExtension({ triggerChar: '#' })] + }); $: (async () => { if (content) { diff --git a/src/lib/components/chat/Messages/Markdown/KatexRenderer.svelte b/src/lib/components/chat/Messages/Markdown/KatexRenderer.svelte index 4dfb9f2c5b..d28edb224f 100644 --- a/src/lib/components/chat/Messages/Markdown/KatexRenderer.svelte +++ b/src/lib/components/chat/Messages/Markdown/KatexRenderer.svelte @@ -1,10 +1,22 @@ -{@html katex.renderToString(content, { displayMode, throwOnError: false })} +{#if renderToString} + {@html renderToString(content, { displayMode, throwOnError: false })} +{/if} diff --git a/src/lib/components/chat/Messages/Markdown/MarkdownInlineTokens.svelte b/src/lib/components/chat/Messages/Markdown/MarkdownInlineTokens.svelte index c49d60df69..8a0358a752 100644 --- a/src/lib/components/chat/Messages/Markdown/MarkdownInlineTokens.svelte +++ b/src/lib/components/chat/Messages/Markdown/MarkdownInlineTokens.svelte @@ -16,6 +16,7 @@ import HtmlToken from './HTMLToken.svelte'; import TextToken from './MarkdownInlineTokens/TextToken.svelte'; import CodespanToken from './MarkdownInlineTokens/CodespanToken.svelte'; + import MentionToken from './MarkdownInlineTokens/MentionToken.svelte'; export let id: string; export let done = true; @@ -60,6 +61,8 @@ frameborder="0" onload="this.style.height=(this.contentWindow.document.body.scrollHeight+20)+'px';" > + {:else if token.type === 'mention'} + {:else if token.type === 'text'} {/if} diff --git a/src/lib/components/chat/Messages/Markdown/MarkdownInlineTokens/MentionToken.svelte b/src/lib/components/chat/Messages/Markdown/MarkdownInlineTokens/MentionToken.svelte new file mode 100644 index 0000000000..19f23b2aa0 --- /dev/null +++ b/src/lib/components/chat/Messages/Markdown/MarkdownInlineTokens/MentionToken.svelte @@ -0,0 +1,108 @@ + + + + + + + + { + if (triggerChar === '@') { + if (idType === 'U') { + // Open user profile + console.log('Clicked user mention', id); + } else if (idType === 'M') { + console.log('Clicked model mention', id); + await goto(`/?model=${id}`); + } + } else if (triggerChar === '#') { + if (idType === 'C') { + // Open channel + if ($channels.find((c) => c.id === id)) { + await goto(`/channels/${id}`); + } + } else if (idType === 'T') { + // Open thread + } + } else { + // Unknown trigger char, just log + console.log('Clicked mention', id); + } + }} + > + {triggerChar}{label} + + + + {#if triggerChar === '@' && idType === 'U'} + + {/if} + diff --git a/src/lib/components/chat/Messages/Markdown/MarkdownTokens.svelte b/src/lib/components/chat/Messages/Markdown/MarkdownTokens.svelte index c5c0b43e88..e568d92bac 100644 --- a/src/lib/components/chat/Messages/Markdown/MarkdownTokens.svelte +++ b/src/lib/components/chat/Messages/Markdown/MarkdownTokens.svelte @@ -17,7 +17,7 @@ import AlertRenderer, { alertComponent } from './AlertRenderer.svelte'; import Collapsible from '$lib/components/common/Collapsible.svelte'; import Tooltip from '$lib/components/common/Tooltip.svelte'; - import ArrowDownTray from '$lib/components/icons/ArrowDownTray.svelte'; + import Download from '$lib/components/icons/Download.svelte'; import Source from './Source.svelte'; import { settings } from '$lib/stores'; @@ -109,7 +109,7 @@ {save} {preview} edit={editCodeBlock} - stickyButtonsClassName={topPadding ? 'top-8' : 'top-0'} + stickyButtonsClassName={topPadding ? 'top-7' : 'top-0'} onSave={(value) => { onSave({ raw: token.raw, @@ -124,19 +124,19 @@ {token.text} {/if} {:else if token.type === 'table'} -
-
+
+
setSortKey('user')} >
@@ -234,7 +228,7 @@
setSortKey('model_id')} >
@@ -257,7 +251,7 @@
setSortKey('rating')} >
@@ -280,7 +274,7 @@
setSortKey('updated_at')} >
@@ -301,7 +295,7 @@
{#each token.header as header, headerIdx} {#each token.rows as row, rowIdx} - + {#each row ?? [] as cell, cellIdx}
@@ -155,10 +155,14 @@
@@ -186,7 +190,7 @@ exportTableToCSVHandler(token, tokenIdx); }} > - +
diff --git a/src/lib/components/chat/Messages/ResponseMessage.svelte b/src/lib/components/chat/Messages/ResponseMessage.svelte index fd4b2ebb45..bbda9ac277 100644 --- a/src/lib/components/chat/Messages/ResponseMessage.svelte +++ b/src/lib/components/chat/Messages/ResponseMessage.svelte @@ -634,7 +634,12 @@ : 'invisible group-hover:visible transition text-gray-400'}" > - {formatDate(message.timestamp * 1000)} + {$i18n.t(formatDate(message.timestamp * 1000), { + LOCALIZED_TIME: dayjs(message.timestamp * 1000).format('LT'), + LOCALIZED_DATE: dayjs(message.timestamp * 1000).format('L') + })} {/if} @@ -663,7 +668,7 @@ name={file.name} type={file.type} size={file?.size} - colorClassName="bg-white dark:bg-gray-850 " + small={true} /> {/if} @@ -700,7 +705,7 @@
@@ -189,7 +203,7 @@ name={file.name} type={file.type} size={file?.size} - colorClassName="bg-white dark:bg-gray-850 " + small={true} /> {/if} @@ -290,7 +304,7 @@
{/if} {/if} + {#if $mobile && !$temporaryChatEnabled && chat && chat.id} + + + + {/if} + {#if shareEnabled && chat && (chat.id || $temporaryChatEnabled)} {/if} - {#if $mobile} - - - - {/if} - {#if $user !== undefined && $user !== null} {#if !history.currentId && !$chatId && ($banners.length > 0 || ($config?.license_metadata?.type ?? null) === 'trial' || (($config?.license_metadata?.seats ?? null) !== null && $config?.user_count > $config?.license_metadata?.seats))} -
+
{#if ($config?.license_metadata?.type ?? null) === 'trial'} {/if} - {#if showBanners} - {#each $banners.filter((b) => ![...JSON.parse(localStorage.getItem('dismissedBannerIds') ?? '[]'), ...closedBannerIds].includes(b.id)) as banner (banner.id)} - { - const bannerId = e.detail; + {#each $banners.filter((b) => ![...JSON.parse(localStorage.getItem('dismissedBannerIds') ?? '[]'), ...closedBannerIds].includes(b.id)) as banner (banner.id)} + { + const bannerId = e.detail; - if (banner.dismissible) { - localStorage.setItem( - 'dismissedBannerIds', - JSON.stringify( - [ - bannerId, - ...JSON.parse(localStorage.getItem('dismissedBannerIds') ?? '[]') - ].filter((id) => $banners.find((b) => b.id === id)) - ) - ); - } else { - closedBannerIds = [...closedBannerIds, bannerId]; - } - }} - /> - {/each} - {/if} + if (banner.dismissible) { + localStorage.setItem( + 'dismissedBannerIds', + JSON.stringify( + [ + bannerId, + ...JSON.parse(localStorage.getItem('dismissedBannerIds') ?? '[]') + ].filter((id) => $banners.find((b) => b.id === id)) + ) + ); + } else { + closedBannerIds = [...closedBannerIds, bannerId]; + } + }} + /> + {/each}
{/if} diff --git a/src/lib/components/chat/Placeholder.svelte b/src/lib/components/chat/Placeholder.svelte index 8d68cb0fae..bf4986c590 100644 --- a/src/lib/components/chat/Placeholder.svelte +++ b/src/lib/components/chat/Placeholder.svelte @@ -77,7 +77,7 @@ className="w-full flex justify-center mb-0.5" placement="top" > -
+
{$i18n.t('Temporary Chat')}
diff --git a/src/lib/components/chat/Placeholder/FolderTitle.svelte b/src/lib/components/chat/Placeholder/FolderTitle.svelte index a8e2004ef0..bfd72681d3 100644 --- a/src/lib/components/chat/Placeholder/FolderTitle.svelte +++ b/src/lib/components/chat/Placeholder/FolderTitle.svelte @@ -32,7 +32,7 @@ let showFolderModal = false; let showDeleteConfirm = false; - const updateHandler = async ({ name, data }) => { + const updateHandler = async ({ name, meta, data }) => { if (name === '') { toast.error($i18n.t('Folder name cannot be empty.')); return; @@ -45,6 +45,7 @@ const res = await updateFolderById(localStorage.token, folder.id, { name, + ...(meta ? { meta } : {}), ...(data ? { data } : {}) }).catch((error) => { toast.error(`${error}`); diff --git a/src/lib/components/chat/Settings/Audio.svelte b/src/lib/components/chat/Settings/Audio.svelte index c3391a338e..f6c02767e0 100644 --- a/src/lib/components/chat/Settings/Audio.svelte +++ b/src/lib/components/chat/Settings/Audio.svelte @@ -6,7 +6,6 @@ import { getVoices as _getVoices } from '$lib/apis/audio'; import Switch from '$lib/components/common/Switch.svelte'; - import { round } from '@huggingface/transformers'; import Spinner from '$lib/components/common/Spinner.svelte'; import Tooltip from '$lib/components/common/Tooltip.svelte'; const dispatch = createEventDispatcher(); diff --git a/src/lib/components/chat/Settings/Chats.svelte b/src/lib/components/chat/Settings/DataControls.svelte similarity index 100% rename from src/lib/components/chat/Settings/Chats.svelte rename to src/lib/components/chat/Settings/DataControls.svelte diff --git a/src/lib/components/chat/Settings/Interface.svelte b/src/lib/components/chat/Settings/Interface.svelte index c383b831bb..f79dd8524f 100644 --- a/src/lib/components/chat/Settings/Interface.svelte +++ b/src/lib/components/chat/Settings/Interface.svelte @@ -9,6 +9,7 @@ import Plus from '$lib/components/icons/Plus.svelte'; import Switch from '$lib/components/common/Switch.svelte'; import ManageFloatingActionButtonsModal from './Interface/ManageFloatingActionButtonsModal.svelte'; + import ManageImageCompressionModal from './Interface/ManageImageCompressionModal.svelte'; const dispatch = createEventDispatcher(); const i18n = getContext('i18n'); @@ -93,6 +94,7 @@ let iframeSandboxAllowForms = false; let showManageFloatingActionButtonsModal = false; + let showManageImageCompressionModal = false; const toggleLandingPageMode = async () => { landingPageMode = landingPageMode === '' ? 'chat' : ''; @@ -260,6 +262,14 @@ }} /> + { + saveSettings({ imageCompressionSize: size }); + }} +/> +
-
+
+ {#if imageCompression} + + {/if} + {#if imageCompression} -
-
-
- {$i18n.t('Image Max Compression Size')} -
- -
- - x - - -
-
-
-
diff --git a/src/lib/components/chat/Settings/Interface/ManageImageCompressionModal.svelte b/src/lib/components/chat/Settings/Interface/ManageImageCompressionModal.svelte new file mode 100644 index 0000000000..652db8ab4e --- /dev/null +++ b/src/lib/components/chat/Settings/Interface/ManageImageCompressionModal.svelte @@ -0,0 +1,108 @@ + + + +
+
+

+ {$i18n.t('Manage')} +

+ +
+ +
+
+ { + e.preventDefault(); + submitHandler(); + }} + > +
+
+
+
+ {$i18n.t('Image Max Compression Size')} +
+ +
+
+ + +
+ +
+ +
+ +
+ + +
+
+
+
+
+ +
+ +
+ +
+
+
+
diff --git a/src/lib/components/chat/Settings/Personalization/AddMemoryModal.svelte b/src/lib/components/chat/Settings/Personalization/AddMemoryModal.svelte index 8c21b89627..9f52fd8c8d 100644 --- a/src/lib/components/chat/Settings/Personalization/AddMemoryModal.svelte +++ b/src/lib/components/chat/Settings/Personalization/AddMemoryModal.svelte @@ -76,7 +76,7 @@
@@ -656,19 +653,7 @@ }} >
- +
{$i18n.t('Interface')}
@@ -693,17 +678,7 @@ }} >
- +
{$i18n.t('Connections')}
@@ -729,19 +704,7 @@ }} >
- +
{$i18n.t('External Tools')}
@@ -766,7 +729,7 @@ }} >
- +
{$i18n.t('Personalization')}
@@ -790,31 +753,18 @@ }} >
- +
{$i18n.t('Audio')}
- {:else if tabId === 'chats'} + {:else if tabId === 'data_controls'} {:else if tabId === 'account'} @@ -899,19 +825,7 @@ }} >
- +
{$i18n.t('About')}
@@ -935,19 +849,7 @@ }} >
- +
{$i18n.t('Admin Settings')}
@@ -997,8 +899,8 @@ toast.success($i18n.t('Settings saved successfully!')); }} /> - {:else if selectedTab === 'chats'} - + {:else if selectedTab === 'data_controls'} + {:else if selectedTab === 'account'}
{content} diff --git a/src/lib/components/common/Banner.svelte b/src/lib/components/common/Banner.svelte index a64cc857ff..c2c32a5e8b 100644 --- a/src/lib/components/common/Banner.svelte +++ b/src/lib/components/common/Banner.svelte @@ -18,7 +18,7 @@ dismissible: true, timestamp: Math.floor(Date.now() / 1000) }; - export let className = 'mx-4'; + export let className = 'mx-2 px-2 rounded-lg'; export let dismissed = false; @@ -46,13 +46,13 @@ {#if !dismissed} {#if mounted}
{#if banner.type.toLowerCase() === 'info'} diff --git a/src/lib/components/common/Dropdown.svelte b/src/lib/components/common/Dropdown.svelte index 0f10a3f553..cac97f884c 100644 --- a/src/lib/components/common/Dropdown.svelte +++ b/src/lib/components/common/Dropdown.svelte @@ -26,7 +26,7 @@ { @@ -47,7 +50,7 @@
+ {:else} +
+ {#if !loading} + + {#if type === 'collection'} + + {:else if type === 'note'} + + {:else if type === 'chat'} + + {:else} + + {/if} + + {:else} + + {/if} +
{/if} {#if !small} @@ -106,6 +138,8 @@ > {#if type === 'file'} {$i18n.t('File')} + {:else if type === 'note'} + {$i18n.t('Note')} {:else if type === 'doc'} {$i18n.t('Document')} {:else if type === 'collection'} @@ -120,15 +154,14 @@
{:else} -
+
- {#if loading} -
- -
+
{decodeString(name)}
+ {#if size} +
{formatFileSize(size)}
+ {:else} +
{type}
{/if} -
{decodeString(name)}
-
{formatFileSize(size)}
diff --git a/src/lib/components/common/FileItemModal.svelte b/src/lib/components/common/FileItemModal.svelte index e912a807ae..52bf75127d 100644 --- a/src/lib/components/common/FileItemModal.svelte +++ b/src/lib/components/common/FileItemModal.svelte @@ -82,7 +82,7 @@ -
+
@@ -236,7 +236,7 @@ /> {:else}
- {item?.file?.data?.content ?? 'No content'} + {(item?.file?.data?.content ?? '').trim() || 'No content'}
{/if} {:else} @@ -251,7 +251,7 @@ {#if item?.file?.data}
- {item?.file?.data?.content ?? 'No content'} + {(item?.file?.data?.content ?? '').trim() || 'No content'}
{/if} {/if} diff --git a/src/lib/components/common/Folder.svelte b/src/lib/components/common/Folder.svelte index ba947901fc..1c307c718b 100644 --- a/src/lib/components/common/Folder.svelte +++ b/src/lib/components/common/Folder.svelte @@ -16,14 +16,15 @@ export let name = ''; export let collapsible = true; + export let className = ''; + export let buttonClassName = 'text-gray-600 dark:text-gray-400'; + export let chevron = true; export let onAddLabel: string = ''; export let onAdd: null | Function = null; export let dragAndDrop = true; - export let className = ''; - let folderElement; let draggedOver = false; @@ -138,20 +139,20 @@ >
diff --git a/src/lib/components/common/Modal.svelte b/src/lib/components/common/Modal.svelte index 8cb7296475..4957d09757 100644 --- a/src/lib/components/common/Modal.svelte +++ b/src/lib/components/common/Modal.svelte @@ -7,7 +7,7 @@ export let show = true; export let size = 'md'; export let containerClassName = 'p-3'; - export let className = 'bg-white dark:bg-gray-900 rounded-2xl'; + export let className = 'bg-white dark:bg-gray-900 rounded-3xl'; let modalElement = null; let mounted = false; diff --git a/src/lib/components/common/RichTextInput.svelte b/src/lib/components/common/RichTextInput.svelte index 6a72c4a470..d6f59ab34f 100644 --- a/src/lib/components/common/RichTextInput.svelte +++ b/src/lib/components/common/RichTextInput.svelte @@ -91,6 +91,18 @@ } }); + // Convert TipTap mention spans -> <@id> + turndownService.addRule('mentions', { + filter: (node) => node.nodeName === 'SPAN' && node.getAttribute('data-type') === 'mention', + replacement: (_content, node: HTMLElement) => { + const id = node.getAttribute('data-id') || ''; + // TipTap stores the trigger char in data-mention-suggestion-char (usually "@") + const ch = node.getAttribute('data-mention-suggestion-char') || '@'; + // Emit <@id> style, e.g. <@llama3.2:latest> + return `<${ch}${id}>`; + } + }); + import { onMount, onDestroy, tick, getContext } from 'svelte'; import { createEventDispatcher } from 'svelte'; @@ -100,7 +112,7 @@ import { Fragment, DOMParser } from 'prosemirror-model'; import { EditorState, Plugin, PluginKey, TextSelection, Selection } from 'prosemirror-state'; import { Decoration, DecorationSet } from 'prosemirror-view'; - import { Editor, Extension } from '@tiptap/core'; + import { Editor, Extension, mergeAttributes } from '@tiptap/core'; // Yjs imports import * as Y from 'yjs'; @@ -137,13 +149,10 @@ import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'; import Mention from '@tiptap/extension-mention'; - - import { all, createLowlight } from 'lowlight'; + import FormattingButtons from './RichTextInput/FormattingButtons.svelte'; import { PASTED_TEXT_CHARACTER_LIMIT } from '$lib/constants'; - - import FormattingButtons from './RichTextInput/FormattingButtons.svelte'; - import { duration } from 'dayjs'; + import { all, createLowlight } from 'lowlight'; export let oncompositionstart = (e) => {}; export let oncompositionend = (e) => {}; @@ -162,9 +171,24 @@ export let className = 'input-prose'; export let placeholder = 'Type here...'; + let _placeholder = placeholder; + + $: if (placeholder !== _placeholder) { + setPlaceholder(); + } + + const setPlaceholder = () => { + _placeholder = placeholder; + if (editor) { + editor?.view.dispatch(editor.state.tr); + } + }; + + export let richText = true; export let link = false; export let image = false; export let fileHandler = false; + export let suggestions = null; export let onFileDrop = (currentEditor, files, pos) => { files.forEach((file) => { @@ -951,6 +975,7 @@ } console.log(bubbleMenuElement, floatingMenuElement); + console.log(suggestions); editor = new Editor({ element: element, @@ -958,29 +983,35 @@ StarterKit.configure({ link: link }), - Placeholder.configure({ placeholder }), + Placeholder.configure({ placeholder: () => _placeholder }), SelectionDecoration, - CodeBlockLowlight.configure({ - lowlight - }), - Highlight, - Typography, + ...(richText + ? [ + CodeBlockLowlight.configure({ + lowlight + }), + Highlight, + Typography, + TableKit.configure({ + table: { resizable: true } + }), + ListKit.configure({ + taskItem: { + nested: true + } + }) + ] + : []), + ...(suggestions + ? [ + Mention.configure({ + HTMLAttributes: { class: 'mention' }, + suggestions: suggestions + }) + ] + : []), - Mention.configure({ - HTMLAttributes: { - class: 'mention' - } - }), - - TableKit.configure({ - table: { resizable: true } - }), - ListKit.configure({ - taskItem: { - nested: true - } - }), CharacterCount.configure({}), ...(image ? [Image] : []), ...(fileHandler @@ -991,8 +1022,7 @@ }) ] : []), - - ...(autocomplete + ...(richText && autocomplete ? [ AIAutocompletion.configure({ generateCompletion: async (text) => { @@ -1010,8 +1040,7 @@ }) ] : []), - - ...(showFormattingToolbar + ...(richText && showFormattingToolbar ? [ BubbleMenu.configure({ element: bubbleMenuElement, @@ -1046,7 +1075,6 @@ htmlValue = editor.getHTML(); jsonValue = editor.getJSON(); - mdValue = turndownService .turndown( htmlValue @@ -1086,6 +1114,38 @@ }, editorProps: { attributes: { id }, + handlePaste: (view, event) => { + // Force plain-text pasting when richText === false + if (!richText) { + // swallow HTML completely + event.preventDefault(); + const { state, dispatch } = view; + + const plainText = (event.clipboardData?.getData('text/plain') ?? '').replace( + /\r\n/g, + '\n' + ); + + const lines = plainText.split('\n'); + const nodes = []; + + lines.forEach((line, index) => { + if (index > 0) { + nodes.push(state.schema.nodes.hardBreak.create()); + } + if (line.length > 0) { + nodes.push(state.schema.text(line)); + } + }); + + const fragment = Fragment.fromArray(nodes); + dispatch(state.tr.replaceSelectionWith(fragment, false).scrollIntoView()); + + return true; // handled + } + + return false; + }, handleDOMEvents: { compositionstart: (view, event) => { oncompositionstart(event); @@ -1143,12 +1203,13 @@ if (event.key === 'Enter') { const isCtrlPressed = event.ctrlKey || event.metaKey; // metaKey is for Cmd key on Mac + + const { state } = view; + const { $from } = state.selection; + const lineStart = $from.before($from.depth); + const lineEnd = $from.after($from.depth); + const lineText = state.doc.textBetween(lineStart, lineEnd, '\n', '\0').trim(); if (event.shiftKey && !isCtrlPressed) { - const { state } = view; - const { $from } = state.selection; - const lineStart = $from.before($from.depth); - const lineEnd = $from.after($from.depth); - const lineText = state.doc.textBetween(lineStart, lineEnd, '\n', '\0').trim(); if (lineText.startsWith('```')) { // Fix GitHub issue #16337: prevent backtick removal for lines starting with ``` return false; // Let ProseMirror handle the Enter key normally @@ -1163,10 +1224,18 @@ const isInList = isInside(['listItem', 'bulletList', 'orderedList', 'taskList']); const isInHeading = isInside(['heading']); + console.log({ isInCodeBlock, isInList, isInHeading }); + if (isInCodeBlock || isInList || isInHeading) { // Let ProseMirror handle the normal Enter behavior return false; } + + const suggestionsElement = document.getElementById('suggestions-container'); + if (lineText.startsWith('#') && suggestionsElement) { + console.log('Letting heading suggestion handle Enter key'); + return true; + } } } @@ -1263,7 +1332,9 @@ editor.storage.files = files; } }, - onSelectionUpdate: onSelectionUpdate + onSelectionUpdate: onSelectionUpdate, + enableInputRules: richText, + enablePasteRules: richText }); if (messageInput) { @@ -1334,7 +1405,7 @@ }; -{#if showFormattingToolbar} +{#if richText && showFormattingToolbar}
diff --git a/src/lib/components/common/RichTextInput/FormattingButtons.svelte b/src/lib/components/common/RichTextInput/FormattingButtons.svelte index 76a35a17cf..48faab4578 100644 --- a/src/lib/components/common/RichTextInput/FormattingButtons.svelte +++ b/src/lib/components/common/RichTextInput/FormattingButtons.svelte @@ -22,7 +22,7 @@
{#if content} -
+
diff --git a/src/lib/components/common/Switch.svelte b/src/lib/components/common/Switch.svelte index ffd649f8cc..c04d9b2ef4 100644 --- a/src/lib/components/common/Switch.svelte +++ b/src/lib/components/common/Switch.svelte @@ -22,15 +22,15 @@ bind:checked={state} {id} aria-labelledby={ariaLabelledbyId} - class="flex h-5 min-h-5 w-9 shrink-0 cursor-pointer items-center rounded-full px-[3px] mx-[1px] transition {($settings?.highContrastMode ?? + class="flex h-[18px] min-h-[18px] w-8 shrink-0 cursor-pointer items-center rounded-full px-1 mx-[1px] transition {($settings?.highContrastMode ?? false) ? 'focus:outline focus:outline-2 focus:outline-gray-800 focus:dark:outline-gray-200' : 'outline outline-1 outline-gray-100 dark:outline-gray-800'} {state - ? ' bg-emerald-600' + ? ' bg-emerald-500 dark:bg-emerald-700' : 'bg-gray-200 dark:bg-transparent'}" > diff --git a/src/lib/components/common/Tooltip.svelte b/src/lib/components/common/Tooltip.svelte index 3d1566e650..a8e79e4841 100644 --- a/src/lib/components/common/Tooltip.svelte +++ b/src/lib/components/common/Tooltip.svelte @@ -7,16 +7,20 @@ export let elementId = ''; + export let as = 'div'; + export let className = 'flex'; + export let placement = 'top'; export let content = `I'm a tooltip!`; export let touch = true; - export let className = 'flex'; export let theme = ''; export let offset = [0, 4]; export let allowHTML = true; export let tippyOptions = {}; export let interactive = false; + export let onClick = () => {}; + let tooltipElement; let tooltipInstance; @@ -59,8 +63,9 @@ }); -
+ + -
+ diff --git a/src/lib/components/common/Valves/MapSelector.svelte b/src/lib/components/common/Valves/MapSelector.svelte index b6b934dc4a..cf4de4b445 100644 --- a/src/lib/components/common/Valves/MapSelector.svelte +++ b/src/lib/components/common/Valves/MapSelector.svelte @@ -1,6 +1,4 @@ + + diff --git a/src/lib/components/icons/AppNotification.svelte b/src/lib/components/icons/AppNotification.svelte new file mode 100644 index 0000000000..fa24a1f8fc --- /dev/null +++ b/src/lib/components/icons/AppNotification.svelte @@ -0,0 +1,23 @@ + + + diff --git a/src/lib/components/icons/ArchiveBox.svelte b/src/lib/components/icons/ArchiveBox.svelte index ef82cdba16..6f60c7b68d 100644 --- a/src/lib/components/icons/ArchiveBox.svelte +++ b/src/lib/components/icons/ArchiveBox.svelte @@ -1,5 +1,5 @@ diff --git a/src/lib/components/icons/Camera.svelte b/src/lib/components/icons/Camera.svelte new file mode 100644 index 0000000000..a553475fa1 --- /dev/null +++ b/src/lib/components/icons/Camera.svelte @@ -0,0 +1,22 @@ + + + diff --git a/src/lib/components/icons/ChatCheck.svelte b/src/lib/components/icons/ChatCheck.svelte new file mode 100644 index 0000000000..f6373b8a51 --- /dev/null +++ b/src/lib/components/icons/ChatCheck.svelte @@ -0,0 +1,18 @@ + + + diff --git a/src/lib/components/icons/Clip.svelte b/src/lib/components/icons/Clip.svelte new file mode 100644 index 0000000000..d3c89be163 --- /dev/null +++ b/src/lib/components/icons/Clip.svelte @@ -0,0 +1,18 @@ + + + diff --git a/src/lib/components/icons/ClockRotateRight.svelte b/src/lib/components/icons/ClockRotateRight.svelte new file mode 100644 index 0000000000..7614a547ff --- /dev/null +++ b/src/lib/components/icons/ClockRotateRight.svelte @@ -0,0 +1,23 @@ + + + diff --git a/src/lib/components/icons/Cloud.svelte b/src/lib/components/icons/Cloud.svelte new file mode 100644 index 0000000000..f314c5fe0c --- /dev/null +++ b/src/lib/components/icons/Cloud.svelte @@ -0,0 +1,18 @@ + + + diff --git a/src/lib/components/icons/Component.svelte b/src/lib/components/icons/Component.svelte new file mode 100644 index 0000000000..cafc7058ec --- /dev/null +++ b/src/lib/components/icons/Component.svelte @@ -0,0 +1,22 @@ + + + diff --git a/src/lib/components/icons/Computer.svelte b/src/lib/components/icons/Computer.svelte new file mode 100644 index 0000000000..3baf42803f --- /dev/null +++ b/src/lib/components/icons/Computer.svelte @@ -0,0 +1,21 @@ + + + diff --git a/src/lib/components/icons/Database.svelte b/src/lib/components/icons/Database.svelte new file mode 100644 index 0000000000..8fcaf1b2de --- /dev/null +++ b/src/lib/components/icons/Database.svelte @@ -0,0 +1,16 @@ + + + diff --git a/src/lib/components/icons/DatabaseSettings.svelte b/src/lib/components/icons/DatabaseSettings.svelte new file mode 100644 index 0000000000..c6e7ca56c7 --- /dev/null +++ b/src/lib/components/icons/DatabaseSettings.svelte @@ -0,0 +1,33 @@ + + + diff --git a/src/lib/components/icons/DocumentPage.svelte b/src/lib/components/icons/DocumentPage.svelte new file mode 100644 index 0000000000..970de9a950 --- /dev/null +++ b/src/lib/components/icons/DocumentPage.svelte @@ -0,0 +1,26 @@ + + + diff --git a/src/lib/components/icons/ArrowDownTray.svelte b/src/lib/components/icons/Download.svelte similarity index 67% rename from src/lib/components/icons/ArrowDownTray.svelte rename to src/lib/components/icons/Download.svelte index 55620e9fea..71282e9a20 100644 --- a/src/lib/components/icons/ArrowDownTray.svelte +++ b/src/lib/components/icons/Download.svelte @@ -10,10 +10,9 @@ stroke-width={strokeWidth} stroke="currentColor" class={className} -> - - + > diff --git a/src/lib/components/icons/Face.svelte b/src/lib/components/icons/Face.svelte new file mode 100644 index 0000000000..32d74058c7 --- /dev/null +++ b/src/lib/components/icons/Face.svelte @@ -0,0 +1,28 @@ + + + diff --git a/src/lib/components/icons/FaceId.svelte b/src/lib/components/icons/FaceId.svelte new file mode 100644 index 0000000000..c0a9f91d8b --- /dev/null +++ b/src/lib/components/icons/FaceId.svelte @@ -0,0 +1,36 @@ + + + diff --git a/src/lib/components/icons/Glasses.svelte b/src/lib/components/icons/Glasses.svelte new file mode 100644 index 0000000000..939924f707 --- /dev/null +++ b/src/lib/components/icons/Glasses.svelte @@ -0,0 +1,23 @@ + + + diff --git a/src/lib/components/icons/Grid.svelte b/src/lib/components/icons/Grid.svelte new file mode 100644 index 0000000000..8846f03107 --- /dev/null +++ b/src/lib/components/icons/Grid.svelte @@ -0,0 +1,22 @@ + + + diff --git a/src/lib/components/icons/Hashtag.svelte b/src/lib/components/icons/Hashtag.svelte new file mode 100644 index 0000000000..08d229954e --- /dev/null +++ b/src/lib/components/icons/Hashtag.svelte @@ -0,0 +1,19 @@ + + + diff --git a/src/lib/components/icons/InfoCircle.svelte b/src/lib/components/icons/InfoCircle.svelte new file mode 100644 index 0000000000..3d748c9c5c --- /dev/null +++ b/src/lib/components/icons/InfoCircle.svelte @@ -0,0 +1,23 @@ + + + diff --git a/src/lib/components/icons/Keyboard.svelte b/src/lib/components/icons/Keyboard.svelte index baf633c0d6..fdb1d06f45 100644 --- a/src/lib/components/icons/Keyboard.svelte +++ b/src/lib/components/icons/Keyboard.svelte @@ -1,19 +1,17 @@ - - diff --git a/src/lib/components/icons/Link.svelte b/src/lib/components/icons/Link.svelte index 7e56ab0dd8..4808da8c25 100644 --- a/src/lib/components/icons/Link.svelte +++ b/src/lib/components/icons/Link.svelte @@ -1,17 +1,22 @@ - - - - + diff --git a/src/lib/components/icons/Lock.svelte b/src/lib/components/icons/Lock.svelte new file mode 100644 index 0000000000..bd0be308d1 --- /dev/null +++ b/src/lib/components/icons/Lock.svelte @@ -0,0 +1,19 @@ + + + diff --git a/src/lib/components/icons/Map.svelte b/src/lib/components/icons/Map.svelte index 93655576cd..79429f9aea 100644 --- a/src/lib/components/icons/Map.svelte +++ b/src/lib/components/icons/Map.svelte @@ -1,6 +1,6 @@ + export let className = 'w-4 h-4'; + export let strokeWidth = '1.5'; + + + diff --git a/src/lib/components/icons/PeopleTag.svelte b/src/lib/components/icons/PeopleTag.svelte new file mode 100644 index 0000000000..6544591f0a --- /dev/null +++ b/src/lib/components/icons/PeopleTag.svelte @@ -0,0 +1,31 @@ + + + diff --git a/src/lib/components/icons/Photo.svelte b/src/lib/components/icons/Photo.svelte index 2933ba6339..26296db7c6 100644 --- a/src/lib/components/icons/Photo.svelte +++ b/src/lib/components/icons/Photo.svelte @@ -4,18 +4,25 @@ + > diff --git a/src/lib/components/icons/PlusAlt.svelte b/src/lib/components/icons/PlusAlt.svelte new file mode 100644 index 0000000000..895fe0b763 --- /dev/null +++ b/src/lib/components/icons/PlusAlt.svelte @@ -0,0 +1,15 @@ + + + diff --git a/src/lib/components/icons/QuestionMarkCircle.svelte b/src/lib/components/icons/QuestionMarkCircle.svelte index 55156392c0..abac715c98 100644 --- a/src/lib/components/icons/QuestionMarkCircle.svelte +++ b/src/lib/components/icons/QuestionMarkCircle.svelte @@ -1,6 +1,6 @@ + export let className = 'w-4 h-4'; + export let strokeWidth = '1.5'; + + + diff --git a/src/lib/components/icons/SettingsAlt.svelte b/src/lib/components/icons/SettingsAlt.svelte new file mode 100644 index 0000000000..fc38afc3f7 --- /dev/null +++ b/src/lib/components/icons/SettingsAlt.svelte @@ -0,0 +1,23 @@ + + + diff --git a/src/lib/components/icons/Share.svelte b/src/lib/components/icons/Share.svelte index f098995c68..f9b7efc213 100644 --- a/src/lib/components/icons/Share.svelte +++ b/src/lib/components/icons/Share.svelte @@ -1,11 +1,23 @@ - - - + diff --git a/src/lib/components/icons/SoundHigh.svelte b/src/lib/components/icons/SoundHigh.svelte new file mode 100644 index 0000000000..11cb329e63 --- /dev/null +++ b/src/lib/components/icons/SoundHigh.svelte @@ -0,0 +1,25 @@ + + + diff --git a/src/lib/components/icons/Terminal.svelte b/src/lib/components/icons/Terminal.svelte new file mode 100644 index 0000000000..be4c5d02d9 --- /dev/null +++ b/src/lib/components/icons/Terminal.svelte @@ -0,0 +1,23 @@ + + + diff --git a/src/lib/components/icons/Union.svelte b/src/lib/components/icons/Union.svelte new file mode 100644 index 0000000000..71953329f4 --- /dev/null +++ b/src/lib/components/icons/Union.svelte @@ -0,0 +1,22 @@ + + + diff --git a/src/lib/components/icons/UserBadgeCheck.svelte b/src/lib/components/icons/UserBadgeCheck.svelte new file mode 100644 index 0000000000..a704caa10f --- /dev/null +++ b/src/lib/components/icons/UserBadgeCheck.svelte @@ -0,0 +1,26 @@ + + + diff --git a/src/lib/components/icons/UserCircle.svelte b/src/lib/components/icons/UserCircle.svelte new file mode 100644 index 0000000000..fdc7594d53 --- /dev/null +++ b/src/lib/components/icons/UserCircle.svelte @@ -0,0 +1,27 @@ + + + diff --git a/src/lib/components/icons/WrenchAlt.svelte b/src/lib/components/icons/WrenchAlt.svelte new file mode 100644 index 0000000000..5328f600b6 --- /dev/null +++ b/src/lib/components/icons/WrenchAlt.svelte @@ -0,0 +1,23 @@ + + + diff --git a/src/lib/components/icons/Youtube.svelte b/src/lib/components/icons/Youtube.svelte new file mode 100644 index 0000000000..81eefb042e --- /dev/null +++ b/src/lib/components/icons/Youtube.svelte @@ -0,0 +1,16 @@ + + + diff --git a/src/lib/components/layout/Navbar/Menu.svelte b/src/lib/components/layout/Navbar/Menu.svelte index 38f9d1b8b0..451afdf88e 100644 --- a/src/lib/components/layout/Navbar/Menu.svelte +++ b/src/lib/components/layout/Navbar/Menu.svelte @@ -6,9 +6,6 @@ import fileSaver from 'file-saver'; const { saveAs } = fileSaver; - import jsPDF from 'jspdf'; - import html2canvas from 'html2canvas-pro'; - import { downloadChatAsPDF } from '$lib/apis/utils'; import { copyToClipboard, createMessagesList } from '$lib/utils'; @@ -36,6 +33,7 @@ import Share from '$lib/components/icons/Share.svelte'; import ArchiveBox from '$lib/components/icons/ArchiveBox.svelte'; import Messages from '$lib/components/chat/Messages.svelte'; + import Download from '$lib/components/icons/Download.svelte'; const i18n = getContext('i18n'); @@ -74,6 +72,11 @@ }; const downloadPdf = async () => { + const [{ default: jsPDF }, { default: html2canvas }] = await Promise.all([ + import('jspdf'), + import('html2canvas-pro') + ]); + if ($settings?.stylizedPdfExport ?? true) { showFullMessages = true; await tick(); @@ -274,14 +277,14 @@
+
+ { + const inputFiles = e.target.files; + + let reader = new FileReader(); + reader.onload = (event) => { + let originalImageUrl = `${event.target.result}`; + meta.background_image_url = originalImageUrl; + }; + + if ( + inputFiles && + inputFiles.length > 0 && + ['image/gif', 'image/webp', 'image/jpeg', 'image/png'].includes( + inputFiles[0]['type'] + ) + ) { + reader.readAsDataURL(inputFiles[0]); + } else { + console.log(`Unsupported File Type '${inputFiles[0]['type']}'.`); + + // clear the input + e.target.value = ''; + } + }} + /> + +
+
{$i18n.t('Folder Background Image')}
+ +
+ +
+
+
{#if $user?.role === 'admin' || ($user?.permissions.chat?.system_prompt ?? true)} diff --git a/src/lib/components/layout/Sidebar/RecursiveFolder.svelte b/src/lib/components/layout/Sidebar/RecursiveFolder.svelte index e9835434dd..8e64fc71aa 100644 --- a/src/lib/components/layout/Sidebar/RecursiveFolder.svelte +++ b/src/lib/components/layout/Sidebar/RecursiveFolder.svelte @@ -60,6 +60,8 @@ let draggedOver = false; let dragged = false; + let clickTimer = null; + let name = ''; const onDragOver = (e) => { @@ -280,7 +282,7 @@ } }; - const updateHandler = async ({ name, data }) => { + const updateHandler = async ({ name, meta, data }) => { if (name === '') { toast.error($i18n.t('Folder name cannot be empty.')); return; @@ -293,6 +295,7 @@ const res = await updateFolderById(localStorage.token, folderId, { name, + ...(meta ? { meta } : {}), ...(data ? { data } : {}) }).catch((error) => { toast.error(`${error}`); @@ -342,14 +345,15 @@ console.log('Edit'); await tick(); name = folders[folderId].name; - edit = true; + + await tick(); await tick(); const input = document.getElementById(`folder-${folderId}-input`); - if (input) { input.focus(); + input.select(); } }; @@ -427,47 +431,64 @@
+
diff --git a/src/lib/components/layout/Sidebar/UserMenu.svelte b/src/lib/components/layout/Sidebar/UserMenu.svelte index 2e568b49a6..ce95120007 100644 --- a/src/lib/components/layout/Sidebar/UserMenu.svelte +++ b/src/lib/components/layout/Sidebar/UserMenu.svelte @@ -64,14 +64,14 @@ fade(e, { duration: 100 })} > { show = false; @@ -90,7 +90,7 @@ { show = false; @@ -113,7 +113,7 @@ { show = false; if ($mobile) { @@ -130,7 +130,7 @@ { show = false; if ($mobile) { @@ -155,7 +155,7 @@ { show = false; @@ -170,7 +170,7 @@ { show = false; @@ -183,7 +183,7 @@ {/if} { show = false; @@ -203,7 +203,7 @@
{ const res = await userSignOut(); user.set(null); @@ -229,7 +229,7 @@ : ''} >
{ getUsageInfo(); }} diff --git a/src/lib/components/notes/AIMenu.svelte b/src/lib/components/notes/AIMenu.svelte index 68c1fbf505..79a0b8ed6c 100644 --- a/src/lib/components/notes/AIMenu.svelte +++ b/src/lib/components/notes/AIMenu.svelte @@ -27,7 +27,7 @@ { try { + const [{ default: jsPDF }, { default: html2canvas }] = await Promise.all([ + import('jspdf'), + import('html2canvas-pro') + ]); + // Define a fixed virtual screen size const virtualWidth = 1024; // Fixed width (adjust as needed) const virtualHeight = 1400; // Fixed height (adjust as needed) diff --git a/src/lib/components/notes/NoteEditor/Chat.svelte b/src/lib/components/notes/NoteEditor/Chat.svelte index 9a509a3dc9..a3fa9b5d25 100644 --- a/src/lib/components/notes/NoteEditor/Chat.svelte +++ b/src/lib/components/notes/NoteEditor/Chat.svelte @@ -327,8 +327,8 @@ Based on the user's instruction, update and enhance the existing notes or select }); -
-
+
+
-
+
@@ -375,7 +375,7 @@ Based on the user's instruction, update and enhance the existing notes or select
-
+
{#if selectedContent}
diff --git a/src/lib/components/notes/NoteEditor/Controls.svelte b/src/lib/components/notes/NoteEditor/Controls.svelte index 6ac64ba3ab..df988c28d9 100644 --- a/src/lib/components/notes/NoteEditor/Controls.svelte +++ b/src/lib/components/notes/NoteEditor/Controls.svelte @@ -17,8 +17,8 @@ }; -
-
+
+
-
+
{#if files.length > 0}
{$i18n.t('Files')}
diff --git a/src/lib/components/notes/NotePanel.svelte b/src/lib/components/notes/NotePanel.svelte index d26f4b72c7..676d86b83d 100644 --- a/src/lib/components/notes/NotePanel.svelte +++ b/src/lib/components/notes/NotePanel.svelte @@ -78,11 +78,12 @@ {/if} {:else if show} -
- -
+
diff --git a/src/lib/components/notes/Notes.svelte b/src/lib/components/notes/Notes.svelte index 7005387631..0e57c67230 100644 --- a/src/lib/components/notes/Notes.svelte +++ b/src/lib/components/notes/Notes.svelte @@ -5,9 +5,6 @@ const { saveAs } = fileSaver; - import jsPDF from 'jspdf'; - import html2canvas from 'html2canvas-pro'; - import dayjs from '$lib/dayjs'; import duration from 'dayjs/plugin/duration'; import relativeTime from 'dayjs/plugin/relativeTime'; @@ -137,6 +134,11 @@ const downloadPdf = async (note) => { try { + const [{ default: jsPDF }, { default: html2canvas }] = await Promise.all([ + import('jspdf'), + import('html2canvas-pro') + ]); + // Define a fixed virtual screen size const virtualWidth = 1024; // Fixed width (adjust as needed) const virtualHeight = 1400; // Fixed height (adjust as needed) @@ -374,7 +376,7 @@ > {#each notes[timeRange] as note, idx (note.id)}
{$i18n.t('Download')}
{ onDownload('txt'); }} @@ -71,7 +71,7 @@ { onDownload('md'); }} @@ -80,7 +80,7 @@ { onDownload('pdf'); }} @@ -93,21 +93,21 @@ {#if onCopyLink || onCopyToClipboard}
{$i18n.t('Share')}
{#if onCopyLink} { onCopyLink(); }} @@ -119,7 +119,7 @@ {#if onCopyToClipboard} { onCopyToClipboard(); }} @@ -133,12 +133,12 @@ {/if} { onDelete(); }} > - +
{$i18n.t('Delete')}
diff --git a/src/lib/components/notes/RecordMenu.svelte b/src/lib/components/notes/RecordMenu.svelte index 13f331eddd..fc9566aede 100644 --- a/src/lib/components/notes/RecordMenu.svelte +++ b/src/lib/components/notes/RecordMenu.svelte @@ -34,7 +34,7 @@ { dispatch('delete'); }} > - +
{$i18n.t('Delete')}
diff --git a/src/lib/components/workspace/Knowledge/KnowledgeBase.svelte b/src/lib/components/workspace/Knowledge/KnowledgeBase.svelte index 80af1d57a9..3c494e7609 100644 --- a/src/lib/components/workspace/Knowledge/KnowledgeBase.svelte +++ b/src/lib/components/workspace/Knowledge/KnowledgeBase.svelte @@ -51,6 +51,7 @@ import AccessControlModal from '../common/AccessControlModal.svelte'; import Search from '$lib/components/icons/Search.svelte'; import Textarea from '$lib/components/common/Textarea.svelte'; + import FilesOverlay from '$lib/components/chat/MessageInput/FilesOverlay.svelte'; let largeScreen = true; @@ -632,29 +633,7 @@ }; -{#if dragged} -
-
-
-
- -
- Drop any files here to add to my documents -
-
-
-
-
-
-{/if} - + -
-
-
{$i18n.t('Actions')}
-
+{#if actions.length > 0} +
+
+
{$i18n.t('Actions')}
+
-
- {$i18n.t('To select actions here, add them to the "Functions" workspace first.')} -
- -
- {#if actions.length > 0} -
+
+
{#each Object.keys(_actions) as action, actionIdx}
@@ -54,6 +50,6 @@
{/each}
- {/if} +
-
+{/if} diff --git a/src/lib/components/workspace/Models/DefaultFeatures.svelte b/src/lib/components/workspace/Models/DefaultFeatures.svelte new file mode 100644 index 0000000000..a64d48a0e4 --- /dev/null +++ b/src/lib/components/workspace/Models/DefaultFeatures.svelte @@ -0,0 +1,54 @@ + + +
+
+
{$i18n.t('Default Features')}
+
+
+ {#each availableFeatures as feature} +
+ { + if (e.detail === 'checked') { + featureIds = [...featureIds, feature]; + } else { + featureIds = featureIds.filter((id) => id !== feature); + } + }} + /> + +
+ + {$i18n.t(featureLabels[feature].label)} + +
+
+ {/each} +
+
diff --git a/src/lib/components/workspace/Models/DefaultFiltersSelector.svelte b/src/lib/components/workspace/Models/DefaultFiltersSelector.svelte new file mode 100644 index 0000000000..a83468e257 --- /dev/null +++ b/src/lib/components/workspace/Models/DefaultFiltersSelector.svelte @@ -0,0 +1,55 @@ + + +
+
+
{$i18n.t('Default Filters')}
+
+ +
+ {#if filters.length > 0} +
+ {#each Object.keys(_filters) as filter, filterIdx} +
+
+ { + _filters[filter].selected = e.detail === 'checked'; + selectedFilterIds = Object.keys(_filters).filter((t) => _filters[t].selected); + }} + /> +
+ +
+ + {_filters[filter].name} + +
+
+ {/each} +
+ {/if} +
+
diff --git a/src/lib/components/workspace/Models/FiltersSelector.svelte b/src/lib/components/workspace/Models/FiltersSelector.svelte index fa595d6f82..0c92419bb6 100644 --- a/src/lib/components/workspace/Models/FiltersSelector.svelte +++ b/src/lib/components/workspace/Models/FiltersSelector.svelte @@ -22,19 +22,15 @@ }); -
-
-
{$i18n.t('Filters')}
-
+{#if filters.length > 0} +
+
+
{$i18n.t('Filters')}
+
-
- {$i18n.t('To select filters here, add them to the "Functions" workspace first.')} -
- - -
- {#if filters.length > 0} -
+ +
+
{#each Object.keys(_filters) as filter, filterIdx}
@@ -62,6 +58,6 @@
{/each}
- {/if} +
-
+{/if} diff --git a/src/lib/components/workspace/Models/Knowledge/Selector.svelte b/src/lib/components/workspace/Models/Knowledge/Selector.svelte index 97719f7c6e..29c1ea7d5e 100644 --- a/src/lib/components/workspace/Models/Knowledge/Selector.svelte +++ b/src/lib/components/workspace/Models/Knowledge/Selector.svelte @@ -143,7 +143,7 @@
{#if item.legacy}
Legacy
{:else if item?.meta?.document}
Document
{:else if item?.type === 'file'}
File
{:else if item?.type === 'note'}
Note
{:else}
Collection
diff --git a/src/lib/components/workspace/Models/ModelEditor.svelte b/src/lib/components/workspace/Models/ModelEditor.svelte index fc9c167e9b..130c678712 100644 --- a/src/lib/components/workspace/Models/ModelEditor.svelte +++ b/src/lib/components/workspace/Models/ModelEditor.svelte @@ -1,8 +1,14 @@
@@ -143,6 +121,7 @@ } }; } + onChange(accessControl); }} > @@ -163,7 +142,7 @@
{#if accessControl !== null} {@const accessGroups = groups.filter((group) => - accessControl.read.group_ids.includes(group.id) + (accessControl?.read?.group_ids ?? []).includes(group.id) )}
@@ -182,11 +161,22 @@ {selectedGroupId ? '' : 'text-gray-500'} dark:placeholder-gray-500" bind:value={selectedGroupId} + on:change={() => { + if (selectedGroupId !== '') { + accessControl.read.group_ids = [ + ...(accessControl?.read?.group_ids ?? []), + selectedGroupId + ]; + + selectedGroupId = ''; + onChange(accessControl); + } + }} > - {#each groups.filter((group) => !accessControl.read.group_ids.includes(group.id)) as group} + {#each groups.filter((group) => !(accessControl?.read?.group_ids ?? []).includes(group.id)) as group} {/each} @@ -228,20 +218,21 @@ type="button" on:click={() => { if (accessRoles.includes('write')) { - if (accessControl.write.group_ids.includes(group.id)) { - accessControl.write.group_ids = accessControl.write.group_ids.filter( - (group_id) => group_id !== group.id - ); + if ((accessControl?.write?.group_ids ?? []).includes(group.id)) { + accessControl.write.group_ids = ( + accessControl?.write?.group_ids ?? [] + ).filter((group_id) => group_id !== group.id); } else { accessControl.write.group_ids = [ - ...accessControl.write.group_ids, + ...(accessControl?.write?.group_ids ?? []), group.id ]; } + onChange(accessControl); } }} > - {#if accessControl.write.group_ids.includes(group.id)} + {#if (accessControl?.write?.group_ids ?? []).includes(group.id)} {:else} @@ -252,9 +243,13 @@ class=" rounded-full p-1 hover:bg-gray-100 dark:hover:bg-gray-850 transition" type="button" on:click={() => { - accessControl.read.group_ids = accessControl.read.group_ids.filter( + accessControl.read.group_ids = (accessControl?.read?.group_ids ?? []).filter( (id) => id !== group.id ); + accessControl.write.group_ids = ( + accessControl?.write?.group_ids ?? [] + ).filter((id) => id !== group.id); + onChange(accessControl); }} > diff --git a/src/lib/components/workspace/common/ManifestModal.svelte b/src/lib/components/workspace/common/ManifestModal.svelte index 813a9fad1f..78146ecfd1 100644 --- a/src/lib/components/workspace/common/ManifestModal.svelte +++ b/src/lib/components/workspace/common/ManifestModal.svelte @@ -61,7 +61,7 @@
-
+
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index d4205bb9ee..934eec1e60 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -454,7 +454,7 @@ goto(`/channels/${event.channel_id}`); }, content: data?.content, - title: event?.channel?.name + title: `#${event?.channel?.name}` }, duration: 15000, unstyled: true @@ -694,6 +694,16 @@ {$WEBUI_NAME} + + + + {#if showRefresh} diff --git a/src/routes/auth/+page.svelte b/src/routes/auth/+page.svelte index 68c0df81ea..62eb47e6b0 100644 --- a/src/routes/auth/+page.svelte +++ b/src/routes/auth/+page.svelte @@ -523,6 +523,16 @@ > {/if} + {#if $config?.oauth?.providers?.feishu} + + {/if}
{/if} diff --git a/src/routes/s/[id]/+page.svelte b/src/routes/s/[id]/+page.svelte index 7d9a8a8a96..02ed91389b 100644 --- a/src/routes/s/[id]/+page.svelte +++ b/src/routes/s/[id]/+page.svelte @@ -199,7 +199,7 @@ >