mirror of
https://github.com/open-webui/open-webui.git
synced 2025-12-13 12:55:19 +00:00
Merge branch 'dev' of https://github.com/andrewbbaek/open-webui into dev
This commit is contained in:
commit
2a1c1e34dc
225 changed files with 5134 additions and 2302 deletions
70
CHANGELOG.md
70
CHANGELOG.md
|
|
@ -5,6 +5,76 @@ 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/),
|
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).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [0.6.30] - 2025-09-17
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- 🔑 Microsoft Entra ID authentication type support was added for Azure OpenAI connections, enabling enhanced security and streamlined authentication workflows.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- ☁️ OneDrive integration was fixed after recent breakage, restoring reliable account connectivity and file access.
|
||||||
|
|
||||||
|
## [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
|
## [0.6.28] - 2025-09-10
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
|
|
@ -730,6 +730,7 @@ def load_oauth_providers():
|
||||||
}
|
}
|
||||||
|
|
||||||
if FEISHU_CLIENT_ID.value and FEISHU_CLIENT_SECRET.value:
|
if FEISHU_CLIENT_ID.value and FEISHU_CLIENT_SECRET.value:
|
||||||
|
|
||||||
def feishu_oauth_register(client: OAuth):
|
def feishu_oauth_register(client: OAuth):
|
||||||
client.register(
|
client.register(
|
||||||
name="feishu",
|
name="feishu",
|
||||||
|
|
@ -2167,6 +2168,12 @@ ENABLE_ONEDRIVE_INTEGRATION = PersistentConfig(
|
||||||
"onedrive.enable",
|
"onedrive.enable",
|
||||||
os.getenv("ENABLE_ONEDRIVE_INTEGRATION", "False").lower() == "true",
|
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 = PersistentConfig(
|
||||||
"ONEDRIVE_CLIENT_ID",
|
"ONEDRIVE_CLIENT_ID",
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ from fastapi import (
|
||||||
from starlette.responses import Response, StreamingResponse
|
from starlette.responses import Response, StreamingResponse
|
||||||
|
|
||||||
|
|
||||||
|
from open_webui.constants import ERROR_MESSAGES
|
||||||
from open_webui.socket.main import (
|
from open_webui.socket.main import (
|
||||||
get_event_call,
|
get_event_call,
|
||||||
get_event_emitter,
|
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)
|
function_module, _, _ = get_function_module_from_cache(request, pipe_id)
|
||||||
|
|
||||||
if hasattr(function_module, "valves") and hasattr(function_module, "Valves"):
|
if hasattr(function_module, "valves") and hasattr(function_module, "Valves"):
|
||||||
|
Valves = function_module.Valves
|
||||||
valves = Functions.get_function_valves_by_id(pipe_id)
|
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
|
return function_module
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -70,6 +83,7 @@ async def get_function_models(request):
|
||||||
pipe_models = []
|
pipe_models = []
|
||||||
|
|
||||||
for pipe in pipes:
|
for pipe in pipes:
|
||||||
|
try:
|
||||||
function_module = get_function_module_by_id(request, pipe.id)
|
function_module = get_function_module_by_id(request, pipe.id)
|
||||||
|
|
||||||
# Check if function is a manifold
|
# Check if function is a manifold
|
||||||
|
|
@ -129,6 +143,9 @@ async def get_function_models(request):
|
||||||
"pipe": pipe_flag,
|
"pipe": pipe_flag,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(e)
|
||||||
|
continue
|
||||||
|
|
||||||
return pipe_models
|
return pipe_models
|
||||||
|
|
||||||
|
|
@ -222,7 +239,7 @@ async def generate_function_chat_completion(
|
||||||
oauth_token = None
|
oauth_token = None
|
||||||
try:
|
try:
|
||||||
if 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(
|
oauth_token = await request.app.state.oauth_manager.get_oauth_token(
|
||||||
user.id,
|
user.id,
|
||||||
request.cookies.get("oauth_session_id", None),
|
request.cookies.get("oauth_session_id", None),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -110,9 +110,6 @@ from open_webui.config import (
|
||||||
OLLAMA_API_CONFIGS,
|
OLLAMA_API_CONFIGS,
|
||||||
# OpenAI
|
# OpenAI
|
||||||
ENABLE_OPENAI_API,
|
ENABLE_OPENAI_API,
|
||||||
ONEDRIVE_CLIENT_ID,
|
|
||||||
ONEDRIVE_SHAREPOINT_URL,
|
|
||||||
ONEDRIVE_SHAREPOINT_TENANT_ID,
|
|
||||||
OPENAI_API_BASE_URLS,
|
OPENAI_API_BASE_URLS,
|
||||||
OPENAI_API_KEYS,
|
OPENAI_API_KEYS,
|
||||||
OPENAI_API_CONFIGS,
|
OPENAI_API_CONFIGS,
|
||||||
|
|
@ -303,14 +300,16 @@ from open_webui.config import (
|
||||||
GOOGLE_PSE_ENGINE_ID,
|
GOOGLE_PSE_ENGINE_ID,
|
||||||
GOOGLE_DRIVE_CLIENT_ID,
|
GOOGLE_DRIVE_CLIENT_ID,
|
||||||
GOOGLE_DRIVE_API_KEY,
|
GOOGLE_DRIVE_API_KEY,
|
||||||
|
ENABLE_ONEDRIVE_INTEGRATION,
|
||||||
ONEDRIVE_CLIENT_ID,
|
ONEDRIVE_CLIENT_ID,
|
||||||
ONEDRIVE_SHAREPOINT_URL,
|
ONEDRIVE_SHAREPOINT_URL,
|
||||||
ONEDRIVE_SHAREPOINT_TENANT_ID,
|
ONEDRIVE_SHAREPOINT_TENANT_ID,
|
||||||
|
ENABLE_ONEDRIVE_PERSONAL,
|
||||||
|
ENABLE_ONEDRIVE_BUSINESS,
|
||||||
ENABLE_RAG_HYBRID_SEARCH,
|
ENABLE_RAG_HYBRID_SEARCH,
|
||||||
ENABLE_RAG_LOCAL_WEB_FETCH,
|
ENABLE_RAG_LOCAL_WEB_FETCH,
|
||||||
ENABLE_WEB_LOADER_SSL_VERIFICATION,
|
ENABLE_WEB_LOADER_SSL_VERIFICATION,
|
||||||
ENABLE_GOOGLE_DRIVE_INTEGRATION,
|
ENABLE_GOOGLE_DRIVE_INTEGRATION,
|
||||||
ENABLE_ONEDRIVE_INTEGRATION,
|
|
||||||
UPLOAD_DIR,
|
UPLOAD_DIR,
|
||||||
EXTERNAL_WEB_SEARCH_URL,
|
EXTERNAL_WEB_SEARCH_URL,
|
||||||
EXTERNAL_WEB_SEARCH_API_KEY,
|
EXTERNAL_WEB_SEARCH_API_KEY,
|
||||||
|
|
@ -448,6 +447,7 @@ from open_webui.utils.models import (
|
||||||
get_all_models,
|
get_all_models,
|
||||||
get_all_base_models,
|
get_all_base_models,
|
||||||
check_model_access,
|
check_model_access,
|
||||||
|
get_filtered_models,
|
||||||
)
|
)
|
||||||
from open_webui.utils.chat import (
|
from open_webui.utils.chat import (
|
||||||
generate_chat_completion as chat_completion_handler,
|
generate_chat_completion as chat_completion_handler,
|
||||||
|
|
@ -1291,33 +1291,6 @@ if audit_level != AuditLevel.NONE:
|
||||||
async def get_models(
|
async def get_models(
|
||||||
request: Request, refresh: bool = False, user=Depends(get_verified_user)
|
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)
|
all_models = await get_all_models(request, refresh=refresh, user=user)
|
||||||
|
|
||||||
models = []
|
models = []
|
||||||
|
|
@ -1353,11 +1326,6 @@ 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(
|
log.debug(
|
||||||
|
|
@ -1730,6 +1698,14 @@ async def get_app_config(request: Request):
|
||||||
"enable_admin_chat_access": ENABLE_ADMIN_CHAT_ACCESS,
|
"enable_admin_chat_access": ENABLE_ADMIN_CHAT_ACCESS,
|
||||||
"enable_google_drive_integration": app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION,
|
"enable_google_drive_integration": app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION,
|
||||||
"enable_onedrive_integration": app.state.config.ENABLE_ONEDRIVE_INTEGRATION,
|
"enable_onedrive_integration": app.state.config.ENABLE_ONEDRIVE_INTEGRATION,
|
||||||
|
**(
|
||||||
|
{
|
||||||
|
"enable_onedrive_personal": ENABLE_ONEDRIVE_PERSONAL,
|
||||||
|
"enable_onedrive_business": ENABLE_ONEDRIVE_BUSINESS,
|
||||||
|
}
|
||||||
|
if app.state.config.ENABLE_ONEDRIVE_INTEGRATION
|
||||||
|
else {}
|
||||||
|
),
|
||||||
}
|
}
|
||||||
if user is not None
|
if user is not None
|
||||||
else {}
|
else {}
|
||||||
|
|
|
||||||
|
|
@ -201,8 +201,14 @@ class MessageTable:
|
||||||
with get_db() as db:
|
with get_db() as db:
|
||||||
message = db.get(Message, id)
|
message = db.get(Message, id)
|
||||||
message.content = form_data.content
|
message.content = form_data.content
|
||||||
message.data = form_data.data
|
message.data = {
|
||||||
message.meta = form_data.meta
|
**(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())
|
message.updated_at = int(time.time_ns())
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(message)
|
db.refresh(message)
|
||||||
|
|
|
||||||
|
|
@ -107,11 +107,21 @@ class UserInfoResponse(BaseModel):
|
||||||
role: str
|
role: str
|
||||||
|
|
||||||
|
|
||||||
|
class UserIdNameResponse(BaseModel):
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
|
||||||
|
|
||||||
class UserInfoListResponse(BaseModel):
|
class UserInfoListResponse(BaseModel):
|
||||||
users: list[UserInfoResponse]
|
users: list[UserInfoResponse]
|
||||||
total: int
|
total: int
|
||||||
|
|
||||||
|
|
||||||
|
class UserIdNameListResponse(BaseModel):
|
||||||
|
users: list[UserIdNameResponse]
|
||||||
|
total: int
|
||||||
|
|
||||||
|
|
||||||
class UserResponse(BaseModel):
|
class UserResponse(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
name: str
|
name: str
|
||||||
|
|
@ -210,7 +220,7 @@ class UsersTable:
|
||||||
filter: Optional[dict] = None,
|
filter: Optional[dict] = None,
|
||||||
skip: Optional[int] = None,
|
skip: Optional[int] = None,
|
||||||
limit: Optional[int] = None,
|
limit: Optional[int] = None,
|
||||||
) -> UserListResponse:
|
) -> dict:
|
||||||
with get_db() as db:
|
with get_db() as db:
|
||||||
query = db.query(User)
|
query = db.query(User)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -621,6 +621,7 @@ def get_sources_from_items(
|
||||||
|
|
||||||
if knowledge_base and (
|
if knowledge_base and (
|
||||||
user.role == "admin"
|
user.role == "admin"
|
||||||
|
or knowledge_base.user_id == user.id
|
||||||
or has_access(user.id, "read", knowledge_base.access_control)
|
or has_access(user.id, "read", knowledge_base.access_control)
|
||||||
):
|
):
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -550,7 +550,7 @@ def transcription_handler(request, file_path, metadata):
|
||||||
metadata = metadata or {}
|
metadata = metadata or {}
|
||||||
|
|
||||||
languages = [
|
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
|
None, # Always fallback to None in case transcription fails
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,9 +24,17 @@ from open_webui.constants import ERROR_MESSAGES
|
||||||
from open_webui.env import SRC_LOG_LEVELS
|
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.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.access_control import has_access, get_users_with_access
|
||||||
from open_webui.utils.webhook import post_webhook
|
from open_webui.utils.webhook import post_webhook
|
||||||
|
from open_webui.utils.channels import extract_mentions, replace_mentions
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
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)
|
users = get_users_with_access("read", channel.access_control)
|
||||||
|
|
||||||
for user in users:
|
for user in users:
|
||||||
if user.id in active_user_ids:
|
if user.id not in active_user_ids:
|
||||||
continue
|
|
||||||
else:
|
|
||||||
if user.settings:
|
if user.settings:
|
||||||
webhook_url = user.settings.ui.get("notifications", {}).get(
|
webhook_url = user.settings.ui.get("notifications", {}).get(
|
||||||
"webhook_url", None
|
"webhook_url", None
|
||||||
)
|
)
|
||||||
|
|
||||||
if webhook_url:
|
if webhook_url:
|
||||||
await post_webhook(
|
await post_webhook(
|
||||||
name,
|
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(
|
async def model_response_handler(request, channel, message, user):
|
||||||
request: Request,
|
MODELS = {
|
||||||
id: str,
|
model["id"]: model
|
||||||
form_data: MessageForm,
|
for model in get_filtered_models(await get_all_models(request, user=user), user)
|
||||||
background_tasks: BackgroundTasks,
|
}
|
||||||
user=Depends(get_verified_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)
|
channel = Channels.get_channel_by_id(id)
|
||||||
if not channel:
|
if not channel:
|
||||||
|
|
@ -302,11 +427,30 @@ async def post_new_message(
|
||||||
},
|
},
|
||||||
to=f"channel:{channel.id}",
|
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()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@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}")
|
active_user_ids = get_user_ids_from_room(f"channel:{channel.id}")
|
||||||
|
|
||||||
background_tasks.add_task(
|
async def background_handler():
|
||||||
send_notification,
|
await model_response_handler(request, channel, message, user)
|
||||||
|
await send_notification(
|
||||||
request.app.state.WEBUI_NAME,
|
request.app.state.WEBUI_NAME,
|
||||||
request.app.state.config.WEBUI_URL,
|
request.app.state.config.WEBUI_URL,
|
||||||
channel,
|
channel,
|
||||||
|
|
@ -314,7 +458,12 @@ async def post_new_message(
|
||||||
active_user_ids,
|
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:
|
except Exception as e:
|
||||||
log.exception(e)
|
log.exception(e)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|
|
||||||
|
|
@ -166,7 +166,7 @@ async def import_chat(form_data: ChatImportForm, user=Depends(get_verified_user)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/search", response_model=list[ChatTitleIdResponse])
|
@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)
|
text: str, page: Optional[int] = None, user=Depends(get_verified_user)
|
||||||
):
|
):
|
||||||
if page is None:
|
if page is None:
|
||||||
|
|
|
||||||
|
|
@ -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"
|
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)
|
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:
|
except Exception as e:
|
||||||
log.error(f"Error processing file: {file_item.id}")
|
log.error(f"Error processing file: {file_item.id}")
|
||||||
Files.update_file_data_by_id(
|
Files.update_file_data_by_id(
|
||||||
|
|
|
||||||
|
|
@ -148,6 +148,18 @@ async def sync_functions(
|
||||||
content=function.content,
|
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)
|
return Functions.sync_functions(user.id, form_data.functions)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(f"Failed to load a function: {e}")
|
log.exception(f"Failed to load a function: {e}")
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
import io
|
||||||
|
import base64
|
||||||
|
|
||||||
from open_webui.models.models import (
|
from open_webui.models.models import (
|
||||||
ModelForm,
|
ModelForm,
|
||||||
|
|
@ -10,12 +12,13 @@ from open_webui.models.models import (
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from open_webui.constants import ERROR_MESSAGES
|
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.auth import get_admin_user, get_verified_user
|
||||||
from open_webui.utils.access_control import has_access, has_permission
|
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()
|
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
|
# ToggleModelById
|
||||||
############################
|
############################
|
||||||
|
|
|
||||||
|
|
@ -1694,13 +1694,15 @@ async def download_file_stream(
|
||||||
yield f'data: {{"progress": {progress}, "completed": {current_size}, "total": {total_size}}}\n\n'
|
yield f'data: {{"progress": {progress}, "completed": {current_size}, "total": {total_size}}}\n\n'
|
||||||
|
|
||||||
if done:
|
if done:
|
||||||
file.seek(0)
|
file.close()
|
||||||
|
|
||||||
|
with open(file_path, "rb") as file:
|
||||||
chunk_size = 1024 * 1024 * 2
|
chunk_size = 1024 * 1024 * 2
|
||||||
hashed = calculate_sha256(file, chunk_size)
|
hashed = calculate_sha256(file, chunk_size)
|
||||||
file.seek(0)
|
|
||||||
|
|
||||||
url = f"{ollama_url}/api/blobs/sha256:{hashed}"
|
url = f"{ollama_url}/api/blobs/sha256:{hashed}"
|
||||||
response = requests.post(url, data=file)
|
with requests.Session() as session:
|
||||||
|
response = session.post(url, data=file, timeout=30)
|
||||||
|
|
||||||
if response.ok:
|
if response.ok:
|
||||||
res = {
|
res = {
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ from aiocache import cached
|
||||||
import requests
|
import requests
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
from azure.identity import DefaultAzureCredential, get_bearer_token_provider
|
||||||
|
|
||||||
from fastapi import Depends, HTTPException, Request, APIRouter
|
from fastapi import Depends, HTTPException, Request, APIRouter
|
||||||
from fastapi.responses import (
|
from fastapi.responses import (
|
||||||
FileResponse,
|
FileResponse,
|
||||||
|
|
@ -119,7 +121,7 @@ def openai_reasoning_model_handler(payload):
|
||||||
return payload
|
return payload
|
||||||
|
|
||||||
|
|
||||||
def get_headers_and_cookies(
|
async def get_headers_and_cookies(
|
||||||
request: Request,
|
request: Request,
|
||||||
url,
|
url,
|
||||||
key=None,
|
key=None,
|
||||||
|
|
@ -172,7 +174,7 @@ def get_headers_and_cookies(
|
||||||
oauth_token = None
|
oauth_token = None
|
||||||
try:
|
try:
|
||||||
if 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(
|
oauth_token = await request.app.state.oauth_manager.get_oauth_token(
|
||||||
user.id,
|
user.id,
|
||||||
request.cookies.get("oauth_session_id", None),
|
request.cookies.get("oauth_session_id", None),
|
||||||
)
|
)
|
||||||
|
|
@ -182,12 +184,30 @@ def get_headers_and_cookies(
|
||||||
if oauth_token:
|
if oauth_token:
|
||||||
token = f"{oauth_token.get('access_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:
|
if token:
|
||||||
headers["Authorization"] = f"Bearer {token}"
|
headers["Authorization"] = f"Bearer {token}"
|
||||||
|
|
||||||
return headers, cookies
|
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
|
# API routes
|
||||||
|
|
@ -285,7 +305,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
|
||||||
request.app.state.config.OPENAI_API_CONFIGS.get(url, {}), # Legacy support
|
request.app.state.config.OPENAI_API_CONFIGS.get(url, {}), # Legacy support
|
||||||
)
|
)
|
||||||
|
|
||||||
headers, cookies = get_headers_and_cookies(
|
headers, cookies = await get_headers_and_cookies(
|
||||||
request, url, key, api_config, user=user
|
request, url, key, api_config, user=user
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -550,7 +570,7 @@ async def get_models(
|
||||||
timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST),
|
timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST),
|
||||||
) as session:
|
) as session:
|
||||||
try:
|
try:
|
||||||
headers, cookies = get_headers_and_cookies(
|
headers, cookies = await get_headers_and_cookies(
|
||||||
request, url, key, api_config, user=user
|
request, url, key, api_config, user=user
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -636,14 +656,17 @@ async def verify_connection(
|
||||||
timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST),
|
timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST),
|
||||||
) as session:
|
) as session:
|
||||||
try:
|
try:
|
||||||
headers, cookies = get_headers_and_cookies(
|
headers, cookies = await get_headers_and_cookies(
|
||||||
request, url, key, api_config, user=user
|
request, url, key, api_config, user=user
|
||||||
)
|
)
|
||||||
|
|
||||||
if api_config.get("azure", False):
|
if api_config.get("azure", False):
|
||||||
|
# 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-key"] = key
|
||||||
api_version = api_config.get("api_version", "") or "2023-03-15-preview"
|
|
||||||
|
|
||||||
|
api_version = api_config.get("api_version", "") or "2023-03-15-preview"
|
||||||
async with session.get(
|
async with session.get(
|
||||||
url=f"{url}/openai/models?api-version={api_version}",
|
url=f"{url}/openai/models?api-version={api_version}",
|
||||||
headers=headers,
|
headers=headers,
|
||||||
|
|
@ -878,14 +901,19 @@ async def generate_chat_completion(
|
||||||
convert_logit_bias_input_to_json(payload["logit_bias"])
|
convert_logit_bias_input_to_json(payload["logit_bias"])
|
||||||
)
|
)
|
||||||
|
|
||||||
headers, cookies = get_headers_and_cookies(
|
headers, cookies = await get_headers_and_cookies(
|
||||||
request, url, key, api_config, metadata, user=user
|
request, url, key, api_config, metadata, user=user
|
||||||
)
|
)
|
||||||
|
|
||||||
if api_config.get("azure", False):
|
if api_config.get("azure", False):
|
||||||
api_version = api_config.get("api_version", "2023-03-15-preview")
|
api_version = api_config.get("api_version", "2023-03-15-preview")
|
||||||
request_url, payload = convert_to_azure_payload(url, payload, api_version)
|
request_url, payload = convert_to_azure_payload(url, payload, api_version)
|
||||||
|
|
||||||
|
# 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-key"] = key
|
||||||
|
|
||||||
headers["api-version"] = api_version
|
headers["api-version"] = api_version
|
||||||
request_url = f"{request_url}/chat/completions?api-version={api_version}"
|
request_url = f"{request_url}/chat/completions?api-version={api_version}"
|
||||||
else:
|
else:
|
||||||
|
|
@ -982,7 +1010,9 @@ async def embeddings(request: Request, form_data: dict, user):
|
||||||
session = None
|
session = None
|
||||||
streaming = False
|
streaming = False
|
||||||
|
|
||||||
headers, cookies = get_headers_and_cookies(request, url, key, api_config, user=user)
|
headers, cookies = await get_headers_and_cookies(
|
||||||
|
request, url, key, api_config, user=user
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
session = aiohttp.ClientSession(trust_env=True)
|
session = aiohttp.ClientSession(trust_env=True)
|
||||||
r = await session.request(
|
r = await session.request(
|
||||||
|
|
@ -1052,13 +1082,18 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
|
||||||
streaming = False
|
streaming = False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
headers, cookies = get_headers_and_cookies(
|
headers, cookies = await get_headers_and_cookies(
|
||||||
request, url, key, api_config, user=user
|
request, url, key, api_config, user=user
|
||||||
)
|
)
|
||||||
|
|
||||||
if api_config.get("azure", False):
|
if api_config.get("azure", False):
|
||||||
api_version = api_config.get("api_version", "2023-03-15-preview")
|
api_version = api_config.get("api_version", "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
|
headers["api-key"] = key
|
||||||
|
|
||||||
headers["api-version"] = api_version
|
headers["api-version"] = api_version
|
||||||
|
|
||||||
payload = json.loads(body)
|
payload = json.loads(body)
|
||||||
|
|
|
||||||
|
|
@ -1584,12 +1584,19 @@ def process_file(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Files.update_file_data_by_id(
|
||||||
|
file.id,
|
||||||
|
{"status": "completed"},
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"status": True,
|
"status": True,
|
||||||
"collection_name": collection_name,
|
"collection_name": collection_name,
|
||||||
"filename": file.filename,
|
"filename": file.filename,
|
||||||
"content": text_content,
|
"content": text_content,
|
||||||
}
|
}
|
||||||
|
else:
|
||||||
|
raise Exception("Error saving document to vector database")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ from open_webui.models.users import (
|
||||||
UserModel,
|
UserModel,
|
||||||
UserListResponse,
|
UserListResponse,
|
||||||
UserInfoListResponse,
|
UserInfoListResponse,
|
||||||
|
UserIdNameListResponse,
|
||||||
UserRoleUpdateForm,
|
UserRoleUpdateForm,
|
||||||
Users,
|
Users,
|
||||||
UserSettings,
|
UserSettings,
|
||||||
|
|
@ -100,6 +101,23 @@ async def get_all_users(
|
||||||
return Users.get_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
|
# User Groups
|
||||||
############################
|
############################
|
||||||
|
|
|
||||||
|
|
@ -130,9 +130,10 @@ def has_access(
|
||||||
# Get all users with access to a resource
|
# Get all users with access to a resource
|
||||||
def get_users_with_access(
|
def get_users_with_access(
|
||||||
type: str = "write", access_control: Optional[dict] = None
|
type: str = "write", access_control: Optional[dict] = None
|
||||||
) -> List[UserModel]:
|
) -> list[UserModel]:
|
||||||
if access_control is None:
|
if access_control is None:
|
||||||
return Users.get_users()
|
result = Users.get_users()
|
||||||
|
return result.get("users", [])
|
||||||
|
|
||||||
permission_access = access_control.get(type, {})
|
permission_access = access_control.get(type, {})
|
||||||
permitted_group_ids = permission_access.get("group_ids", [])
|
permitted_group_ids = permission_access.get("group_ids", [])
|
||||||
|
|
|
||||||
31
backend/open_webui/utils/channels.py
Normal file
31
backend/open_webui/utils/channels.py
Normal file
|
|
@ -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)
|
||||||
|
|
@ -20,6 +20,7 @@ from concurrent.futures import ThreadPoolExecutor
|
||||||
|
|
||||||
|
|
||||||
from fastapi import Request, HTTPException
|
from fastapi import Request, HTTPException
|
||||||
|
from fastapi.responses import HTMLResponse
|
||||||
from starlette.responses import Response, StreamingResponse, JSONResponse
|
from starlette.responses import Response, StreamingResponse, JSONResponse
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -818,7 +819,7 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
||||||
oauth_token = None
|
oauth_token = None
|
||||||
try:
|
try:
|
||||||
if 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(
|
oauth_token = await request.app.state.oauth_manager.get_oauth_token(
|
||||||
user.id,
|
user.id,
|
||||||
request.cookies.get("oauth_session_id", None),
|
request.cookies.get("oauth_session_id", None),
|
||||||
)
|
)
|
||||||
|
|
@ -1498,7 +1499,7 @@ async def process_chat_response(
|
||||||
oauth_token = None
|
oauth_token = None
|
||||||
try:
|
try:
|
||||||
if 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(
|
oauth_token = await request.app.state.oauth_manager.get_oauth_token(
|
||||||
user.id,
|
user.id,
|
||||||
request.cookies.get("oauth_session_id", None),
|
request.cookies.get("oauth_session_id", None),
|
||||||
)
|
)
|
||||||
|
|
@ -1581,7 +1582,8 @@ async def process_chat_response(
|
||||||
break
|
break
|
||||||
|
|
||||||
if tool_result is not None:
|
if tool_result is not None:
|
||||||
tool_calls_display_content = f'{tool_calls_display_content}<details type="tool_calls" done="true" id="{tool_call_id}" name="{tool_name}" arguments="{html.escape(json.dumps(tool_arguments))}" result="{html.escape(json.dumps(tool_result, ensure_ascii=False))}" files="{html.escape(json.dumps(tool_result_files)) if tool_result_files else ""}">\n<summary>Tool Executed</summary>\n</details>\n'
|
tool_result_embeds = result.get("embeds", "")
|
||||||
|
tool_calls_display_content = f'{tool_calls_display_content}<details type="tool_calls" done="true" id="{tool_call_id}" name="{tool_name}" arguments="{html.escape(json.dumps(tool_arguments))}" result="{html.escape(json.dumps(tool_result, ensure_ascii=False))}" files="{html.escape(json.dumps(tool_result_files)) if tool_result_files else ""}" embeds="{html.escape(json.dumps(tool_result_embeds))}">\n<summary>Tool Executed</summary>\n</details>\n'
|
||||||
else:
|
else:
|
||||||
tool_calls_display_content = f'{tool_calls_display_content}<details type="tool_calls" done="false" id="{tool_call_id}" name="{tool_name}" arguments="{html.escape(json.dumps(tool_arguments))}">\n<summary>Executing...</summary>\n</details>\n'
|
tool_calls_display_content = f'{tool_calls_display_content}<details type="tool_calls" done="false" id="{tool_call_id}" name="{tool_name}" arguments="{html.escape(json.dumps(tool_arguments))}">\n<summary>Executing...</summary>\n</details>\n'
|
||||||
|
|
||||||
|
|
@ -2031,6 +2033,20 @@ async def process_chat_response(
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
choices = data.get("choices", [])
|
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:
|
if not choices:
|
||||||
error = data.get("error", {})
|
error = data.get("error", {})
|
||||||
if error:
|
if error:
|
||||||
|
|
@ -2042,20 +2058,6 @@ async def process_chat_response(
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
usage = data.get("usage", {})
|
|
||||||
usage.update(
|
|
||||||
data.get("timings", {})
|
|
||||||
) # llama.cpp
|
|
||||||
|
|
||||||
if usage:
|
|
||||||
await event_emitter(
|
|
||||||
{
|
|
||||||
"type": "chat:completion",
|
|
||||||
"data": {
|
|
||||||
"usage": usage,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
delta = choices[0].get("delta", {})
|
delta = choices[0].get("delta", {})
|
||||||
|
|
@ -2402,6 +2404,75 @@ async def process_chat_response(
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
tool_result = str(e)
|
tool_result = str(e)
|
||||||
|
|
||||||
|
tool_result_embeds = []
|
||||||
|
|
||||||
|
if isinstance(tool_result, HTMLResponse):
|
||||||
|
content_disposition = tool_result.headers.get(
|
||||||
|
"Content-Disposition", ""
|
||||||
|
)
|
||||||
|
if "inline" in content_disposition:
|
||||||
|
content = tool_result.body.decode("utf-8")
|
||||||
|
tool_result_embeds.append(content)
|
||||||
|
|
||||||
|
if 200 <= tool_result.status_code < 300:
|
||||||
|
tool_result = {
|
||||||
|
"status": "success",
|
||||||
|
"code": "ui_component",
|
||||||
|
"message": "Embedded UI result is active and visible to the user.",
|
||||||
|
}
|
||||||
|
elif 400 <= tool_result.status_code < 500:
|
||||||
|
tool_result = {
|
||||||
|
"status": "error",
|
||||||
|
"code": "ui_component",
|
||||||
|
"message": f"Client error {tool_result.status_code} from embedded UI result.",
|
||||||
|
}
|
||||||
|
elif 500 <= tool_result.status_code < 600:
|
||||||
|
tool_result = {
|
||||||
|
"status": "error",
|
||||||
|
"code": "ui_component",
|
||||||
|
"message": f"Server error {tool_result.status_code} from embedded UI result.",
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
tool_result = {
|
||||||
|
"status": "error",
|
||||||
|
"code": "ui_component",
|
||||||
|
"message": f"Unexpected status code {tool_result.status_code} from embedded UI result.",
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
tool_result = tool_result.body.decode("utf-8")
|
||||||
|
|
||||||
|
elif tool.get("type") == "external" and isinstance(
|
||||||
|
tool_result, tuple
|
||||||
|
):
|
||||||
|
tool_result, tool_response_headers = tool_result
|
||||||
|
|
||||||
|
if tool_response_headers:
|
||||||
|
content_disposition = tool_response_headers.get(
|
||||||
|
"Content-Disposition", ""
|
||||||
|
)
|
||||||
|
|
||||||
|
if "inline" in content_disposition:
|
||||||
|
content_type = tool_response_headers.get(
|
||||||
|
"Content-Type", ""
|
||||||
|
)
|
||||||
|
location = tool_response_headers.get("Location", "")
|
||||||
|
|
||||||
|
if "text/html" in content_type:
|
||||||
|
# Display as iframe embed
|
||||||
|
tool_result_embeds.append(tool_result)
|
||||||
|
tool_result = {
|
||||||
|
"status": "success",
|
||||||
|
"code": "ui_component",
|
||||||
|
"message": "Embedded UI result is active and visible to the user.",
|
||||||
|
}
|
||||||
|
elif location:
|
||||||
|
tool_result_embeds.append(location)
|
||||||
|
tool_result = {
|
||||||
|
"status": "success",
|
||||||
|
"code": "ui_component",
|
||||||
|
"message": "Embedded UI result is active and visible to the user.",
|
||||||
|
}
|
||||||
|
|
||||||
tool_result_files = []
|
tool_result_files = []
|
||||||
if isinstance(tool_result, list):
|
if isinstance(tool_result, list):
|
||||||
for item in tool_result:
|
for item in tool_result:
|
||||||
|
|
@ -2426,6 +2497,11 @@ async def process_chat_response(
|
||||||
if tool_result_files
|
if tool_result_files
|
||||||
else {}
|
else {}
|
||||||
),
|
),
|
||||||
|
**(
|
||||||
|
{"embeds": tool_result_embeds}
|
||||||
|
if tool_result_embeds
|
||||||
|
else {}
|
||||||
|
),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,10 +22,11 @@ from open_webui.utils.access_control import has_access
|
||||||
|
|
||||||
|
|
||||||
from open_webui.config import (
|
from open_webui.config import (
|
||||||
|
BYPASS_ADMIN_ACCESS_CONTROL,
|
||||||
DEFAULT_ARENA_MODEL,
|
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
|
from open_webui.models.users import UserModel
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -332,3 +333,40 @@ def check_model_access(user, model):
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
raise Exception("Model not found")
|
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
|
||||||
|
|
|
||||||
|
|
@ -157,7 +157,7 @@ class OAuthManager:
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_oauth_token(
|
async def get_oauth_token(
|
||||||
self, user_id: str, session_id: str, force_refresh: bool = False
|
self, user_id: str, session_id: str, force_refresh: bool = False
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
|
|
@ -186,7 +186,7 @@ class OAuthManager:
|
||||||
log.debug(
|
log.debug(
|
||||||
f"Token refresh needed for user {user_id}, provider {session.provider}"
|
f"Token refresh needed for user {user_id}, provider {session.provider}"
|
||||||
)
|
)
|
||||||
refreshed_token = self._refresh_token(session)
|
refreshed_token = await self._refresh_token(session)
|
||||||
if refreshed_token:
|
if refreshed_token:
|
||||||
return refreshed_token
|
return refreshed_token
|
||||||
else:
|
else:
|
||||||
|
|
@ -602,7 +602,11 @@ class OAuthManager:
|
||||||
or (auth_manager_config.OAUTH_USERNAME_CLAIM not in user_data)
|
or (auth_manager_config.OAUTH_USERNAME_CLAIM not in user_data)
|
||||||
):
|
):
|
||||||
user_data: UserInfo = await client.userinfo(token=token)
|
user_data: UserInfo = await client.userinfo(token=token)
|
||||||
if provider == "feishu" and isinstance(user_data, dict) and "data" in user_data:
|
if (
|
||||||
|
provider == "feishu"
|
||||||
|
and isinstance(user_data, dict)
|
||||||
|
and "data" in user_data
|
||||||
|
):
|
||||||
user_data = user_data["data"]
|
user_data = user_data["data"]
|
||||||
if not user_data:
|
if not user_data:
|
||||||
log.warning(f"OAuth callback failed, user data is missing: {token}")
|
log.warning(f"OAuth callback failed, user data is missing: {token}")
|
||||||
|
|
|
||||||
|
|
@ -163,7 +163,16 @@ def setup_metrics(app: FastAPI, resource: Resource) -> None:
|
||||||
@app.middleware("http")
|
@app.middleware("http")
|
||||||
async def _metrics_middleware(request: Request, call_next):
|
async def _metrics_middleware(request: Request, call_next):
|
||||||
start_time = time.perf_counter()
|
start_time = time.perf_counter()
|
||||||
|
|
||||||
|
status_code = None
|
||||||
|
try:
|
||||||
response = await call_next(request)
|
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
|
elapsed_ms = (time.perf_counter() - start_time) * 1000.0
|
||||||
|
|
||||||
# Route template e.g. "/items/{item_id}" instead of real path.
|
# Route template e.g. "/items/{item_id}" instead of real path.
|
||||||
|
|
@ -173,10 +182,8 @@ def setup_metrics(app: FastAPI, resource: Resource) -> None:
|
||||||
attrs: Dict[str, str | int] = {
|
attrs: Dict[str, str | int] = {
|
||||||
"http.method": request.method,
|
"http.method": request.method,
|
||||||
"http.route": route_path,
|
"http.route": route_path,
|
||||||
"http.status_code": response.status_code,
|
"http.status_code": status_code,
|
||||||
}
|
}
|
||||||
|
|
||||||
request_counter.add(1, attrs)
|
request_counter.add(1, attrs)
|
||||||
duration_histogram.record(elapsed_ms, attrs)
|
duration_histogram.record(elapsed_ms, attrs)
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
|
||||||
|
|
@ -171,6 +171,8 @@ async def get_tools(
|
||||||
"tool_id": tool_id,
|
"tool_id": tool_id,
|
||||||
"callable": callable,
|
"callable": callable,
|
||||||
"spec": spec,
|
"spec": spec,
|
||||||
|
# Misc info
|
||||||
|
"type": "external",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Handle function name collisions
|
# Handle function name collisions
|
||||||
|
|
@ -646,7 +648,7 @@ async def execute_tool_server(
|
||||||
name: str,
|
name: str,
|
||||||
params: Dict[str, Any],
|
params: Dict[str, Any],
|
||||||
server_data: Dict[str, Any],
|
server_data: Dict[str, Any],
|
||||||
) -> Any:
|
) -> Tuple[Dict[str, Any], Optional[Dict[str, Any]]]:
|
||||||
error = None
|
error = None
|
||||||
try:
|
try:
|
||||||
openapi = server_data.get("openapi", {})
|
openapi = server_data.get("openapi", {})
|
||||||
|
|
@ -718,6 +720,7 @@ async def execute_tool_server(
|
||||||
headers=headers,
|
headers=headers,
|
||||||
cookies=cookies,
|
cookies=cookies,
|
||||||
ssl=AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL,
|
ssl=AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL,
|
||||||
|
allow_redirects=False,
|
||||||
) as response:
|
) as response:
|
||||||
if response.status >= 400:
|
if response.status >= 400:
|
||||||
text = await response.text()
|
text = await response.text()
|
||||||
|
|
@ -728,13 +731,15 @@ async def execute_tool_server(
|
||||||
except Exception:
|
except Exception:
|
||||||
response_data = await response.text()
|
response_data = await response.text()
|
||||||
|
|
||||||
return response_data
|
response_headers = response.headers
|
||||||
|
return (response_data, response_headers)
|
||||||
else:
|
else:
|
||||||
async with request_method(
|
async with request_method(
|
||||||
final_url,
|
final_url,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
cookies=cookies,
|
cookies=cookies,
|
||||||
ssl=AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL,
|
ssl=AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL,
|
||||||
|
allow_redirects=False,
|
||||||
) as response:
|
) as response:
|
||||||
if response.status >= 400:
|
if response.status >= 400:
|
||||||
text = await response.text()
|
text = await response.text()
|
||||||
|
|
@ -745,12 +750,13 @@ async def execute_tool_server(
|
||||||
except Exception:
|
except Exception:
|
||||||
response_data = await response.text()
|
response_data = await response.text()
|
||||||
|
|
||||||
return response_data
|
response_headers = response.headers
|
||||||
|
return (response_data, response_headers)
|
||||||
|
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
error = str(err)
|
error = str(err)
|
||||||
log.exception(f"API Request Error: {error}")
|
log.exception(f"API Request Error: {error}")
|
||||||
return {"error": error}
|
return ({"error": error}, None)
|
||||||
|
|
||||||
|
|
||||||
def get_tool_server_url(url: Optional[str], path: str) -> str:
|
def get_tool_server_url(url: Optional[str], path: str) -> str:
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ fastapi==0.115.7
|
||||||
uvicorn[standard]==0.35.0
|
uvicorn[standard]==0.35.0
|
||||||
pydantic==2.11.7
|
pydantic==2.11.7
|
||||||
python-multipart==0.0.20
|
python-multipart==0.0.20
|
||||||
|
itsdangerous==2.2.0
|
||||||
|
|
||||||
python-socketio==5.13.0
|
python-socketio==5.13.0
|
||||||
python-jose==3.4.0
|
python-jose==3.4.0
|
||||||
|
|
|
||||||
29
package-lock.json
generated
29
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "open-webui",
|
"name": "open-webui",
|
||||||
"version": "0.6.29",
|
"version": "0.6.30",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "open-webui",
|
"name": "open-webui",
|
||||||
"version": "0.6.29",
|
"version": "0.6.30",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@azure/msal-browser": "^4.5.0",
|
"@azure/msal-browser": "^4.5.0",
|
||||||
"@codemirror/lang-javascript": "^6.2.2",
|
"@codemirror/lang-javascript": "^6.2.2",
|
||||||
|
|
@ -39,6 +39,7 @@
|
||||||
"@tiptap/starter-kit": "^3.0.7",
|
"@tiptap/starter-kit": "^3.0.7",
|
||||||
"@tiptap/suggestion": "^3.4.2",
|
"@tiptap/suggestion": "^3.4.2",
|
||||||
"@xyflow/svelte": "^0.1.19",
|
"@xyflow/svelte": "^0.1.19",
|
||||||
|
"alpinejs": "^3.15.0",
|
||||||
"async": "^3.2.5",
|
"async": "^3.2.5",
|
||||||
"bits-ui": "^0.21.15",
|
"bits-ui": "^0.21.15",
|
||||||
"chart.js": "^4.5.0",
|
"chart.js": "^4.5.0",
|
||||||
|
|
@ -4569,6 +4570,21 @@
|
||||||
"@types/estree": "^1.0.0"
|
"@types/estree": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@vue/reactivity": {
|
||||||
|
"version": "3.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.1.5.tgz",
|
||||||
|
"integrity": "sha512-1tdfLmNjWG6t/CsPldh+foumYFo3cpyCHgBYQ34ylaMsJ+SNHQ1kApMIa8jN+i593zQuaw3AdWH0nJTARzCFhg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/shared": "3.1.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vue/shared": {
|
||||||
|
"version": "3.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.5.tgz",
|
||||||
|
"integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@webreflection/fetch": {
|
"node_modules/@webreflection/fetch": {
|
||||||
"version": "0.1.5",
|
"version": "0.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@webreflection/fetch/-/fetch-0.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@webreflection/fetch/-/fetch-0.1.5.tgz",
|
||||||
|
|
@ -4672,6 +4688,15 @@
|
||||||
"url": "https://github.com/sponsors/epoberezkin"
|
"url": "https://github.com/sponsors/epoberezkin"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/alpinejs": {
|
||||||
|
"version": "3.15.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.0.tgz",
|
||||||
|
"integrity": "sha512-lpokA5okCF1BKh10LG8YjqhfpxyHBk4gE7boIgVHltJzYoM7O9nK3M7VlntLEJGsVmu7U/RzUWajmHREGT38Eg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/reactivity": "~3.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/amator": {
|
"node_modules/amator": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/amator/-/amator-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/amator/-/amator-1.1.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "open-webui",
|
"name": "open-webui",
|
||||||
"version": "0.6.29",
|
"version": "0.6.30",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "npm run pyodide:fetch && vite dev --host",
|
"dev": "npm run pyodide:fetch && vite dev --host",
|
||||||
|
|
@ -83,6 +83,7 @@
|
||||||
"@tiptap/starter-kit": "^3.0.7",
|
"@tiptap/starter-kit": "^3.0.7",
|
||||||
"@tiptap/suggestion": "^3.4.2",
|
"@tiptap/suggestion": "^3.4.2",
|
||||||
"@xyflow/svelte": "^0.1.19",
|
"@xyflow/svelte": "^0.1.19",
|
||||||
|
"alpinejs": "^3.15.0",
|
||||||
"async": "^3.2.5",
|
"async": "^3.2.5",
|
||||||
"bits-ui": "^0.21.15",
|
"bits-ui": "^0.21.15",
|
||||||
"chart.js": "^4.5.0",
|
"chart.js": "^4.5.0",
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ dependencies = [
|
||||||
"uvicorn[standard]==0.35.0",
|
"uvicorn[standard]==0.35.0",
|
||||||
"pydantic==2.11.7",
|
"pydantic==2.11.7",
|
||||||
"python-multipart==0.0.20",
|
"python-multipart==0.0.20",
|
||||||
|
"itsdangerous==2.2.0",
|
||||||
|
|
||||||
"python-socketio==5.13.0",
|
"python-socketio==5.13.0",
|
||||||
"python-jose==3.4.0",
|
"python-jose==3.4.0",
|
||||||
|
|
|
||||||
20
src/app.css
20
src/app.css
|
|
@ -70,23 +70,23 @@ textarea::placeholder {
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-prose {
|
.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 {
|
.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 {
|
.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 {
|
.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 {
|
.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 {
|
.markdown a {
|
||||||
|
|
@ -116,7 +116,7 @@ li p {
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
--tw-border-opacity: 1;
|
--tw-border-opacity: 1;
|
||||||
background-color: rgba(215, 215, 215, 0.8);
|
background-color: rgba(215, 215, 215, 0.6);
|
||||||
border-color: rgba(255, 255, 255, var(--tw-border-opacity));
|
border-color: rgba(255, 255, 255, var(--tw-border-opacity));
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
|
|
@ -124,12 +124,12 @@ li p {
|
||||||
|
|
||||||
/* Dark theme scrollbar styles */
|
/* Dark theme scrollbar styles */
|
||||||
.dark ::-webkit-scrollbar-thumb {
|
.dark ::-webkit-scrollbar-thumb {
|
||||||
background-color: rgba(67, 67, 67, 0.8); /* Darker color for dark theme */
|
background-color: rgba(67, 67, 67, 0.6); /* Darker color for dark theme */
|
||||||
border-color: rgba(0, 0, 0, var(--tw-border-opacity));
|
border-color: rgba(0, 0, 0, var(--tw-border-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
height: 0.6rem;
|
height: 0.4rem;
|
||||||
width: 0.4rem;
|
width: 0.4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -413,7 +413,7 @@ input[type='number'] {
|
||||||
border-radius: 0.4rem;
|
border-radius: 0.4rem;
|
||||||
box-decoration-break: clone;
|
box-decoration-break: clone;
|
||||||
padding: 0.1rem 0.3rem;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mention::after {
|
.mention::after {
|
||||||
|
|
@ -424,7 +424,7 @@ input[type='number'] {
|
||||||
border-radius: 0.4rem;
|
border-radius: 0.4rem;
|
||||||
box-decoration-break: clone;
|
box-decoration-break: clone;
|
||||||
padding: 0.1rem 0.3rem;
|
padding: 0.1rem 0.3rem;
|
||||||
@apply bg-purple-100/20 text-purple-900 dark:bg-purple-500/20 dark:text-purple-100;
|
@apply text-sky-800 dark:text-sky-200 bg-sky-300/15 dark:bg-sky-500/15;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tiptap .suggestion::after {
|
.tiptap .suggestion::after {
|
||||||
|
|
|
||||||
|
|
@ -194,6 +194,34 @@ export const getAllUsers = async (token: string) => {
|
||||||
return res;
|
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) => {
|
export const getUserSettings = async (token: string) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
const res = await fetch(`${WEBUI_API_BASE_URL}/users/user/settings`, {
|
const res = await fetch(`${WEBUI_API_BASE_URL}/users/user/settings`, {
|
||||||
|
|
|
||||||
|
|
@ -122,7 +122,7 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!key) {
|
if (!key && !['azure_ad', 'microsoft_entra_id'].includes(auth_type)) {
|
||||||
loading = false;
|
loading = false;
|
||||||
|
|
||||||
toast.error($i18n.t('Key is required'));
|
toast.error($i18n.t('Key is required'));
|
||||||
|
|
@ -331,6 +331,9 @@
|
||||||
<option value="session">{$i18n.t('Session')}</option>
|
<option value="session">{$i18n.t('Session')}</option>
|
||||||
{#if !direct}
|
{#if !direct}
|
||||||
<option value="system_oauth">{$i18n.t('OAuth')}</option>
|
<option value="system_oauth">{$i18n.t('OAuth')}</option>
|
||||||
|
{#if azure}
|
||||||
|
<option value="microsoft_entra_id">{$i18n.t('Entra ID')}</option>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</select>
|
</select>
|
||||||
|
|
@ -361,6 +364,12 @@
|
||||||
>
|
>
|
||||||
{$i18n.t('Forwards system user OAuth access token to authenticate')}
|
{$i18n.t('Forwards system user OAuth access token to authenticate')}
|
||||||
</div>
|
</div>
|
||||||
|
{:else if ['azure_ad', 'microsoft_entra_id'].includes(auth_type)}
|
||||||
|
<div
|
||||||
|
class={`text-xs self-center translate-y-[1px] ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||||
|
>
|
||||||
|
{$i18n.t('Uses DefaultAzureCredential to authenticate')}
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -443,7 +452,7 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="flex flex-col w-full">
|
<div class="flex flex-col w-full mt-2">
|
||||||
<div class="mb-1 flex justify-between">
|
<div class="mb-1 flex justify-between">
|
||||||
<div
|
<div
|
||||||
class={`mb-0.5 text-xs text-gray-500
|
class={`mb-0.5 text-xs text-gray-500
|
||||||
|
|
@ -499,8 +508,6 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-700/10 my-1.5 w-full" />
|
|
||||||
|
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<label class="sr-only" for="add-model-id-input">{$i18n.t('Add a model ID')}</label>
|
<label class="sr-only" for="add-model-id-input">{$i18n.t('Add a model ID')}</label>
|
||||||
<input
|
<input
|
||||||
|
|
@ -528,9 +535,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class=" border-gray-50 dark:border-gray-850 my-2.5 w-full" />
|
<div class="flex gap-2 mt-2">
|
||||||
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<div class="flex flex-col w-full">
|
<div class="flex flex-col w-full">
|
||||||
<div
|
<div
|
||||||
class={`mb-0.5 text-xs text-gray-500
|
class={`mb-0.5 text-xs text-gray-500
|
||||||
|
|
|
||||||
|
|
@ -19,16 +19,19 @@
|
||||||
|
|
||||||
let changelog = null;
|
let changelog = null;
|
||||||
|
|
||||||
onMount(async () => {
|
const init = async () => {
|
||||||
const res = await getChangelog();
|
changelog = await getChangelog();
|
||||||
changelog = res;
|
};
|
||||||
});
|
|
||||||
|
$: if (show) {
|
||||||
|
init();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal bind:show size="xl">
|
<Modal bind:show size="xl">
|
||||||
<div class="px-5 pt-4 dark:text-gray-300 text-gray-700">
|
<div class="px-6 pt-5 dark:text-white text-black">
|
||||||
<div class="flex justify-between items-start">
|
<div class="flex justify-between items-start">
|
||||||
<div class="text-xl font-semibold">
|
<div class="text-xl font-medium">
|
||||||
{$i18n.t("What's New in")}
|
{$i18n.t("What's New in")}
|
||||||
{$WEBUI_NAME}
|
{$WEBUI_NAME}
|
||||||
<Confetti x={[-1, -0.25]} y={[0, 0.5]} />
|
<Confetti x={[-1, -0.25]} y={[0, 0.5]} />
|
||||||
|
|
@ -48,7 +51,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center mt-1">
|
<div class="flex items-center mt-1">
|
||||||
<div class="text-sm dark:text-gray-200">{$i18n.t('Release Notes')}</div>
|
<div class="text-sm dark:text-gray-200">{$i18n.t('Release Notes')}</div>
|
||||||
<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-200 dark:bg-gray-700" />
|
<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-50/50 dark:bg-gray-850/50" />
|
||||||
<div class="text-sm dark:text-gray-200">
|
<div class="text-sm dark:text-gray-200">
|
||||||
v{WEBUI_VERSION}
|
v{WEBUI_VERSION}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -56,7 +59,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" w-full p-4 px-5 text-gray-700 dark:text-gray-100">
|
<div class=" w-full p-4 px-5 text-gray-700 dark:text-gray-100">
|
||||||
<div class=" overflow-y-scroll max-h-[32rem] scrollbar-hidden">
|
<div class=" overflow-y-scroll max-h-[30rem] scrollbar-hidden">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
{#if changelog}
|
{#if changelog}
|
||||||
{#each Object.keys(changelog) as version}
|
{#each Object.keys(changelog) as version}
|
||||||
|
|
@ -65,20 +68,20 @@
|
||||||
v{version} - {changelog[version].date}
|
v{version} - {changelog[version].date}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class="border-gray-100 dark:border-gray-850 my-2" />
|
<hr class="border-gray-50/50 dark:border-gray-850/50 my-2" />
|
||||||
|
|
||||||
{#each Object.keys(changelog[version]).filter((section) => section !== 'date') as section}
|
{#each Object.keys(changelog[version]).filter((section) => section !== 'date') as section}
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<div
|
<div
|
||||||
class="font-semibold uppercase text-xs {section === 'added'
|
class="font-semibold uppercase text-xs {section === 'added'
|
||||||
? 'text-white bg-blue-600'
|
? 'bg-blue-500/20 text-blue-700 dark:text-blue-200'
|
||||||
: section === 'fixed'
|
: section === 'fixed'
|
||||||
? 'text-white bg-green-600'
|
? 'bg-green-500/20 text-green-700 dark:text-green-200'
|
||||||
: section === 'changed'
|
: section === 'changed'
|
||||||
? 'text-white bg-yellow-600'
|
? 'bg-yellow-500/20 text-yellow-700 dark:text-yellow-200'
|
||||||
: section === 'removed'
|
: section === 'removed'
|
||||||
? 'text-white bg-red-600'
|
? 'bg-red-500/20 text-red-700 dark:text-red-200'
|
||||||
: ''} w-fit px-3 rounded-full my-2.5"
|
: ''} w-fit rounded-xl px-2 my-2.5"
|
||||||
>
|
>
|
||||||
{section}
|
{section}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,43 @@
|
||||||
export let title: string = 'HI';
|
export let title: string = 'HI';
|
||||||
export let content: string;
|
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(() => {
|
onMount(() => {
|
||||||
if (!navigator.userActivation.hasBeenActive) {
|
if (!navigator.userActivation.hasBeenActive) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -31,24 +68,33 @@
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<button
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
class="flex gap-2.5 text-left min-w-[var(--width)] w-full dark:bg-gray-850 dark:text-white bg-white text-black border border-gray-100 dark:border-gray-850 rounded-xl px-3.5 py-3.5"
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
on:click={() => {
|
<div
|
||||||
onClick();
|
class="flex gap-2.5 text-left min-w-[var(--width)] w-full dark:bg-gray-850 dark:text-white bg-white text-black border border-gray-100 dark:border-gray-800 rounded-3xl px-4 py-3.5 cursor-pointer select-none"
|
||||||
dispatch('closeToast');
|
on:dragstart|preventDefault
|
||||||
|
on:pointerdown={onPointerDown}
|
||||||
|
on:pointermove={onPointerMove}
|
||||||
|
on:pointerup={onPointerUp}
|
||||||
|
on:pointercancel={() => (moved = true)}
|
||||||
|
on:keydown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
clickHandler();
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="shrink-0 self-top -translate-y-0.5">
|
<div class="shrink-0 self-top -translate-y-0.5">
|
||||||
<img src="{WEBUI_BASE_URL}/static/favicon.png" alt="favicon" class="size-7 rounded-full" />
|
<img src="{WEBUI_BASE_URL}/static/favicon.png" alt="favicon" class="size-6 rounded-full" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{#if title}
|
{#if title}
|
||||||
<div class=" text-[13px] font-medium mb-0.5 line-clamp-1 capitalize">{title}</div>
|
<div class=" text-[13px] font-medium mb-0.5 line-clamp-1">{title}</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class=" line-clamp-2 text-xs self-center dark:text-gray-300 font-normal">
|
<div class=" line-clamp-2 text-xs self-center dark:text-gray-300 font-normal">
|
||||||
{@html DOMPurify.sanitize(marked(content))}
|
{@html DOMPurify.sanitize(marked(content))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@
|
||||||
<div class="flex flex-col lg:flex-row w-full h-full pb-2 lg:space-x-4">
|
<div class="flex flex-col lg:flex-row w-full h-full pb-2 lg:space-x-4">
|
||||||
<div
|
<div
|
||||||
id="users-tabs-container"
|
id="users-tabs-container"
|
||||||
class="tabs flex flex-row overflow-x-auto gap-2.5 max-w-full lg:gap-1 lg:flex-col lg:flex-none lg:w-40 dark:text-gray-200 text-sm font-medium text-left scrollbar-none"
|
class="tabs mx-[16px] lg:mx-0 lg:px-[16px] flex flex-row overflow-x-auto gap-2.5 max-w-full lg:gap-1 lg:flex-col lg:flex-none lg:w-50 dark:text-gray-200 text-sm font-medium text-left scrollbar-none"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
id="leaderboard"
|
id="leaderboard"
|
||||||
|
|
@ -113,7 +113,7 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-1 mt-1 lg:mt-0 overflow-y-scroll">
|
<div class="flex-1 mt-1 lg:mt-0 px-[16px] lg:pr-[16px] lg:pl-0 overflow-y-scroll">
|
||||||
{#if selectedTab === 'leaderboard'}
|
{#if selectedTab === 'leaderboard'}
|
||||||
<Leaderboard {feedbacks} />
|
<Leaderboard {feedbacks} />
|
||||||
{:else if selectedTab === 'feedbacks'}
|
{:else if selectedTab === 'feedbacks'}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
import GarbageBin from '$lib/components/icons/GarbageBin.svelte';
|
import GarbageBin from '$lib/components/icons/GarbageBin.svelte';
|
||||||
import Pencil from '$lib/components/icons/Pencil.svelte';
|
import Pencil from '$lib/components/icons/Pencil.svelte';
|
||||||
import Tooltip from '$lib/components/common/Tooltip.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;
|
let show = false;
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -25,7 +25,7 @@
|
||||||
|
|
||||||
<div slot="content">
|
<div slot="content">
|
||||||
<DropdownMenu.Content
|
<DropdownMenu.Content
|
||||||
class="w-full max-w-[150px] rounded-xl px-1 py-1.5 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg"
|
class="w-full max-w-[150px] rounded-xl p-1 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg"
|
||||||
sideOffset={-2}
|
sideOffset={-2}
|
||||||
side="bottom"
|
side="bottom"
|
||||||
align="start"
|
align="start"
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
import { deleteFeedbackById, exportAllFeedbacks, getAllFeedbacks } from '$lib/apis/evaluations';
|
import { deleteFeedbackById, exportAllFeedbacks, getAllFeedbacks } from '$lib/apis/evaluations';
|
||||||
|
|
||||||
import Tooltip from '$lib/components/common/Tooltip.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 Badge from '$lib/components/common/Badge.svelte';
|
import Badge from '$lib/components/common/Badge.svelte';
|
||||||
import CloudArrowUp from '$lib/components/icons/CloudArrowUp.svelte';
|
import CloudArrowUp from '$lib/components/icons/CloudArrowUp.svelte';
|
||||||
import Pagination from '$lib/components/common/Pagination.svelte';
|
import Pagination from '$lib/components/common/Pagination.svelte';
|
||||||
|
|
@ -169,7 +169,7 @@
|
||||||
|
|
||||||
<FeedbackModal bind:show={showFeedbackModal} {selectedFeedback} onClose={closeFeedbackModal} />
|
<FeedbackModal bind:show={showFeedbackModal} {selectedFeedback} onClose={closeFeedbackModal} />
|
||||||
|
|
||||||
<div class="mt-0.5 mb-2 gap-1 flex flex-row justify-between">
|
<div class="mt-0.5 mb-1 gap-1 flex flex-row justify-between">
|
||||||
<div class="flex md:self-center text-lg font-medium px-0.5">
|
<div class="flex md:self-center text-lg font-medium px-0.5">
|
||||||
{$i18n.t('Feedback History')}
|
{$i18n.t('Feedback History')}
|
||||||
|
|
||||||
|
|
@ -187,31 +187,25 @@
|
||||||
exportHandler();
|
exportHandler();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ArrowDownTray className="size-3" />
|
<Download className="size-3" />
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full">
|
||||||
class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full rounded-sm pt-0.5"
|
|
||||||
>
|
|
||||||
{#if (feedbacks ?? []).length === 0}
|
{#if (feedbacks ?? []).length === 0}
|
||||||
<div class="text-center text-xs text-gray-500 dark:text-gray-400 py-1">
|
<div class="text-center text-xs text-gray-500 dark:text-gray-400 py-1">
|
||||||
{$i18n.t('No feedbacks found')}
|
{$i18n.t('No feedbacks found')}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<table
|
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400 table-auto max-w-full">
|
||||||
class="w-full text-sm text-left text-gray-500 dark:text-gray-400 table-auto max-w-full rounded-sm"
|
<thead class="text-xs text-gray-800 uppercase bg-transparent dark:text-gray-200">
|
||||||
>
|
<tr class=" border-b-2 border-gray-100 dark:border-gray-800">
|
||||||
<thead
|
|
||||||
class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-850 dark:text-gray-400 -translate-y-0.5"
|
|
||||||
>
|
|
||||||
<tr class="">
|
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
class="px-3 py-1.5 cursor-pointer select-none w-3"
|
class="px-2.5 py-2 cursor-pointer select-none w-3"
|
||||||
on:click={() => setSortKey('user')}
|
on:click={() => setSortKey('user')}
|
||||||
>
|
>
|
||||||
<div class="flex gap-1.5 items-center justify-end">
|
<div class="flex gap-1.5 items-center justify-end">
|
||||||
|
|
@ -234,7 +228,7 @@
|
||||||
|
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
class="px-3 pr-1.5 cursor-pointer select-none"
|
class="px-2.5 py-2 cursor-pointer select-none"
|
||||||
on:click={() => setSortKey('model_id')}
|
on:click={() => setSortKey('model_id')}
|
||||||
>
|
>
|
||||||
<div class="flex gap-1.5 items-center">
|
<div class="flex gap-1.5 items-center">
|
||||||
|
|
@ -257,7 +251,7 @@
|
||||||
|
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
class="px-3 py-1.5 text-right cursor-pointer select-none w-fit"
|
class="px-2.5 py-2 text-right cursor-pointer select-none w-fit"
|
||||||
on:click={() => setSortKey('rating')}
|
on:click={() => setSortKey('rating')}
|
||||||
>
|
>
|
||||||
<div class="flex gap-1.5 items-center justify-end">
|
<div class="flex gap-1.5 items-center justify-end">
|
||||||
|
|
@ -280,7 +274,7 @@
|
||||||
|
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
class="px-3 py-1.5 text-right cursor-pointer select-none w-0"
|
class="px-2.5 py-2 text-right cursor-pointer select-none w-0"
|
||||||
on:click={() => setSortKey('updated_at')}
|
on:click={() => setSortKey('updated_at')}
|
||||||
>
|
>
|
||||||
<div class="flex gap-1.5 items-center justify-end">
|
<div class="flex gap-1.5 items-center justify-end">
|
||||||
|
|
@ -301,7 +295,7 @@
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
|
|
||||||
<th scope="col" class="px-3 py-1.5 text-right cursor-pointer select-none w-0"> </th>
|
<th scope="col" class="px-2.5 py-2 text-right cursor-pointer select-none w-0"> </th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="">
|
<tbody class="">
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,4 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as ort from 'onnxruntime-web';
|
|
||||||
import { env, AutoModel, AutoTokenizer } from '@huggingface/transformers';
|
|
||||||
|
|
||||||
env.backends.onnx.wasm.wasmPaths = '/wasm/';
|
|
||||||
|
|
||||||
import { onMount, getContext } from 'svelte';
|
import { onMount, getContext } from 'svelte';
|
||||||
import { models } from '$lib/stores';
|
import { models } from '$lib/stores';
|
||||||
|
|
||||||
|
|
@ -237,6 +232,11 @@
|
||||||
//////////////////////
|
//////////////////////
|
||||||
|
|
||||||
const loadEmbeddingModel = async () => {
|
const loadEmbeddingModel = async () => {
|
||||||
|
const { env, AutoModel, AutoTokenizer } = await import('@huggingface/transformers');
|
||||||
|
if (env.backends.onnx.wasm) {
|
||||||
|
env.backends.onnx.wasm.wasmPaths = '/wasm/';
|
||||||
|
}
|
||||||
|
|
||||||
// Check if the tokenizer and model are already loaded and stored in the window object
|
// Check if the tokenizer and model are already loaded and stored in the window object
|
||||||
if (!window.tokenizer) {
|
if (!window.tokenizer) {
|
||||||
window.tokenizer = await AutoTokenizer.from_pretrained(EMBEDDING_MODEL);
|
window.tokenizer = await AutoTokenizer.from_pretrained(EMBEDDING_MODEL);
|
||||||
|
|
@ -337,7 +337,7 @@
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="pt-0.5 pb-2 gap-1 flex flex-col md:flex-row justify-between sticky top-0 z-10 bg-white dark:bg-gray-900"
|
class="pt-0.5 pb-1 gap-1 flex flex-col md:flex-row justify-between sticky top-0 z-10 bg-white dark:bg-gray-900"
|
||||||
>
|
>
|
||||||
<div class="flex md:self-center text-lg font-medium px-0.5 shrink-0 items-center">
|
<div class="flex md:self-center text-lg font-medium px-0.5 shrink-0 items-center">
|
||||||
<div class=" gap-1">
|
<div class=" gap-1">
|
||||||
|
|
@ -370,9 +370,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full rounded-sm">
|
||||||
class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full rounded-sm pt-0.5"
|
|
||||||
>
|
|
||||||
{#if loadingLeaderboard}
|
{#if loadingLeaderboard}
|
||||||
<div class=" absolute top-0 bottom-0 left-0 right-0 flex">
|
<div class=" absolute top-0 bottom-0 left-0 right-0 flex">
|
||||||
<div class="m-auto">
|
<div class="m-auto">
|
||||||
|
|
@ -386,17 +384,15 @@
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<table
|
<table
|
||||||
class="w-full text-sm text-left text-gray-500 dark:text-gray-400 table-auto max-w-full rounded {loadingLeaderboard
|
class="w-full text-sm text-left text-gray-500 dark:text-gray-400 table-auto max-w-full {loadingLeaderboard
|
||||||
? 'opacity-20'
|
? 'opacity-20'
|
||||||
: ''}"
|
: ''}"
|
||||||
>
|
>
|
||||||
<thead
|
<thead class="text-xs text-gray-800 uppercase bg-transparent dark:text-gray-200">
|
||||||
class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-850 dark:text-gray-400 -translate-y-0.5"
|
<tr class=" border-b-2 border-gray-100 dark:border-gray-800">
|
||||||
>
|
|
||||||
<tr class="">
|
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
class="px-3 py-1.5 cursor-pointer select-none w-3"
|
class="px-2.5 py-2 cursor-pointer select-none w-3"
|
||||||
on:click={() => setSortKey('rating')}
|
on:click={() => setSortKey('rating')}
|
||||||
>
|
>
|
||||||
<div class="flex gap-1.5 items-center">
|
<div class="flex gap-1.5 items-center">
|
||||||
|
|
@ -418,7 +414,7 @@
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
class="px-3 py-1.5 cursor-pointer select-none"
|
class="px-2.5 py-2 cursor-pointer select-none"
|
||||||
on:click={() => setSortKey('name')}
|
on:click={() => setSortKey('name')}
|
||||||
>
|
>
|
||||||
<div class="flex gap-1.5 items-center">
|
<div class="flex gap-1.5 items-center">
|
||||||
|
|
@ -440,7 +436,7 @@
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
class="px-3 py-1.5 text-right cursor-pointer select-none w-fit"
|
class="px-2.5 py-2 text-right cursor-pointer select-none w-fit"
|
||||||
on:click={() => setSortKey('rating')}
|
on:click={() => setSortKey('rating')}
|
||||||
>
|
>
|
||||||
<div class="flex gap-1.5 items-center justify-end">
|
<div class="flex gap-1.5 items-center justify-end">
|
||||||
|
|
@ -462,7 +458,7 @@
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
class="px-3 py-1.5 text-right cursor-pointer select-none w-5"
|
class="px-2.5 py-2 text-right cursor-pointer select-none w-5"
|
||||||
on:click={() => setSortKey('won')}
|
on:click={() => setSortKey('won')}
|
||||||
>
|
>
|
||||||
<div class="flex gap-1.5 items-center justify-end">
|
<div class="flex gap-1.5 items-center justify-end">
|
||||||
|
|
@ -484,7 +480,7 @@
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
class="px-3 py-1.5 text-right cursor-pointer select-none w-5"
|
class="px-2.5 py-2 text-right cursor-pointer select-none w-5"
|
||||||
on:click={() => setSortKey('lost')}
|
on:click={() => setSortKey('lost')}
|
||||||
>
|
>
|
||||||
<div class="flex gap-1.5 items-center justify-end">
|
<div class="flex gap-1.5 items-center justify-end">
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
toggleGlobalById
|
toggleGlobalById
|
||||||
} from '$lib/apis/functions';
|
} from '$lib/apis/functions';
|
||||||
|
|
||||||
import ArrowDownTray from '../icons/ArrowDownTray.svelte';
|
import Download from '../icons/Download.svelte';
|
||||||
import Tooltip from '../common/Tooltip.svelte';
|
import Tooltip from '../common/Tooltip.svelte';
|
||||||
import ConfirmDialog from '../common/ConfirmDialog.svelte';
|
import ConfirmDialog from '../common/ConfirmDialog.svelte';
|
||||||
import { getModels } from '$lib/apis';
|
import { getModels } from '$lib/apis';
|
||||||
|
|
@ -222,7 +222,7 @@
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="flex flex-col mt-1.5 mb-0.5">
|
<div class="flex flex-col mt-1.5 mb-0.5 px-[16px]">
|
||||||
<div class="flex justify-between items-center mb-1">
|
<div class="flex justify-between items-center mb-1">
|
||||||
<div class="flex md:self-center text-xl items-center font-medium px-0.5">
|
<div class="flex md:self-center text-xl items-center font-medium px-0.5">
|
||||||
{$i18n.t('Functions')}
|
{$i18n.t('Functions')}
|
||||||
|
|
@ -317,7 +317,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-5">
|
<div class="mb-5 px-[16px]">
|
||||||
{#each filteredItems as func (func.id)}
|
{#each filteredItems as func (func.id)}
|
||||||
<div
|
<div
|
||||||
class=" flex space-x-4 cursor-pointer w-full px-2 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl"
|
class=" flex space-x-4 cursor-pointer w-full px-2 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl"
|
||||||
|
|
@ -330,14 +330,14 @@
|
||||||
<div class=" flex-1 self-center pl-1">
|
<div class=" flex-1 self-center pl-1">
|
||||||
<div class=" font-semibold flex items-center gap-1.5">
|
<div class=" font-semibold flex items-center gap-1.5">
|
||||||
<div
|
<div
|
||||||
class=" text-xs font-bold px-1 rounded-sm uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
|
class=" text-xs font-semibold px-1 rounded-sm uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
|
||||||
>
|
>
|
||||||
{func.type}
|
{func.type}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if func?.meta?.manifest?.version}
|
{#if func?.meta?.manifest?.version}
|
||||||
<div
|
<div
|
||||||
class="text-xs font-bold px-1 rounded-sm line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
|
class="text-xs font-semibold px-1 rounded-sm line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
|
||||||
>
|
>
|
||||||
v{func?.meta?.manifest?.version ?? ''}
|
v{func?.meta?.manifest?.version ?? ''}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -482,7 +482,7 @@
|
||||||
)}
|
)}
|
||||||
</div> -->
|
</div> -->
|
||||||
|
|
||||||
<div class=" flex justify-end w-full mb-2">
|
<div class=" flex justify-end w-full mb-2 px-[16px]">
|
||||||
<div class="flex space-x-2">
|
<div class="flex space-x-2">
|
||||||
<input
|
<input
|
||||||
id="documents-import-input"
|
id="documents-import-input"
|
||||||
|
|
@ -562,7 +562,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if $config?.features.enable_community_sharing}
|
{#if $config?.features.enable_community_sharing}
|
||||||
<div class=" my-16">
|
<div class=" my-16 px-[16px]">
|
||||||
<div class=" text-xl font-medium mb-1 line-clamp-1">
|
<div class=" text-xl font-medium mb-1 line-clamp-1">
|
||||||
{$i18n.t('Made by Open WebUI Community')}
|
{$i18n.t('Made by Open WebUI Community')}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
import Share from '$lib/components/icons/Share.svelte';
|
import Share from '$lib/components/icons/Share.svelte';
|
||||||
import DocumentDuplicate from '$lib/components/icons/DocumentDuplicate.svelte';
|
import DocumentDuplicate from '$lib/components/icons/DocumentDuplicate.svelte';
|
||||||
import ArrowDownTray from '$lib/components/icons/ArrowDownTray.svelte';
|
import Download from '$lib/components/icons/Download.svelte';
|
||||||
import Switch from '$lib/components/common/Switch.svelte';
|
import Switch from '$lib/components/common/Switch.svelte';
|
||||||
import GlobeAlt from '$lib/components/icons/GlobeAlt.svelte';
|
import GlobeAlt from '$lib/components/icons/GlobeAlt.svelte';
|
||||||
import Github from '$lib/components/icons/Github.svelte';
|
import Github from '$lib/components/icons/Github.svelte';
|
||||||
|
|
@ -41,7 +41,7 @@
|
||||||
|
|
||||||
<div slot="content">
|
<div slot="content">
|
||||||
<DropdownMenu.Content
|
<DropdownMenu.Content
|
||||||
class="w-full max-w-[190px] text-sm rounded-xl px-1 py-1.5 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg font-primary"
|
class="w-full max-w-[190px] text-sm rounded-xl p-1 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg font-primary"
|
||||||
sideOffset={-2}
|
sideOffset={-2}
|
||||||
side="bottom"
|
side="bottom"
|
||||||
align="start"
|
align="start"
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
import CodeEditor from '$lib/components/common/CodeEditor.svelte';
|
|
||||||
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||||
import Badge from '$lib/components/common/Badge.svelte';
|
import Badge from '$lib/components/common/Badge.svelte';
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
|
|
@ -367,6 +366,7 @@ class Pipe:
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-2 flex-1 overflow-auto h-0 rounded-lg">
|
<div class="mb-2 flex-1 overflow-auto h-0 rounded-lg">
|
||||||
|
{#await import('$lib/components/common/CodeEditor.svelte') then { default: CodeEditor }}
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
bind:this={codeEditor}
|
bind:this={codeEditor}
|
||||||
value={content}
|
value={content}
|
||||||
|
|
@ -381,6 +381,7 @@ class Pipe:
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{/await}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pb-3 flex justify-between">
|
<div class="pb-3 flex justify-between">
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
import Share from '$lib/components/icons/Share.svelte';
|
import Share from '$lib/components/icons/Share.svelte';
|
||||||
import DocumentDuplicate from '$lib/components/icons/DocumentDuplicate.svelte';
|
import DocumentDuplicate from '$lib/components/icons/DocumentDuplicate.svelte';
|
||||||
import ArrowDownTray from '$lib/components/icons/ArrowDownTray.svelte';
|
import Download from '$lib/components/icons/Download.svelte';
|
||||||
import Switch from '$lib/components/common/Switch.svelte';
|
import Switch from '$lib/components/common/Switch.svelte';
|
||||||
import GlobeAlt from '$lib/components/icons/GlobeAlt.svelte';
|
import GlobeAlt from '$lib/components/icons/GlobeAlt.svelte';
|
||||||
|
|
||||||
|
|
@ -42,7 +42,7 @@
|
||||||
|
|
||||||
<div slot="content">
|
<div slot="content">
|
||||||
<DropdownMenu.Content
|
<DropdownMenu.Content
|
||||||
class="w-full max-w-[180px] rounded-xl px-1 py-1.5 border border-gray-100 dark:border-gray-800 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-sm"
|
class="w-full max-w-[180px] rounded-xl p-1 border border-gray-100 dark:border-gray-800 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-sm"
|
||||||
sideOffset={-2}
|
sideOffset={-2}
|
||||||
side="bottom"
|
side="bottom"
|
||||||
align="start"
|
align="start"
|
||||||
|
|
@ -63,7 +63,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class="border-gray-100 dark:border-gray-850 my-1" />
|
<hr class="border-gray-50 dark:border-gray-850 my-1" />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
|
|
@ -117,12 +117,12 @@
|
||||||
exportHandler();
|
exportHandler();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ArrowDownTray />
|
<Download />
|
||||||
|
|
||||||
<div class="flex items-center">{$i18n.t('Export')}</div>
|
<div class="flex items-center">{$i18n.t('Export')}</div>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
|
|
||||||
<hr class="border-gray-100 dark:border-gray-850 my-1" />
|
<hr class="border-gray-50 dark:border-gray-850 my-1" />
|
||||||
|
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
class="flex gap-2 items-center px-3 py-1.5 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
class="flex gap-2 items-center px-3 py-1.5 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@
|
||||||
<div class="flex flex-col lg:flex-row w-full h-full pb-2 lg:space-x-4">
|
<div class="flex flex-col lg:flex-row w-full h-full pb-2 lg:space-x-4">
|
||||||
<div
|
<div
|
||||||
id="admin-settings-tabs-container"
|
id="admin-settings-tabs-container"
|
||||||
class="tabs flex flex-row overflow-x-auto gap-2.5 max-w-full lg:gap-1 lg:flex-col lg:flex-none lg:w-40 dark:text-gray-200 text-sm font-medium text-left scrollbar-none"
|
class="tabs mx-[16px] lg:mx-0 lg:px-[16px] flex flex-row overflow-x-auto gap-2.5 max-w-full lg:gap-1 lg:flex-col lg:flex-none lg:w-50 dark:text-gray-200 text-sm font-medium text-left scrollbar-none"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
id="general"
|
id="general"
|
||||||
|
|
@ -433,7 +433,9 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-1 mt-3 lg:mt-0 overflow-y-scroll pr-1 scrollbar-hidden">
|
<div
|
||||||
|
class="flex-1 mt-3 lg:mt-0 px-[16px] lg:pr-[16px] lg:pl-0 overflow-y-scroll scrollbar-hidden"
|
||||||
|
>
|
||||||
{#if selectedTab === 'general'}
|
{#if selectedTab === 'general'}
|
||||||
<General
|
<General
|
||||||
saveHandler={async () => {
|
saveHandler={async () => {
|
||||||
|
|
|
||||||
|
|
@ -261,7 +261,7 @@
|
||||||
<div class="flex flex-col gap-1.5 mt-1.5">
|
<div class="flex flex-col gap-1.5 mt-1.5">
|
||||||
{#each OPENAI_API_BASE_URLS as url, idx}
|
{#each OPENAI_API_BASE_URLS as url, idx}
|
||||||
<OpenAIConnection
|
<OpenAIConnection
|
||||||
{url}
|
bind:url={OPENAI_API_BASE_URLS[idx]}
|
||||||
bind:key={OPENAI_API_KEYS[idx]}
|
bind:key={OPENAI_API_KEYS[idx]}
|
||||||
bind:config={OPENAI_API_CONFIGS[idx]}
|
bind:config={OPENAI_API_CONFIGS[idx]}
|
||||||
pipeline={pipelineUrls[url] ? true : false}
|
pipeline={pipelineUrls[url] ? true : false}
|
||||||
|
|
@ -326,7 +326,7 @@
|
||||||
<div class="flex-1 flex flex-col gap-1.5 mt-1.5">
|
<div class="flex-1 flex flex-col gap-1.5 mt-1.5">
|
||||||
{#each OLLAMA_BASE_URLS as url, idx}
|
{#each OLLAMA_BASE_URLS as url, idx}
|
||||||
<OllamaConnection
|
<OllamaConnection
|
||||||
{url}
|
bind:url={OLLAMA_BASE_URLS[idx]}
|
||||||
bind:config={OLLAMA_API_CONFIGS[idx]}
|
bind:config={OLLAMA_API_CONFIGS[idx]}
|
||||||
{idx}
|
{idx}
|
||||||
onSubmit={() => {
|
onSubmit={() => {
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
import Cog6 from '$lib/components/icons/Cog6.svelte';
|
import Cog6 from '$lib/components/icons/Cog6.svelte';
|
||||||
import Wrench from '$lib/components/icons/Wrench.svelte';
|
import Wrench from '$lib/components/icons/Wrench.svelte';
|
||||||
import ManageOllamaModal from './ManageOllamaModal.svelte';
|
import ManageOllamaModal from './ManageOllamaModal.svelte';
|
||||||
import ArrowDownTray from '$lib/components/icons/ArrowDownTray.svelte';
|
import Download from '$lib/components/icons/Download.svelte';
|
||||||
|
|
||||||
export let onDelete = () => {};
|
export let onDelete = () => {};
|
||||||
export let onSubmit = () => {};
|
export let onSubmit = () => {};
|
||||||
|
|
@ -84,7 +84,7 @@
|
||||||
}}
|
}}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<ArrowDownTray />
|
<Download />
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -143,7 +143,7 @@
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<hr class="border-gray-100 dark:border-gray-850 my-1" />
|
<hr class="border-gray-50 dark:border-gray-850 my-1" />
|
||||||
|
|
||||||
{#if $config?.features.enable_admin_export ?? true}
|
{#if $config?.features.enable_admin_export ?? true}
|
||||||
<div class=" flex w-full justify-between">
|
<div class=" flex w-full justify-between">
|
||||||
|
|
|
||||||
|
|
@ -1143,7 +1143,7 @@
|
||||||
<div class=" mb-2.5 py-0.5 w-full justify-between">
|
<div class=" mb-2.5 py-0.5 w-full justify-between">
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={$i18n.t(
|
content={$i18n.t(
|
||||||
'The Weight of BM25 Hybrid Search. 0 more lexical, 1 more semantic. Default 0.5'
|
'The Weight of BM25 Hybrid Search. 0 more semantic, 1 more lexical. Default 0.5'
|
||||||
)}
|
)}
|
||||||
placement="top-start"
|
placement="top-start"
|
||||||
className="inline-tooltip"
|
className="inline-tooltip"
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
import Cog6 from '$lib/components/icons/Cog6.svelte';
|
import Cog6 from '$lib/components/icons/Cog6.svelte';
|
||||||
import ConfigureModelsModal from './Models/ConfigureModelsModal.svelte';
|
import ConfigureModelsModal from './Models/ConfigureModelsModal.svelte';
|
||||||
import Wrench from '$lib/components/icons/Wrench.svelte';
|
import Wrench from '$lib/components/icons/Wrench.svelte';
|
||||||
import ArrowDownTray from '$lib/components/icons/ArrowDownTray.svelte';
|
import Download from '$lib/components/icons/Download.svelte';
|
||||||
import ManageModelsModal from './Models/ManageModelsModal.svelte';
|
import ManageModelsModal from './Models/ManageModelsModal.svelte';
|
||||||
import ModelMenu from '$lib/components/admin/Settings/Models/ModelMenu.svelte';
|
import ModelMenu from '$lib/components/admin/Settings/Models/ModelMenu.svelte';
|
||||||
import EllipsisHorizontal from '$lib/components/icons/EllipsisHorizontal.svelte';
|
import EllipsisHorizontal from '$lib/components/icons/EllipsisHorizontal.svelte';
|
||||||
|
|
@ -265,7 +265,7 @@
|
||||||
showManageModal = true;
|
showManageModal = true;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ArrowDownTray />
|
<Download />
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
import Share from '$lib/components/icons/Share.svelte';
|
import Share from '$lib/components/icons/Share.svelte';
|
||||||
import ArchiveBox from '$lib/components/icons/ArchiveBox.svelte';
|
import ArchiveBox from '$lib/components/icons/ArchiveBox.svelte';
|
||||||
import DocumentDuplicate from '$lib/components/icons/DocumentDuplicate.svelte';
|
import DocumentDuplicate from '$lib/components/icons/DocumentDuplicate.svelte';
|
||||||
import ArrowDownTray from '$lib/components/icons/ArrowDownTray.svelte';
|
import Download from '$lib/components/icons/Download.svelte';
|
||||||
import ArrowUpCircle from '$lib/components/icons/ArrowUpCircle.svelte';
|
import ArrowUpCircle from '$lib/components/icons/ArrowUpCircle.svelte';
|
||||||
|
|
||||||
import { config } from '$lib/stores';
|
import { config } from '$lib/stores';
|
||||||
|
|
@ -45,7 +45,7 @@
|
||||||
|
|
||||||
<div slot="content">
|
<div slot="content">
|
||||||
<DropdownMenu.Content
|
<DropdownMenu.Content
|
||||||
class="w-full max-w-[170px] rounded-xl px-1 py-1.5 border border-gray-100 dark:border-gray-800 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-sm"
|
class="w-full max-w-[170px] rounded-xl p-1 border border-gray-100 dark:border-gray-800 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-sm"
|
||||||
sideOffset={-2}
|
sideOffset={-2}
|
||||||
side="bottom"
|
side="bottom"
|
||||||
align="start"
|
align="start"
|
||||||
|
|
@ -120,7 +120,7 @@
|
||||||
exportHandler();
|
exportHandler();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ArrowDownTray />
|
<Download />
|
||||||
|
|
||||||
<div class="flex items-center">{$i18n.t('Export')}</div>
|
<div class="flex items-center">{$i18n.t('Export')}</div>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@
|
||||||
<div class="flex flex-col lg:flex-row w-full h-full pb-2 lg:space-x-4">
|
<div class="flex flex-col lg:flex-row w-full h-full pb-2 lg:space-x-4">
|
||||||
<div
|
<div
|
||||||
id="users-tabs-container"
|
id="users-tabs-container"
|
||||||
class=" flex flex-row overflow-x-auto gap-2.5 max-w-full lg:gap-1 lg:flex-col lg:flex-none lg:w-40 dark:text-gray-200 text-sm font-medium text-left scrollbar-none"
|
class="mx-[16px] lg:mx-0 lg:px-[16px] flex flex-row overflow-x-auto gap-2.5 max-w-full lg:gap-1 lg:flex-col lg:flex-none lg:w-50 dark:text-gray-200 text-sm font-medium text-left scrollbar-none"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
id="overview"
|
id="overview"
|
||||||
|
|
@ -111,7 +111,7 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-1 mt-1 lg:mt-0 overflow-y-scroll">
|
<div class="flex-1 mt-1 lg:mt-0 px-[16px] lg:pr-[16px] lg:pl-0 overflow-y-scroll">
|
||||||
{#if selectedTab === 'overview'}
|
{#if selectedTab === 'overview'}
|
||||||
<UserList />
|
<UserList />
|
||||||
{:else if selectedTab === 'groups'}
|
{:else if selectedTab === 'groups'}
|
||||||
|
|
|
||||||
|
|
@ -216,7 +216,7 @@
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div>
|
<div>
|
||||||
<div class=" flex items-center gap-3 justify-between text-xs uppercase px-1 font-bold">
|
<div class=" flex items-center gap-3 justify-between text-xs uppercase px-1 font-semibold">
|
||||||
<div class="w-full basis-3/5">{$i18n.t('Group')}</div>
|
<div class="w-full basis-3/5">{$i18n.t('Group')}</div>
|
||||||
|
|
||||||
<div class="w-full basis-2/5 text-right">{$i18n.t('Users')}</div>
|
<div class="w-full basis-2/5 text-right">{$i18n.t('Users')}</div>
|
||||||
|
|
|
||||||
|
|
@ -154,7 +154,7 @@
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div
|
<div
|
||||||
class="pt-0.5 pb-2 gap-1 flex flex-col md:flex-row justify-between sticky top-0 z-10 bg-white dark:bg-gray-900"
|
class="pt-0.5 pb-1 gap-1 flex flex-col md:flex-row justify-between sticky top-0 z-10 bg-white dark:bg-gray-900"
|
||||||
>
|
>
|
||||||
<div class="flex md:self-center text-lg font-medium px-0.5">
|
<div class="flex md:self-center text-lg font-medium px-0.5">
|
||||||
<div class="flex-shrink-0">
|
<div class="flex-shrink-0">
|
||||||
|
|
@ -219,19 +219,13 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full">
|
||||||
class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full rounded-sm pt-0.5"
|
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400 table-auto max-w-full">
|
||||||
>
|
<thead class="text-xs text-gray-800 uppercase bg-transparent dark:text-gray-200">
|
||||||
<table
|
<tr class=" border-b-[1.5px] border-gray-50 dark:border-gray-850">
|
||||||
class="w-full text-sm text-left text-gray-500 dark:text-gray-400 table-auto max-w-full rounded-sm"
|
|
||||||
>
|
|
||||||
<thead
|
|
||||||
class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-850 dark:text-gray-400 -translate-y-0.5"
|
|
||||||
>
|
|
||||||
<tr class="">
|
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
class="px-3 py-1.5 cursor-pointer select-none"
|
class="px-2.5 py-2 cursor-pointer select-none"
|
||||||
on:click={() => setSortKey('role')}
|
on:click={() => setSortKey('role')}
|
||||||
>
|
>
|
||||||
<div class="flex gap-1.5 items-center">
|
<div class="flex gap-1.5 items-center">
|
||||||
|
|
@ -254,7 +248,7 @@
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
class="px-3 py-1.5 cursor-pointer select-none"
|
class="px-2.5 py-2 cursor-pointer select-none"
|
||||||
on:click={() => setSortKey('name')}
|
on:click={() => setSortKey('name')}
|
||||||
>
|
>
|
||||||
<div class="flex gap-1.5 items-center">
|
<div class="flex gap-1.5 items-center">
|
||||||
|
|
@ -277,7 +271,7 @@
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
class="px-3 py-1.5 cursor-pointer select-none"
|
class="px-2.5 py-2 cursor-pointer select-none"
|
||||||
on:click={() => setSortKey('email')}
|
on:click={() => setSortKey('email')}
|
||||||
>
|
>
|
||||||
<div class="flex gap-1.5 items-center">
|
<div class="flex gap-1.5 items-center">
|
||||||
|
|
@ -301,7 +295,7 @@
|
||||||
|
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
class="px-3 py-1.5 cursor-pointer select-none"
|
class="px-2.5 py-2 cursor-pointer select-none"
|
||||||
on:click={() => setSortKey('last_active_at')}
|
on:click={() => setSortKey('last_active_at')}
|
||||||
>
|
>
|
||||||
<div class="flex gap-1.5 items-center">
|
<div class="flex gap-1.5 items-center">
|
||||||
|
|
@ -324,7 +318,7 @@
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
class="px-3 py-1.5 cursor-pointer select-none"
|
class="px-2.5 py-2 cursor-pointer select-none"
|
||||||
on:click={() => setSortKey('created_at')}
|
on:click={() => setSortKey('created_at')}
|
||||||
>
|
>
|
||||||
<div class="flex gap-1.5 items-center">
|
<div class="flex gap-1.5 items-center">
|
||||||
|
|
@ -347,7 +341,7 @@
|
||||||
|
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
class="px-3 py-1.5 cursor-pointer select-none"
|
class="px-2.5 py-2 cursor-pointer select-none"
|
||||||
on:click={() => setSortKey('oauth_sub')}
|
on:click={() => setSortKey('oauth_sub')}
|
||||||
>
|
>
|
||||||
<div class="flex gap-1.5 items-center">
|
<div class="flex gap-1.5 items-center">
|
||||||
|
|
@ -369,7 +363,7 @@
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
|
|
||||||
<th scope="col" class="px-3 py-2 text-right" />
|
<th scope="col" class="px-2.5 py-2 text-right" />
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="">
|
<tbody class="">
|
||||||
|
|
@ -508,11 +502,11 @@
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> # **Hey there! 👋**
|
> # **Hey there! 👋**
|
||||||
>
|
>
|
||||||
> It looks like you have over 50 users — that usually falls under organizational usage.
|
> It looks like you have over 50 users, that usually falls under organizational usage.
|
||||||
>
|
>
|
||||||
> Open WebUI is proudly open source and completely free, with no hidden limits — and we'd love to keep it that way. 🌱
|
> Open WebUI is completely free to use as-is, with no restrictions or hidden limits, and we'd love to keep it that way. 🌱
|
||||||
>
|
>
|
||||||
> By supporting the project through sponsorship or an enterprise license, you’re not only helping us stay independent, you’re also helping us ship new features faster, improve stability, and grow the project for the long haul. With an *enterprise license*, you also get additional perks like dedicated support, customization options, and more — all at a fraction of what it would cost to build and maintain internally.
|
> By supporting the project through sponsorship or an enterprise license, you’re not only helping us stay independent, you’re also helping us ship new features faster, improve stability, and grow the project for the long haul. With an *enterprise license*, you also get additional perks like dedicated support, customization options, and more, all at a fraction of what it would cost to build and maintain internally.
|
||||||
>
|
>
|
||||||
> Your support helps us stay independent and continue building great tools for everyone. 💛
|
> Your support helps us stay independent and continue building great tools for everyone. 💛
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,10 @@
|
||||||
let searchDebounceTimeout;
|
let searchDebounceTimeout;
|
||||||
|
|
||||||
const searchHandler = async () => {
|
const searchHandler = async () => {
|
||||||
|
if (!show) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (searchDebounceTimeout) {
|
if (searchDebounceTimeout) {
|
||||||
clearTimeout(searchDebounceTimeout);
|
clearTimeout(searchDebounceTimeout);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -250,6 +250,8 @@
|
||||||
<MessageInput
|
<MessageInput
|
||||||
id="root"
|
id="root"
|
||||||
{typingUsers}
|
{typingUsers}
|
||||||
|
userSuggestions={true}
|
||||||
|
channelSuggestions={true}
|
||||||
{onChange}
|
{onChange}
|
||||||
onSubmit={submitHandler}
|
onSubmit={submitHandler}
|
||||||
{scrollToBottom}
|
{scrollToBottom}
|
||||||
|
|
@ -279,11 +281,12 @@
|
||||||
{/if}
|
{/if}
|
||||||
{:else if threadId !== null}
|
{:else if threadId !== null}
|
||||||
<PaneResizer
|
<PaneResizer
|
||||||
class="relative flex w-[3px] items-center justify-center bg-background group bg-gray-50 dark:bg-gray-850"
|
class="relative flex items-center justify-center group border-l border-gray-50 dark:border-gray-850 hover:border-gray-200 dark:hover:border-gray-800 transition z-20"
|
||||||
|
id="controls-resizer"
|
||||||
>
|
>
|
||||||
<div class="z-10 flex h-7 w-5 items-center justify-center rounded-xs">
|
<div
|
||||||
<EllipsisVertical className="size-4 invisible group-hover:visible" />
|
class=" absolute -left-1.5 -right-1.5 -top-0 -bottom-0 z-20 cursor-col-resize bg-transparent"
|
||||||
</div>
|
/>
|
||||||
</PaneResizer>
|
</PaneResizer>
|
||||||
|
|
||||||
<Pane defaultSize={50} minSize={30} class="h-full w-full">
|
<Pane defaultSize={50} minSize={30} class="h-full w-full">
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@
|
||||||
import { getSuggestionRenderer } from '../common/RichTextInput/suggestions';
|
import { getSuggestionRenderer } from '../common/RichTextInput/suggestions';
|
||||||
import CommandSuggestionList from '../chat/MessageInput/CommandSuggestionList.svelte';
|
import CommandSuggestionList from '../chat/MessageInput/CommandSuggestionList.svelte';
|
||||||
import MentionList from './MessageInput/MentionList.svelte';
|
import MentionList from './MessageInput/MentionList.svelte';
|
||||||
|
import Skeleton from '../chat/Messages/Skeleton.svelte';
|
||||||
|
|
||||||
export let placeholder = $i18n.t('Send a Message');
|
export let placeholder = $i18n.t('Send a Message');
|
||||||
|
|
||||||
|
|
@ -55,6 +56,11 @@
|
||||||
export let acceptFiles = true;
|
export let acceptFiles = true;
|
||||||
export let showFormattingToolbar = true;
|
export let showFormattingToolbar = true;
|
||||||
|
|
||||||
|
export let userSuggestions = false;
|
||||||
|
export let channelSuggestions = false;
|
||||||
|
|
||||||
|
export let typingUsersClassName = 'from-white dark:from-gray-900';
|
||||||
|
|
||||||
let loaded = false;
|
let loaded = false;
|
||||||
let draggedOver = false;
|
let draggedOver = false;
|
||||||
|
|
||||||
|
|
@ -223,12 +229,17 @@
|
||||||
const chatInput = document.getElementById('chat-input');
|
const chatInput = document.getElementById('chat-input');
|
||||||
|
|
||||||
if (chatInput) {
|
if (chatInput) {
|
||||||
|
if (text !== '') {
|
||||||
text = await textVariableHandler(text || '');
|
text = await textVariableHandler(text || '');
|
||||||
|
}
|
||||||
|
|
||||||
chatInputElement?.setText(text);
|
chatInputElement?.setText(text);
|
||||||
chatInputElement?.focus();
|
chatInputElement?.focus();
|
||||||
|
|
||||||
|
if (text !== '') {
|
||||||
text = await inputVariableHandler(text);
|
text = await inputVariableHandler(text);
|
||||||
|
}
|
||||||
|
|
||||||
await tick();
|
await tick();
|
||||||
if (cb) await cb(text);
|
if (cb) await cb(text);
|
||||||
}
|
}
|
||||||
|
|
@ -555,9 +566,24 @@
|
||||||
{
|
{
|
||||||
char: '@',
|
char: '@',
|
||||||
render: getSuggestionRenderer(MentionList, {
|
render: getSuggestionRenderer(MentionList, {
|
||||||
i18n
|
i18n,
|
||||||
|
triggerChar: '@',
|
||||||
|
modelSuggestions: true,
|
||||||
|
userSuggestions
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
...(channelSuggestions
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
char: '#',
|
||||||
|
render: getSuggestionRenderer(MentionList, {
|
||||||
|
i18n,
|
||||||
|
triggerChar: '#',
|
||||||
|
channelSuggestions
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: []),
|
||||||
{
|
{
|
||||||
char: '/',
|
char: '/',
|
||||||
render: getSuggestionRenderer(CommandSuggestionList, {
|
render: getSuggestionRenderer(CommandSuggestionList, {
|
||||||
|
|
@ -652,11 +678,7 @@
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="bg-transparent">
|
<div class="bg-transparent">
|
||||||
<div
|
<div class="max-w-full mx-auto inset-x-0 relative">
|
||||||
class="{($settings?.widescreenMode ?? null)
|
|
||||||
? 'max-w-full'
|
|
||||||
: 'max-w-6xl'} mx-auto inset-x-0 relative"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
class="absolute top-0 left-0 right-0 mx-auto inset-x-0 bg-transparent flex justify-center"
|
class="absolute top-0 left-0 right-0 mx-auto inset-x-0 bg-transparent flex justify-center"
|
||||||
>
|
>
|
||||||
|
|
@ -690,20 +712,24 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="relative">
|
|
||||||
<div class=" -mt-5">
|
|
||||||
{#if typingUsers.length > 0}
|
{#if typingUsers.length > 0}
|
||||||
<div class=" text-xs px-4 mb-1">
|
<div
|
||||||
|
class=" -mt-7 pb-2.5 bg-gradient-to-t to-transparent {typingUsersClassName} pointer-events-none select-none"
|
||||||
|
>
|
||||||
|
<div class=" text-xs px-1 mt-1.5 flex items-center gap-1.5">
|
||||||
|
<Skeleton size="xs" />
|
||||||
|
|
||||||
|
<div>
|
||||||
<span class=" font-normal text-black dark:text-white">
|
<span class=" font-normal text-black dark:text-white">
|
||||||
{typingUsers.map((user) => user.name).join(', ')}
|
{typingUsers.map((user) => user.name).join(', ')}
|
||||||
</span>
|
</span>
|
||||||
{$i18n.t('is typing...')}
|
{$i18n.t('is typing...')}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="">
|
<div class="">
|
||||||
{#if recording}
|
{#if recording}
|
||||||
|
|
@ -802,7 +828,7 @@
|
||||||
|
|
||||||
<div class="px-2.5">
|
<div class="px-2.5">
|
||||||
<div
|
<div
|
||||||
class="scrollbar-hidden rtl:text-right ltr:text-left bg-transparent dark:text-gray-100 outline-hidden w-full pt-2.5 pb-[5px] px-1 resize-none h-fit max-h-80 overflow-auto"
|
class="scrollbar-hidden rtl:text-right ltr:text-left bg-transparent dark:text-gray-100 outline-hidden w-full pt-2.5 pb-[5px] px-1 resize-none h-fit max-h-96 overflow-auto"
|
||||||
>
|
>
|
||||||
{#key $settings?.richTextInput}
|
{#key $settings?.richTextInput}
|
||||||
<RichTextInput
|
<RichTextInput
|
||||||
|
|
@ -811,7 +837,7 @@
|
||||||
json={true}
|
json={true}
|
||||||
messageInput={true}
|
messageInput={true}
|
||||||
richText={$settings?.richTextInput ?? true}
|
richText={$settings?.richTextInput ?? true}
|
||||||
{showFormattingToolbar}
|
showFormattingToolbar={$settings?.showFormattingToolbar ?? false}
|
||||||
shiftEnter={!($settings?.ctrlEnterToSend ?? false) &&
|
shiftEnter={!($settings?.ctrlEnterToSend ?? false) &&
|
||||||
(!$mobile ||
|
(!$mobile ||
|
||||||
!(
|
!(
|
||||||
|
|
@ -862,7 +888,37 @@
|
||||||
}}
|
}}
|
||||||
on:paste={async (e) => {
|
on:paste={async (e) => {
|
||||||
e = e.detail.event;
|
e = e.detail.event;
|
||||||
console.info(e);
|
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}
|
{/key}
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@
|
||||||
|
|
||||||
<div slot="content">
|
<div slot="content">
|
||||||
<DropdownMenu.Content
|
<DropdownMenu.Content
|
||||||
class="w-full max-w-[200px] rounded-2xl px-1 py-1 border border-gray-100 dark:border-gray-850 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg transition"
|
class="w-full max-w-[200px] rounded-2xl px-1 py-1 border border-gray-100 dark:border-gray-800 z-999 bg-white dark:bg-gray-850 dark:text-white shadow-lg transition"
|
||||||
sideOffset={4}
|
sideOffset={4}
|
||||||
alignOffset={-6}
|
alignOffset={-6}
|
||||||
side="bottom"
|
side="bottom"
|
||||||
|
|
@ -54,7 +54,7 @@
|
||||||
transition={flyAndScale}
|
transition={flyAndScale}
|
||||||
>
|
>
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
class="flex gap-2 items-center px-3 py-1.5 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
|
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800/50 rounded-xl"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
uploadFilesHandler();
|
uploadFilesHandler();
|
||||||
}}
|
}}
|
||||||
|
|
@ -63,9 +63,8 @@
|
||||||
<div class="line-clamp-1">{$i18n.t('Upload Files')}</div>
|
<div class="line-clamp-1">{$i18n.t('Upload Files')}</div>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
|
|
||||||
{#if !$mobile}
|
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
class="flex gap-2 items-center px-3 py-1.5 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
|
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800/50 rounded-xl"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
screenCaptureHandler();
|
screenCaptureHandler();
|
||||||
}}
|
}}
|
||||||
|
|
@ -73,7 +72,6 @@
|
||||||
<Camera />
|
<Camera />
|
||||||
<div class=" line-clamp-1">{$i18n.t('Capture')}</div>
|
<div class=" line-clamp-1">{$i18n.t('Capture')}</div>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
{/if}
|
|
||||||
</DropdownMenu.Content>
|
</DropdownMenu.Content>
|
||||||
</div>
|
</div>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
|
|
||||||
|
|
@ -1,41 +1,88 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getContext } from 'svelte';
|
import { getContext, onDestroy, onMount } from 'svelte';
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
import { models } from '$lib/stores';
|
import { channels, models, user } from '$lib/stores';
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
|
import Hashtag from '$lib/components/icons/Hashtag.svelte';
|
||||||
|
import Lock from '$lib/components/icons/Lock.svelte';
|
||||||
|
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
||||||
|
import { searchUsers } from '$lib/apis/users';
|
||||||
|
|
||||||
export let query = '';
|
export let query = '';
|
||||||
|
|
||||||
export let command: (payload: { id: string; label: string }) => void;
|
export let command: (payload: { id: string; label: string }) => void;
|
||||||
export let selectedIndex = 0;
|
export let selectedIndex = 0;
|
||||||
|
|
||||||
let items = [];
|
export let label = '';
|
||||||
|
export let triggerChar = '@';
|
||||||
|
|
||||||
$: filteredItems = $models.filter((u) => u.name.toLowerCase().includes(query.toLowerCase()));
|
export let modelSuggestions = false;
|
||||||
|
export let userSuggestions = false;
|
||||||
|
export let channelSuggestions = false;
|
||||||
|
|
||||||
|
let _models = [];
|
||||||
|
let _users = [];
|
||||||
|
let _channels = [];
|
||||||
|
|
||||||
|
$: filteredItems = [..._users, ..._models, ..._channels].filter(
|
||||||
|
(u) =>
|
||||||
|
u.label.toLowerCase().includes(query.toLowerCase()) ||
|
||||||
|
u.id.toLowerCase().includes(query.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
const getUserList = async () => {
|
||||||
|
const res = await searchUsers(localStorage.token, query).catch((error) => {
|
||||||
|
console.error('Error searching users:', error);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
_users = [...res.users.map((u) => ({ type: 'user', id: u.id, label: u.name }))].sort((a, b) =>
|
||||||
|
a.label.localeCompare(b.label)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$: if (query !== null && userSuggestions) {
|
||||||
|
getUserList();
|
||||||
|
}
|
||||||
|
|
||||||
const select = (index: number) => {
|
const select = (index: number) => {
|
||||||
const item = filteredItems[index];
|
const item = filteredItems[index];
|
||||||
// Add the "A:" prefix to the id to indicate it's an agent/assistant/ai model
|
if (!item) return;
|
||||||
if (item) command({ id: `A:${item.id}|${item.name}`, label: item.name });
|
|
||||||
|
// Add the "U:", "M:" or "C:" prefix to the id
|
||||||
|
// and also append the label after a pipe |
|
||||||
|
// so that the mention renderer can show the label
|
||||||
|
if (item)
|
||||||
|
command({
|
||||||
|
id: `${item.type === 'user' ? 'U' : item.type === 'model' ? 'M' : 'C'}:${item.id}|${item.label}`,
|
||||||
|
label: item.label
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onKeyDown = (event: KeyboardEvent) => {
|
const onKeyDown = (event: KeyboardEvent) => {
|
||||||
if (!['ArrowUp', 'ArrowDown', 'Enter', 'Tab', 'Escape'].includes(event.key)) return false;
|
if (!['ArrowUp', 'ArrowDown', 'Enter', 'Tab', 'Escape'].includes(event.key)) return false;
|
||||||
|
|
||||||
if (event.key === 'ArrowUp') {
|
if (event.key === 'ArrowUp') {
|
||||||
selectedIndex = (selectedIndex + filteredItems.length - 1) % filteredItems.length;
|
selectedIndex = Math.max(0, selectedIndex - 1);
|
||||||
const item = document.querySelector(`[data-selected="true"]`);
|
const item = document.querySelector(`[data-selected="true"]`);
|
||||||
item?.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'instant' });
|
item?.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'instant' });
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (event.key === 'ArrowDown') {
|
if (event.key === 'ArrowDown') {
|
||||||
selectedIndex = (selectedIndex + 1) % filteredItems.length;
|
selectedIndex = Math.min(selectedIndex + 1, filteredItems.length - 1);
|
||||||
const item = document.querySelector(`[data-selected="true"]`);
|
const item = document.querySelector(`[data-selected="true"]`);
|
||||||
item?.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'instant' });
|
item?.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'instant' });
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (event.key === 'Enter' || event.key === 'Tab') {
|
if (event.key === 'Enter' || event.key === 'Tab') {
|
||||||
select(selectedIndex);
|
select(selectedIndex);
|
||||||
|
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (event.key === 'Escape') {
|
if (event.key === 'Escape') {
|
||||||
|
|
@ -50,18 +97,57 @@
|
||||||
export function _onKeyDown(event: KeyboardEvent) {
|
export function _onKeyDown(event: KeyboardEvent) {
|
||||||
return onKeyDown(event);
|
return onKeyDown(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const keydownListener = (e) => {
|
||||||
|
// required to prevent the default enter behavior
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
select(selectedIndex);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
window.addEventListener('keydown', keydownListener);
|
||||||
|
if (channelSuggestions) {
|
||||||
|
// Add a dummy channel item
|
||||||
|
_channels = [
|
||||||
|
...$channels.map((c) => ({ type: 'channel', id: c.id, label: c.name, data: c }))
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
if (userSuggestions) {
|
||||||
|
await getUserList();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modelSuggestions) {
|
||||||
|
_models = [...$models.map((m) => ({ type: 'model', id: m.id, label: m.name, data: m }))];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
window.removeEventListener('keydown', keydownListener);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if filteredItems.length}
|
{#if filteredItems.length}
|
||||||
<div
|
<div
|
||||||
class="mention-list text-black dark:text-white rounded-2xl shadow-lg border border-gray-200 dark:border-gray-800 flex flex-col bg-white dark:bg-gray-850 w-60 p-1"
|
class="mention-list text-black dark:text-white rounded-2xl shadow-lg border border-gray-200 dark:border-gray-800 flex flex-col bg-white dark:bg-gray-850 w-72 p-1"
|
||||||
id="suggestions-container"
|
id="suggestions-container"
|
||||||
>
|
>
|
||||||
<div class="overflow-y-auto scrollbar-thin max-h-60">
|
<div class="overflow-y-auto scrollbar-thin max-h-60">
|
||||||
<div class="px-2 text-xs text-gray-500 py-1">
|
|
||||||
{$i18n.t('Models')}
|
|
||||||
</div>
|
|
||||||
{#each filteredItems as item, i}
|
{#each filteredItems as item, i}
|
||||||
|
{#if i === 0 || item?.type !== filteredItems[i - 1]?.type}
|
||||||
|
<div class="px-2 text-xs text-gray-500 py-1">
|
||||||
|
{#if item?.type === 'user'}
|
||||||
|
{$i18n.t('Users')}
|
||||||
|
{:else if item?.type === 'model'}
|
||||||
|
{$i18n.t('Models')}
|
||||||
|
{:else if item?.type === 'channel'}
|
||||||
|
{$i18n.t('Channels')}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<Tooltip content={item?.id} placement="top-start">
|
<Tooltip content={item?.id} placement="top-start">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -69,13 +155,47 @@
|
||||||
on:mousemove={() => {
|
on:mousemove={() => {
|
||||||
selectedIndex = i;
|
selectedIndex = i;
|
||||||
}}
|
}}
|
||||||
class="px-2.5 py-1.5 rounded-xl w-full text-left {i === selectedIndex
|
class="flex items-center justify-between px-2.5 py-1.5 rounded-xl w-full text-left {i ===
|
||||||
|
selectedIndex
|
||||||
? 'bg-gray-50 dark:bg-gray-800 selected-command-option-button'
|
? 'bg-gray-50 dark:bg-gray-800 selected-command-option-button'
|
||||||
: ''}"
|
: ''}"
|
||||||
data-selected={i === selectedIndex}
|
data-selected={i === selectedIndex}
|
||||||
>
|
>
|
||||||
<div class="truncate">
|
{#if item.type === 'channel'}
|
||||||
@{item.name}
|
<div class=" size-4 justify-center flex items-center mr-0.5">
|
||||||
|
{#if item?.data?.access_control === null}
|
||||||
|
<Hashtag className="size-3" strokeWidth="2.5" />
|
||||||
|
{:else}
|
||||||
|
<Lock className="size-[15px]" strokeWidth="2" />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{:else if item.type === 'model'}
|
||||||
|
<img
|
||||||
|
src={item?.data?.info?.meta?.profile_image_url ??
|
||||||
|
`${WEBUI_BASE_URL}/static/favicon.png`}
|
||||||
|
alt={item?.data?.name ?? item.id}
|
||||||
|
class="rounded-full size-5 items-center mr-2"
|
||||||
|
/>
|
||||||
|
{:else if item.type === 'user'}
|
||||||
|
<img
|
||||||
|
src={`${WEBUI_API_BASE_URL}/users/${item.id}/profile/image`}
|
||||||
|
alt={item?.label ?? item.id}
|
||||||
|
class="rounded-full size-5 items-center mr-2"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="truncate flex-1 pr-2">
|
||||||
|
{item.label}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="shrink-0 text-xs text-gray-500">
|
||||||
|
{#if item.type === 'user'}
|
||||||
|
{$i18n.t('User')}
|
||||||
|
{:else if item.type === 'model'}
|
||||||
|
{$i18n.t('Model')}
|
||||||
|
{:else if item.type === 'channel'}
|
||||||
|
{$i18n.t('Channel')}
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
|
||||||
|
|
@ -63,11 +63,7 @@
|
||||||
</div>
|
</div>
|
||||||
</Loader>
|
</Loader>
|
||||||
{:else if !thread}
|
{:else if !thread}
|
||||||
<div
|
<div class="px-5 max-w-full mx-auto">
|
||||||
class="px-5
|
|
||||||
|
|
||||||
{($settings?.widescreenMode ?? null) ? 'max-w-full' : 'max-w-5xl'} mx-auto"
|
|
||||||
>
|
|
||||||
{#if channel}
|
{#if channel}
|
||||||
<div class="flex flex-col gap-1.5 pb-5 pt-10">
|
<div class="flex flex-col gap-1.5 pb-5 pt-10">
|
||||||
<div class="text-2xl font-medium capitalize">{channel.name}</div>
|
<div class="text-2xl font-medium capitalize">{channel.name}</div>
|
||||||
|
|
@ -99,7 +95,8 @@
|
||||||
{message}
|
{message}
|
||||||
{thread}
|
{thread}
|
||||||
showUserProfile={messageIdx === 0 ||
|
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={() => {
|
onDelete={() => {
|
||||||
messages = messages.filter((m) => m.id !== message.id);
|
messages = messages.filter((m) => m.id !== message.id);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
|
|
||||||
import { settings, user, shortCodesToEmojis } from '$lib/stores';
|
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 Markdown from '$lib/components/chat/Messages/Markdown.svelte';
|
||||||
import ProfileImage from '$lib/components/chat/Messages/ProfileImage.svelte';
|
import ProfileImage from '$lib/components/chat/Messages/ProfileImage.svelte';
|
||||||
|
|
@ -34,6 +34,8 @@
|
||||||
import ChevronRight from '$lib/components/icons/ChevronRight.svelte';
|
import ChevronRight from '$lib/components/icons/ChevronRight.svelte';
|
||||||
import { formatDate } from '$lib/utils';
|
import { formatDate } from '$lib/utils';
|
||||||
import Emoji from '$lib/components/common/Emoji.svelte';
|
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 message;
|
||||||
export let showUserProfile = true;
|
export let showUserProfile = true;
|
||||||
|
|
@ -64,9 +66,7 @@
|
||||||
<div
|
<div
|
||||||
class="flex flex-col justify-between px-5 {showUserProfile
|
class="flex flex-col justify-between px-5 {showUserProfile
|
||||||
? 'pt-1.5 pb-0.5'
|
? 'pt-1.5 pb-0.5'
|
||||||
: ''} w-full {($settings?.widescreenMode ?? null)
|
: ''} w-full max-w-full mx-auto group hover:bg-gray-300/5 dark:hover:bg-gray-700/5 transition relative"
|
||||||
? 'max-w-full'
|
|
||||||
: 'max-w-5xl'} mx-auto group hover:bg-gray-300/5 dark:hover:bg-gray-700/5 transition relative"
|
|
||||||
>
|
>
|
||||||
{#if !edit}
|
{#if !edit}
|
||||||
<div
|
<div
|
||||||
|
|
@ -140,15 +140,20 @@
|
||||||
>
|
>
|
||||||
<div class={`shrink-0 mr-3 w-9`}>
|
<div class={`shrink-0 mr-3 w-9`}>
|
||||||
{#if showUserProfile}
|
{#if showUserProfile}
|
||||||
|
{#if message?.meta?.model_id}
|
||||||
|
<img
|
||||||
|
src={`${WEBUI_API_BASE_URL}/models/model/profile/image?id=${message.meta.model_id}`}
|
||||||
|
alt={message.meta.model_name ?? message.meta.model_id}
|
||||||
|
class="size-8 translate-y-1 ml-0.5 object-cover rounded-full"
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
<ProfilePreview user={message.user}>
|
<ProfilePreview user={message.user}>
|
||||||
<ProfileImage
|
<ProfileImage
|
||||||
src={message.user?.profile_image_url ??
|
src={message.user?.profile_image_url ?? `${WEBUI_BASE_URL}/static/favicon.png`}
|
||||||
($i18n.language === 'dg-DG'
|
|
||||||
? `${WEBUI_BASE_URL}/doge.png`
|
|
||||||
: `${WEBUI_BASE_URL}/static/favicon.png`)}
|
|
||||||
className={'size-8 translate-y-1 ml-0.5'}
|
className={'size-8 translate-y-1 ml-0.5'}
|
||||||
/>
|
/>
|
||||||
</ProfilePreview>
|
</ProfilePreview>
|
||||||
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<!-- <div class="w-7 h-7 rounded-full bg-transparent" /> -->
|
<!-- <div class="w-7 h-7 rounded-full bg-transparent" /> -->
|
||||||
|
|
||||||
|
|
@ -168,7 +173,11 @@
|
||||||
{#if showUserProfile}
|
{#if showUserProfile}
|
||||||
<Name>
|
<Name>
|
||||||
<div class=" self-end text-base shrink-0 font-medium truncate">
|
<div class=" self-end text-base shrink-0 font-medium truncate">
|
||||||
|
{#if message?.meta?.model_id}
|
||||||
|
{message?.meta?.model_name ?? message?.meta?.model_id}
|
||||||
|
{:else}
|
||||||
{message?.user?.name}
|
{message?.user?.name}
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if message.created_at}
|
{#if message.created_at}
|
||||||
|
|
@ -256,12 +265,16 @@
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class=" min-w-full markdown-prose">
|
<div class=" min-w-full markdown-prose">
|
||||||
|
{#if (message?.content ?? '').trim() === '' && message?.meta?.model_id}
|
||||||
|
<Skeleton />
|
||||||
|
{:else}
|
||||||
<Markdown
|
<Markdown
|
||||||
id={message.id}
|
id={message.id}
|
||||||
content={message.content}
|
content={message.content}
|
||||||
/>{#if message.created_at !== message.updated_at}<span class="text-gray-500 text-[10px]"
|
/>{#if message.created_at !== message.updated_at && (message?.meta?.model_id ?? null) === null}<span
|
||||||
>(edited)</span
|
class="text-gray-500 text-[10px]">({$i18n.t('edited')})</span
|
||||||
>{/if}
|
>{/if}
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if (message?.reactions ?? []).length > 0}
|
{#if (message?.reactions ?? []).length > 0}
|
||||||
|
|
|
||||||
|
|
@ -1,101 +1,18 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { DropdownMenu } from 'bits-ui';
|
import { LinkPreview } from 'bits-ui';
|
||||||
import { getContext } from 'svelte';
|
import { getContext } from 'svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
import UserStatus from './UserStatus.svelte';
|
||||||
import { flyAndScale } from '$lib/utils/transitions';
|
import UserStatusLinkPreview from './UserStatusLinkPreview.svelte';
|
||||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
|
||||||
import { getUserActiveStatusById } from '$lib/apis/users';
|
|
||||||
|
|
||||||
export let side = 'right';
|
|
||||||
export let align = 'top';
|
|
||||||
|
|
||||||
export let user = null;
|
export let user = null;
|
||||||
let show = false;
|
|
||||||
|
|
||||||
let active = false;
|
|
||||||
|
|
||||||
const getActiveStatus = async () => {
|
|
||||||
const res = await getUserActiveStatusById(localStorage.token, user.id).catch((error) => {
|
|
||||||
console.error('Error fetching user active status:', error);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res) {
|
|
||||||
active = res.active;
|
|
||||||
} else {
|
|
||||||
active = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
$: if (show) {
|
|
||||||
getActiveStatus();
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DropdownMenu.Root
|
<LinkPreview.Root openDelay={0} closeDelay={0}>
|
||||||
bind:open={show}
|
<LinkPreview.Trigger class=" cursor-pointer no-underline! font-normal! ">
|
||||||
closeFocus={false}
|
|
||||||
onOpenChange={(state) => {}}
|
|
||||||
typeahead={false}
|
|
||||||
>
|
|
||||||
<DropdownMenu.Trigger>
|
|
||||||
<slot />
|
<slot />
|
||||||
</DropdownMenu.Trigger>
|
</LinkPreview.Trigger>
|
||||||
|
|
||||||
<slot name="content">
|
<UserStatusLinkPreview id={user?.id} side="right" align="center" sideOffset={8} />
|
||||||
<DropdownMenu.Content
|
</LinkPreview.Root>
|
||||||
class="max-w-full w-[240px] rounded-lg z-9999 bg-white dark:bg-black dark:text-white shadow-lg"
|
|
||||||
sideOffset={8}
|
|
||||||
{side}
|
|
||||||
{align}
|
|
||||||
transition={flyAndScale}
|
|
||||||
>
|
|
||||||
{#if user}
|
|
||||||
<div class=" flex flex-col gap-2 w-full rounded-lg">
|
|
||||||
<div class="py-8 relative bg-gray-900 rounded-t-lg">
|
|
||||||
<img
|
|
||||||
crossorigin="anonymous"
|
|
||||||
src={user?.profile_image_url ?? `${WEBUI_BASE_URL}/static/favicon.png`}
|
|
||||||
class=" absolute -bottom-5 left-3 size-12 ml-0.5 object-cover rounded-full -translate-y-[1px]"
|
|
||||||
alt="profile"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class=" flex flex-col pt-4 pb-2.5 px-4">
|
|
||||||
<div class=" -mb-1">
|
|
||||||
<span class="font-medium text-sm line-clamp-1"> {user.name} </span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class=" flex items-center gap-2">
|
|
||||||
{#if active}
|
|
||||||
<div>
|
|
||||||
<span class="relative flex size-2">
|
|
||||||
<span
|
|
||||||
class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"
|
|
||||||
/>
|
|
||||||
<span class="relative inline-flex rounded-full size-2 bg-green-500" />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class=" -translate-y-[1px]">
|
|
||||||
<span class="text-xs"> {$i18n.t('Active')} </span>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<div>
|
|
||||||
<span class="relative flex size-2">
|
|
||||||
<span class="relative inline-flex rounded-full size-2 bg-gray-500" />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class=" -translate-y-[1px]">
|
|
||||||
<span class="text-xs"> {$i18n.t('Away')} </span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</DropdownMenu.Content>
|
|
||||||
</slot>
|
|
||||||
</DropdownMenu.Root>
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { getContext, onMount } from 'svelte';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||||
|
|
||||||
|
export let user = null;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if user}
|
||||||
|
<div class=" flex gap-3.5 w-full py-3 px-3 items-center">
|
||||||
|
<div class=" items-center flex shrink-0">
|
||||||
|
<img
|
||||||
|
crossorigin="anonymous"
|
||||||
|
src={user?.profile_image_url ?? `${WEBUI_BASE_URL}/static/favicon.png`}
|
||||||
|
class=" size-12 object-cover rounded-xl"
|
||||||
|
alt="profile"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class=" flex flex-col w-full flex-1">
|
||||||
|
<div class="mb-0.5 font-medium line-clamp-1 pr-2">
|
||||||
|
{user.name}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class=" flex items-center gap-2">
|
||||||
|
{#if user?.active}
|
||||||
|
<div>
|
||||||
|
<span class="relative flex size-2">
|
||||||
|
<span
|
||||||
|
class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"
|
||||||
|
/>
|
||||||
|
<span class="relative inline-flex rounded-full size-2 bg-green-500" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span class="text-xs"> {$i18n.t('Active')} </span>
|
||||||
|
{:else}
|
||||||
|
<div>
|
||||||
|
<span class="relative flex size-2">
|
||||||
|
<span class="relative inline-flex rounded-full size-2 bg-gray-500" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span class="text-xs"> {$i18n.t('Away')} </span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { getContext, onMount } from 'svelte';
|
||||||
|
import { LinkPreview } from 'bits-ui';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
import { getUserById } from '$lib/apis/users';
|
||||||
|
|
||||||
|
import UserStatus from './UserStatus.svelte';
|
||||||
|
|
||||||
|
export let id = null;
|
||||||
|
|
||||||
|
export let side = 'top';
|
||||||
|
export let align = 'start';
|
||||||
|
export let sideOffset = 6;
|
||||||
|
|
||||||
|
let user = null;
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
if (id) {
|
||||||
|
user = await getUserById(localStorage.token, id).catch((error) => {
|
||||||
|
console.error('Error fetching user by ID:', error);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if user}
|
||||||
|
<LinkPreview.Content
|
||||||
|
class="w-full max-w-[260px] rounded-2xl border border-gray-100 dark:border-gray-800 z-999 bg-white dark:bg-gray-850 dark:text-white shadow-lg transition"
|
||||||
|
{side}
|
||||||
|
{align}
|
||||||
|
{sideOffset}
|
||||||
|
>
|
||||||
|
<UserStatus {user} />
|
||||||
|
</LinkPreview.Content>
|
||||||
|
{/if}
|
||||||
|
|
@ -159,7 +159,7 @@
|
||||||
|
|
||||||
{#if channel}
|
{#if channel}
|
||||||
<div class="flex flex-col w-full h-full bg-gray-50 dark:bg-gray-850">
|
<div class="flex flex-col w-full h-full bg-gray-50 dark:bg-gray-850">
|
||||||
<div class="flex items-center justify-between px-3.5 pt-3">
|
<div class="sticky top-0 flex items-center justify-between px-3.5 py-3">
|
||||||
<div class=" font-medium text-lg">{$i18n.t('Thread')}</div>
|
<div class=" font-medium text-lg">{$i18n.t('Thread')}</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -174,7 +174,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" max-h-full w-full overflow-y-auto pt-3" bind:this={messagesContainerElement}>
|
<div class=" max-h-full w-full overflow-y-auto" bind:this={messagesContainerElement}>
|
||||||
<Messages
|
<Messages
|
||||||
id={threadId}
|
id={threadId}
|
||||||
{channel}
|
{channel}
|
||||||
|
|
@ -198,8 +198,16 @@
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class=" pb-[1rem] px-2.5">
|
<div class=" pb-[1rem] px-2.5 w-full">
|
||||||
<MessageInput id={threadId} {typingUsers} {onChange} onSubmit={submitHandler} />
|
<MessageInput
|
||||||
|
id={threadId}
|
||||||
|
typingUsersClassName="from-gray-50 dark:from-gray-850"
|
||||||
|
{typingUsers}
|
||||||
|
userSuggestions={true}
|
||||||
|
channelSuggestions={true}
|
||||||
|
{onChange}
|
||||||
|
onSubmit={submitHandler}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
import Tooltip from '../common/Tooltip.svelte';
|
import Tooltip from '../common/Tooltip.svelte';
|
||||||
import SvgPanZoom from '../common/SVGPanZoom.svelte';
|
import SvgPanZoom from '../common/SVGPanZoom.svelte';
|
||||||
import ArrowLeft from '../icons/ArrowLeft.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 overlay = false;
|
||||||
export let history;
|
export let history;
|
||||||
|
|
@ -205,7 +205,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class=" w-full h-full relative flex flex-col bg-gray-50 dark:bg-gray-850"
|
class=" w-full h-full relative flex flex-col bg-white dark:bg-gray-850"
|
||||||
id="artifacts-container"
|
id="artifacts-container"
|
||||||
>
|
>
|
||||||
<div class="w-full h-full flex flex-col flex-1 relative">
|
<div class="w-full h-full flex flex-col flex-1 relative">
|
||||||
|
|
@ -213,15 +213,6 @@
|
||||||
<div
|
<div
|
||||||
class="pointer-events-auto z-20 flex justify-between items-center p-2.5 font-primar text-gray-900 dark:text-white"
|
class="pointer-events-auto z-20 flex justify-between items-center p-2.5 font-primar text-gray-900 dark:text-white"
|
||||||
>
|
>
|
||||||
<button
|
|
||||||
class="self-center pointer-events-auto p-1 rounded-full bg-white dark:bg-gray-850"
|
|
||||||
on:click={() => {
|
|
||||||
showArtifacts.set(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ArrowLeft className="size-3.5 text-gray-900 dark:text-white" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="flex-1 flex items-center justify-between pr-1">
|
<div class="flex-1 flex items-center justify-between pr-1">
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<div class="flex items-center gap-0.5 self-center min-w-fit" dir="ltr">
|
<div class="flex items-center gap-0.5 self-center min-w-fit" dir="ltr">
|
||||||
|
|
@ -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"
|
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}
|
on:click={downloadArtifact}
|
||||||
>
|
>
|
||||||
<ArrowDownTray className="size-3.5" />
|
<Download className="size-3.5" />
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import mermaid from 'mermaid';
|
|
||||||
import { PaneGroup, Pane, PaneResizer } from 'paneforge';
|
import { PaneGroup, Pane, PaneResizer } from 'paneforge';
|
||||||
|
|
||||||
import { getContext, onDestroy, onMount, tick } from 'svelte';
|
import { getContext, onDestroy, onMount, tick } from 'svelte';
|
||||||
|
|
@ -90,6 +89,7 @@
|
||||||
import Tooltip from '../common/Tooltip.svelte';
|
import Tooltip from '../common/Tooltip.svelte';
|
||||||
import Sidebar from '../icons/Sidebar.svelte';
|
import Sidebar from '../icons/Sidebar.svelte';
|
||||||
import { getFunctions } from '$lib/apis/functions';
|
import { getFunctions } from '$lib/apis/functions';
|
||||||
|
import Image from '../common/Image.svelte';
|
||||||
|
|
||||||
export let chatIdProp = '';
|
export let chatIdProp = '';
|
||||||
|
|
||||||
|
|
@ -465,6 +465,15 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (event.data.type === 'action:submit') {
|
||||||
|
console.debug(event.data.text);
|
||||||
|
|
||||||
|
if (prompt !== '') {
|
||||||
|
await tick();
|
||||||
|
submitPrompt(prompt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Replace with your iframe's origin
|
// Replace with your iframe's origin
|
||||||
if (event.data.type === 'input:prompt') {
|
if (event.data.type === 'input:prompt') {
|
||||||
console.debug(event.data.text);
|
console.debug(event.data.text);
|
||||||
|
|
@ -477,15 +486,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.data.type === 'action:submit') {
|
|
||||||
console.debug(event.data.text);
|
|
||||||
|
|
||||||
if (prompt !== '') {
|
|
||||||
await tick();
|
|
||||||
submitPrompt(prompt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.data.type === 'input:prompt:submit') {
|
if (event.data.type === 'input:prompt:submit') {
|
||||||
console.debug(event.data.text);
|
console.debug(event.data.text);
|
||||||
|
|
||||||
|
|
@ -1788,6 +1788,23 @@
|
||||||
}))
|
}))
|
||||||
.filter((message) => message?.role === 'user' || message?.content?.trim());
|
.filter((message) => message?.role === 'user' || message?.content?.trim());
|
||||||
|
|
||||||
|
const toolIds = [];
|
||||||
|
const toolServerIds = [];
|
||||||
|
|
||||||
|
for (const toolId of selectedToolIds) {
|
||||||
|
if (toolId.startsWith('direct_server:')) {
|
||||||
|
let serverId = toolId.replace('direct_server:', '');
|
||||||
|
// Check if serverId is a number
|
||||||
|
if (!isNaN(parseInt(serverId))) {
|
||||||
|
toolServerIds.push(parseInt(serverId));
|
||||||
|
} else {
|
||||||
|
toolServerIds.push(serverId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toolIds.push(toolId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const res = await generateOpenAIChatCompletion(
|
const res = await generateOpenAIChatCompletion(
|
||||||
localStorage.token,
|
localStorage.token,
|
||||||
{
|
{
|
||||||
|
|
@ -1808,8 +1825,10 @@
|
||||||
files: (files?.length ?? 0) > 0 ? files : undefined,
|
files: (files?.length ?? 0) > 0 ? files : undefined,
|
||||||
|
|
||||||
filter_ids: selectedFilterIds.length > 0 ? selectedFilterIds : undefined,
|
filter_ids: selectedFilterIds.length > 0 ? selectedFilterIds : undefined,
|
||||||
tool_ids: selectedToolIds.length > 0 ? selectedToolIds : undefined,
|
tool_ids: toolIds.length > 0 ? toolIds : undefined,
|
||||||
tool_servers: $toolServers,
|
tool_servers: ($toolServers ?? []).filter(
|
||||||
|
(server, idx) => toolServerIds.includes(idx) || toolServerIds.includes(server?.id)
|
||||||
|
),
|
||||||
features: getFeatures(),
|
features: getFeatures(),
|
||||||
variables: {
|
variables: {
|
||||||
...getPromptVariables($user?.name, $settings?.userLocation ? userLocation : undefined)
|
...getPromptVariables($user?.name, $settings?.userLocation ? userLocation : undefined)
|
||||||
|
|
@ -2244,7 +2263,18 @@
|
||||||
>
|
>
|
||||||
{#if !loading}
|
{#if !loading}
|
||||||
<div in:fade={{ duration: 50 }} class="w-full h-full flex flex-col">
|
<div in:fade={{ duration: 50 }} class="w-full h-full flex flex-col">
|
||||||
{#if $settings?.backgroundImageUrl ?? $config?.license_metadata?.background_image_url ?? null}
|
{#if $selectedFolder && $selectedFolder?.meta?.background_image_url}
|
||||||
|
<div
|
||||||
|
class="absolute {$showSidebar
|
||||||
|
? 'md:max-w-[calc(100%-260px)] md:translate-x-[260px]'
|
||||||
|
: ''} top-0 left-0 w-full h-full bg-cover bg-center bg-no-repeat"
|
||||||
|
style="background-image: url({$selectedFolder?.meta?.background_image_url}) "
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="absolute top-0 left-0 w-full h-full bg-linear-to-t from-white to-white/85 dark:from-gray-900 dark:to-gray-900/90 z-0"
|
||||||
|
/>
|
||||||
|
{:else if $settings?.backgroundImageUrl ?? $config?.license_metadata?.background_image_url ?? null}
|
||||||
<div
|
<div
|
||||||
class="absolute {$showSidebar
|
class="absolute {$showSidebar
|
||||||
? 'md:max-w-[calc(100%-260px)] md:translate-x-[260px]'
|
? 'md:max-w-[calc(100%-260px)] md:translate-x-[260px]'
|
||||||
|
|
@ -2259,7 +2289,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<PaneGroup direction="horizontal" class="w-full h-full">
|
<PaneGroup direction="horizontal" class="w-full h-full">
|
||||||
<Pane defaultSize={50} class="h-full flex relative max-w-full flex-col">
|
<Pane defaultSize={50} minSize={30} class="h-full flex relative max-w-full flex-col">
|
||||||
<Navbar
|
<Navbar
|
||||||
bind:this={navbarElement}
|
bind:this={navbarElement}
|
||||||
chat={{
|
chat={{
|
||||||
|
|
|
||||||
|
|
@ -205,12 +205,12 @@
|
||||||
|
|
||||||
{#if $showControls}
|
{#if $showControls}
|
||||||
<PaneResizer
|
<PaneResizer
|
||||||
class="relative flex w-2 items-center justify-center bg-background group"
|
class="relative flex items-center justify-center group border-l border-gray-50 dark:border-gray-850 hover:border-gray-200 dark:hover:border-gray-800 transition z-20"
|
||||||
id="controls-resizer"
|
id="controls-resizer"
|
||||||
>
|
>
|
||||||
<div class="z-10 flex h-7 w-5 items-center justify-center rounded-xs">
|
<div
|
||||||
<EllipsisVertical className="size-4 invisible group-hover:visible" />
|
class=" absolute -left-1.5 -right-1.5 -top-0 -bottom-0 z-20 cursor-col-resize bg-transparent"
|
||||||
</div>
|
/>
|
||||||
</PaneResizer>
|
</PaneResizer>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|
@ -236,14 +236,14 @@
|
||||||
showControls.set(false);
|
showControls.set(false);
|
||||||
}}
|
}}
|
||||||
collapsible={true}
|
collapsible={true}
|
||||||
class=" z-10 "
|
class=" z-10 bg-white dark:bg-gray-850"
|
||||||
>
|
>
|
||||||
{#if $showControls}
|
{#if $showControls}
|
||||||
<div class="flex max-h-full min-h-full">
|
<div class="flex max-h-full min-h-full">
|
||||||
<div
|
<div
|
||||||
class="w-full {($showOverview || $showArtifacts) && !$showCallOverlay
|
class="w-full {($showOverview || $showArtifacts) && !$showCallOverlay
|
||||||
? ' '
|
? ' '
|
||||||
: 'px-4 py-4 bg-white dark:shadow-lg dark:bg-gray-850 border border-gray-100 dark:border-gray-850'} z-40 pointer-events-auto overflow-y-auto scrollbar-hidden"
|
: 'px-4 py-4 bg-white dark:shadow-lg dark:bg-gray-850 '} z-40 pointer-events-auto overflow-y-auto scrollbar-hidden"
|
||||||
id="controls-container"
|
id="controls-container"
|
||||||
>
|
>
|
||||||
{#if $showCallOverlay}
|
{#if $showCallOverlay}
|
||||||
|
|
|
||||||
|
|
@ -74,14 +74,14 @@
|
||||||
className="w-full flex justify-start mb-0.5"
|
className="w-full flex justify-start mb-0.5"
|
||||||
placement="top"
|
placement="top"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-2 text-gray-500 font-medium text-lg mt-2 w-fit">
|
<div class="flex items-center gap-2 text-gray-500 text-lg mt-2 w-fit">
|
||||||
<EyeSlash strokeWidth="2.5" className="size-5" />{$i18n.t('Temporary Chat')}
|
<EyeSlash strokeWidth="2.5" className="size-5" />{$i18n.t('Temporary Chat')}
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class=" mt-2 mb-4 text-3xl text-gray-800 dark:text-gray-100 font-medium text-left flex items-center gap-4 font-primary"
|
class=" mt-2 mb-4 text-3xl text-gray-800 dark:text-gray-100 text-left flex items-center gap-4 font-primary"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<div class=" capitalize line-clamp-1" in:fade={{ duration: 200 }}>
|
<div class=" capitalize line-clamp-1" in:fade={{ duration: 200 }}>
|
||||||
|
|
@ -120,7 +120,7 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<div class=" font-medium text-gray-400 dark:text-gray-500 line-clamp-1 font-p">
|
<div class=" text-gray-400 dark:text-gray-500 line-clamp-1 font-p">
|
||||||
{$i18n.t('How can I help you today?')}
|
{$i18n.t('How can I help you today?')}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -172,7 +172,7 @@
|
||||||
>{$i18n.t('Select a tool')}</option
|
>{$i18n.t('Select a tool')}</option
|
||||||
>
|
>
|
||||||
|
|
||||||
{#each $tools as tool, toolIdx}
|
{#each $tools.filter((tool) => !tool?.id?.startsWith('server:')) as tool, toolIdx}
|
||||||
<option value={tool.id} class="bg-gray-100 dark:bg-gray-800">{tool.name}</option>
|
<option value={tool.id} class="bg-gray-100 dark:bg-gray-800">{tool.name}</option>
|
||||||
{/each}
|
{/each}
|
||||||
{:else if tab === 'functions'}
|
{:else if tab === 'functions'}
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,11 @@
|
||||||
type Model,
|
type Model,
|
||||||
mobile,
|
mobile,
|
||||||
settings,
|
settings,
|
||||||
showSidebar,
|
|
||||||
models,
|
models,
|
||||||
config,
|
config,
|
||||||
showCallOverlay,
|
showCallOverlay,
|
||||||
tools,
|
tools,
|
||||||
|
toolServers,
|
||||||
user as _user,
|
user as _user,
|
||||||
showControls,
|
showControls,
|
||||||
TTSWorker,
|
TTSWorker,
|
||||||
|
|
@ -45,6 +45,7 @@
|
||||||
import { generateAutoCompletion } from '$lib/apis';
|
import { generateAutoCompletion } from '$lib/apis';
|
||||||
import { deleteFileById } from '$lib/apis/files';
|
import { deleteFileById } from '$lib/apis/files';
|
||||||
import { getSessionUser } from '$lib/apis/auths';
|
import { getSessionUser } from '$lib/apis/auths';
|
||||||
|
import { getTools } from '$lib/apis/tools';
|
||||||
|
|
||||||
import { WEBUI_BASE_URL, WEBUI_API_BASE_URL, PASTED_TEXT_CHARACTER_LIMIT } from '$lib/constants';
|
import { WEBUI_BASE_URL, WEBUI_API_BASE_URL, PASTED_TEXT_CHARACTER_LIMIT } from '$lib/constants';
|
||||||
|
|
||||||
|
|
@ -99,8 +100,6 @@
|
||||||
export let prompt = '';
|
export let prompt = '';
|
||||||
export let files = [];
|
export let files = [];
|
||||||
|
|
||||||
export let toolServers = [];
|
|
||||||
|
|
||||||
export let selectedToolIds = [];
|
export let selectedToolIds = [];
|
||||||
export let selectedFilterIds = [];
|
export let selectedFilterIds = [];
|
||||||
|
|
||||||
|
|
@ -284,12 +283,17 @@
|
||||||
const chatInput = document.getElementById('chat-input');
|
const chatInput = document.getElementById('chat-input');
|
||||||
|
|
||||||
if (chatInput) {
|
if (chatInput) {
|
||||||
|
if (text !== '') {
|
||||||
text = await textVariableHandler(text || '');
|
text = await textVariableHandler(text || '');
|
||||||
|
}
|
||||||
|
|
||||||
chatInputElement?.setText(text);
|
chatInputElement?.setText(text);
|
||||||
chatInputElement?.focus();
|
chatInputElement?.focus();
|
||||||
|
|
||||||
|
if (text !== '') {
|
||||||
text = await inputVariableHandler(text);
|
text = await inputVariableHandler(text);
|
||||||
|
}
|
||||||
|
|
||||||
await tick();
|
await tick();
|
||||||
if (cb) await cb(text);
|
if (cb) await cb(text);
|
||||||
}
|
}
|
||||||
|
|
@ -437,7 +441,7 @@
|
||||||
.reduce((acc, filters) => acc.filter((f1) => filters.some((f2) => f2.id === f1.id)));
|
.reduce((acc, filters) => acc.filter((f1) => filters.some((f2) => f2.id === f1.id)));
|
||||||
|
|
||||||
let showToolsButton = false;
|
let showToolsButton = false;
|
||||||
$: showToolsButton = toolServers.length + selectedToolIds.length > 0;
|
$: showToolsButton = ($tools ?? []).length > 0 || ($toolServers ?? []).length > 0;
|
||||||
|
|
||||||
let showWebSearchButton = false;
|
let showWebSearchButton = false;
|
||||||
$: showWebSearchButton =
|
$: showWebSearchButton =
|
||||||
|
|
@ -897,6 +901,8 @@
|
||||||
dropzoneElement?.addEventListener('dragover', onDragOver);
|
dropzoneElement?.addEventListener('dragover', onDragOver);
|
||||||
dropzoneElement?.addEventListener('drop', onDrop);
|
dropzoneElement?.addEventListener('drop', onDrop);
|
||||||
dropzoneElement?.addEventListener('dragleave', onDragLeave);
|
dropzoneElement?.addEventListener('dragleave', onDragLeave);
|
||||||
|
|
||||||
|
await tools.set(await getTools(localStorage.token));
|
||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
|
|
@ -1024,7 +1030,9 @@
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex-1 flex flex-col relative w-full shadow-lg rounded-3xl border border-gray-50 dark:border-gray-850 hover:border-gray-100 focus-within:border-gray-100 hover:dark:border-gray-800 focus-within:dark:border-gray-800 transition px-1 bg-white/90 dark:bg-gray-400/5 dark:text-gray-100"
|
class="flex-1 flex flex-col relative w-full shadow-lg rounded-3xl border {$temporaryChatEnabled
|
||||||
|
? 'border-dashed border-gray-100 dark:border-gray-800 hover:border-gray-200 focus-within:border-gray-200 hover:dark:border-gray-700 focus-within:dark:border-gray-700'
|
||||||
|
: ' border-gray-50 dark:border-gray-850 hover:border-gray-100 focus-within:border-gray-100 hover:dark:border-gray-800 focus-within:dark:border-gray-800'} transition px-1 bg-white/90 dark:bg-gray-400/5 dark:text-gray-100"
|
||||||
dir={$settings?.chatDirection ?? 'auto'}
|
dir={$settings?.chatDirection ?? 'auto'}
|
||||||
id="message-input-container"
|
id="message-input-container"
|
||||||
>
|
>
|
||||||
|
|
@ -1152,7 +1160,7 @@
|
||||||
|
|
||||||
<div class="px-2.5">
|
<div class="px-2.5">
|
||||||
<div
|
<div
|
||||||
class="scrollbar-hidden rtl:text-right ltr:text-left bg-transparent dark:text-gray-100 outline-hidden w-full pb-1 px-1 resize-none h-fit max-h-80 overflow-auto {files.length ===
|
class="scrollbar-hidden rtl:text-right ltr:text-left bg-transparent dark:text-gray-100 outline-hidden w-full pb-1 px-1 resize-none h-fit max-h-96 overflow-auto {files.length ===
|
||||||
0
|
0
|
||||||
? atSelectedModel !== undefined
|
? atSelectedModel !== undefined
|
||||||
? 'pt-1.5'
|
? 'pt-1.5'
|
||||||
|
|
@ -1451,10 +1459,10 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="ml-1 flex gap-1.5">
|
<div class="ml-1 flex gap-1.5">
|
||||||
{#if showToolsButton}
|
{#if (selectedToolIds ?? []).length > 0}
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={$i18n.t('{{COUNT}} Available Tools', {
|
content={$i18n.t('{{COUNT}} Available Tools', {
|
||||||
COUNT: toolServers.length + selectedToolIds.length
|
COUNT: selectedToolIds.length
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
|
|
@ -1468,7 +1476,7 @@
|
||||||
<Wrench className="size-4" strokeWidth="1.75" />
|
<Wrench className="size-4" strokeWidth="1.75" />
|
||||||
|
|
||||||
<span class="text-sm">
|
<span class="text-sm">
|
||||||
{toolServers.length + selectedToolIds.length}
|
{selectedToolIds.length}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@
|
||||||
|
|
||||||
<div slot="content">
|
<div slot="content">
|
||||||
<DropdownMenu.Content
|
<DropdownMenu.Content
|
||||||
class="w-full max-w-[180px] rounded-lg px-1 py-1.5 border border-gray-100 dark:border-gray-800 z-9999 bg-white dark:bg-gray-900 dark:text-white shadow-xs"
|
class="w-full max-w-[180px] rounded-lg p-1 border border-gray-100 dark:border-gray-800 z-9999 bg-white dark:bg-gray-900 dark:text-white shadow-xs"
|
||||||
sideOffset={6}
|
sideOffset={6}
|
||||||
side="top"
|
side="top"
|
||||||
align="start"
|
align="start"
|
||||||
|
|
|
||||||
|
|
@ -120,7 +120,7 @@
|
||||||
className="w-full"
|
className="w-full"
|
||||||
>
|
>
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl {!fileUploadEnabled
|
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800/50 rounded-xl {!fileUploadEnabled
|
||||||
? 'opacity-50'
|
? 'opacity-50'
|
||||||
: ''}"
|
: ''}"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
|
|
@ -144,7 +144,7 @@
|
||||||
className="w-full"
|
className="w-full"
|
||||||
>
|
>
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl {!fileUploadEnabled
|
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800/50 rounded-xl {!fileUploadEnabled
|
||||||
? 'opacity-50'
|
? 'opacity-50'
|
||||||
: ''}"
|
: ''}"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
|
|
@ -176,7 +176,7 @@
|
||||||
className="w-full"
|
className="w-full"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="flex gap-2 w-full items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl {!fileUploadEnabled
|
class="flex gap-2 w-full items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800/50 rounded-xl {!fileUploadEnabled
|
||||||
? 'opacity-50'
|
? 'opacity-50'
|
||||||
: ''}"
|
: ''}"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
|
|
@ -207,7 +207,7 @@
|
||||||
className="w-full"
|
className="w-full"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="flex gap-2 w-full items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl {!fileUploadEnabled
|
class="flex gap-2 w-full items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800/50 rounded-xl {!fileUploadEnabled
|
||||||
? 'opacity-50'
|
? 'opacity-50'
|
||||||
: ''}"
|
: ''}"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
|
|
@ -238,7 +238,7 @@
|
||||||
className="w-full"
|
className="w-full"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="flex gap-2 w-full items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl {!fileUploadEnabled
|
class="flex gap-2 w-full items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800/50 rounded-xl {!fileUploadEnabled
|
||||||
? 'opacity-50'
|
? 'opacity-50'
|
||||||
: ''}"
|
: ''}"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
|
|
@ -263,7 +263,7 @@
|
||||||
{#if fileUploadEnabled}
|
{#if fileUploadEnabled}
|
||||||
{#if $config?.features?.enable_google_drive_integration}
|
{#if $config?.features?.enable_google_drive_integration}
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
|
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800/50 rounded-xl"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
uploadGoogleDriveHandler();
|
uploadGoogleDriveHandler();
|
||||||
}}
|
}}
|
||||||
|
|
@ -298,10 +298,10 @@
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if $config?.features?.enable_onedrive_integration}
|
{#if $config?.features?.enable_onedrive_integration && ($config?.features?.enable_onedrive_personal || $config?.features?.enable_onedrive_business)}
|
||||||
<DropdownMenu.Sub>
|
<DropdownMenu.Sub>
|
||||||
<DropdownMenu.SubTrigger
|
<DropdownMenu.SubTrigger
|
||||||
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl w-full"
|
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800/50 rounded-xl w-full"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
|
@ -396,25 +396,34 @@
|
||||||
sideOffset={$mobile ? 5 : 0}
|
sideOffset={$mobile ? 5 : 0}
|
||||||
alignOffset={$mobile ? 0 : -8}
|
alignOffset={$mobile ? 0 : -8}
|
||||||
>
|
>
|
||||||
|
{#if $config?.features?.enable_onedrive_personal}
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
|
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800/50 rounded-xl"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
uploadOneDriveHandler('personal');
|
uploadOneDriveHandler('personal');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<div class="flex flex-col">
|
||||||
<div class="line-clamp-1">{$i18n.t('Microsoft OneDrive (personal)')}</div>
|
<div class="line-clamp-1">{$i18n.t('Microsoft OneDrive (personal)')}</div>
|
||||||
|
</div>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if $config?.features?.enable_onedrive_business}
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
|
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800/50 rounded-xl"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
uploadOneDriveHandler('organizations');
|
uploadOneDriveHandler('organizations');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<div class="line-clamp-1">{$i18n.t('Microsoft OneDrive (work/school)')}</div>
|
<div class="line-clamp-1">
|
||||||
|
{$i18n.t('Microsoft OneDrive (work/school)')}
|
||||||
|
</div>
|
||||||
<div class="text-xs text-gray-500">{$i18n.t('Includes SharePoint')}</div>
|
<div class="text-xs text-gray-500">{$i18n.t('Includes SharePoint')}</div>
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
|
{/if}
|
||||||
</DropdownMenu.SubContent>
|
</DropdownMenu.SubContent>
|
||||||
</DropdownMenu.Sub>
|
</DropdownMenu.Sub>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -423,7 +432,7 @@
|
||||||
{:else if tab === 'knowledge'}
|
{:else if tab === 'knowledge'}
|
||||||
<div in:fly={{ x: 20, duration: 150 }}>
|
<div in:fly={{ x: 20, duration: 150 }}>
|
||||||
<button
|
<button
|
||||||
class="flex w-full justify-between gap-2 items-center px-3 py-1.5 text-sm cursor-pointer rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800"
|
class="flex w-full justify-between gap-2 items-center px-3 py-1.5 text-sm cursor-pointer rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800/50"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
tab = '';
|
tab = '';
|
||||||
}}
|
}}
|
||||||
|
|
@ -442,7 +451,7 @@
|
||||||
{:else if tab === 'notes'}
|
{:else if tab === 'notes'}
|
||||||
<div in:fly={{ x: 20, duration: 150 }}>
|
<div in:fly={{ x: 20, duration: 150 }}>
|
||||||
<button
|
<button
|
||||||
class="flex w-full justify-between gap-2 items-center px-3 py-1.5 text-sm cursor-pointer rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800"
|
class="flex w-full justify-between gap-2 items-center px-3 py-1.5 text-sm cursor-pointer rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800/50"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
tab = '';
|
tab = '';
|
||||||
}}
|
}}
|
||||||
|
|
@ -461,7 +470,7 @@
|
||||||
{:else if tab === 'chats'}
|
{:else if tab === 'chats'}
|
||||||
<div in:fly={{ x: 20, duration: 150 }}>
|
<div in:fly={{ x: 20, duration: 150 }}>
|
||||||
<button
|
<button
|
||||||
class="flex w-full justify-between gap-2 items-center px-3 py-1.5 text-sm cursor-pointer rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800"
|
class="flex w-full justify-between gap-2 items-center px-3 py-1.5 text-sm cursor-pointer rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800/50"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
tab = '';
|
tab = '';
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@
|
||||||
<div class=" self-center text-xs font-medium">
|
<div class=" self-center text-xs font-medium">
|
||||||
{variable}
|
{variable}
|
||||||
|
|
||||||
{#if variables[variable]?.required ?? true}
|
{#if variables[variable]?.required ?? false}
|
||||||
<span class=" text-gray-500">*{$i18n.t('required')}</span>
|
<span class=" text-gray-500">*{$i18n.t('required')}</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -134,7 +134,7 @@
|
||||||
placeholder={$i18n.t('Enter value (true/false)')}
|
placeholder={$i18n.t('Enter value (true/false)')}
|
||||||
bind:value={variableValues[variable]}
|
bind:value={variableValues[variable]}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
required
|
required={variables[variable]?.required ?? false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{:else if variables[variable]?.type === 'color'}
|
{:else if variables[variable]?.type === 'color'}
|
||||||
|
|
@ -159,7 +159,7 @@
|
||||||
placeholder={$i18n.t('Enter hex color (e.g. #FF0000)')}
|
placeholder={$i18n.t('Enter hex color (e.g. #FF0000)')}
|
||||||
bind:value={variableValues[variable]}
|
bind:value={variableValues[variable]}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
required
|
required={variables[variable]?.required ?? false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{:else if variables[variable]?.type === 'date'}
|
{:else if variables[variable]?.type === 'date'}
|
||||||
|
|
@ -170,7 +170,7 @@
|
||||||
bind:value={variableValues[variable]}
|
bind:value={variableValues[variable]}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
id="input-variable-{idx}"
|
id="input-variable-{idx}"
|
||||||
required
|
required={variables[variable]?.required ?? false}
|
||||||
{...variableAttributes}
|
{...variableAttributes}
|
||||||
/>
|
/>
|
||||||
{:else if variables[variable]?.type === 'datetime-local'}
|
{:else if variables[variable]?.type === 'datetime-local'}
|
||||||
|
|
@ -181,7 +181,7 @@
|
||||||
bind:value={variableValues[variable]}
|
bind:value={variableValues[variable]}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
id="input-variable-{idx}"
|
id="input-variable-{idx}"
|
||||||
required
|
required={variables[variable]?.required ?? false}
|
||||||
{...variableAttributes}
|
{...variableAttributes}
|
||||||
/>
|
/>
|
||||||
{:else if variables[variable]?.type === 'email'}
|
{:else if variables[variable]?.type === 'email'}
|
||||||
|
|
@ -192,7 +192,7 @@
|
||||||
bind:value={variableValues[variable]}
|
bind:value={variableValues[variable]}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
id="input-variable-{idx}"
|
id="input-variable-{idx}"
|
||||||
required
|
required={variables[variable]?.required ?? false}
|
||||||
{...variableAttributes}
|
{...variableAttributes}
|
||||||
/>
|
/>
|
||||||
{:else if variables[variable]?.type === 'month'}
|
{:else if variables[variable]?.type === 'month'}
|
||||||
|
|
@ -203,7 +203,7 @@
|
||||||
bind:value={variableValues[variable]}
|
bind:value={variableValues[variable]}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
id="input-variable-{idx}"
|
id="input-variable-{idx}"
|
||||||
required
|
required={variables[variable]?.required ?? false}
|
||||||
{...variableAttributes}
|
{...variableAttributes}
|
||||||
/>
|
/>
|
||||||
{:else if variables[variable]?.type === 'number'}
|
{:else if variables[variable]?.type === 'number'}
|
||||||
|
|
@ -214,7 +214,7 @@
|
||||||
bind:value={variableValues[variable]}
|
bind:value={variableValues[variable]}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
id="input-variable-{idx}"
|
id="input-variable-{idx}"
|
||||||
required
|
required={variables[variable]?.required ?? false}
|
||||||
{...variableAttributes}
|
{...variableAttributes}
|
||||||
/>
|
/>
|
||||||
{:else if variables[variable]?.type === 'range'}
|
{:else if variables[variable]?.type === 'range'}
|
||||||
|
|
@ -235,7 +235,7 @@
|
||||||
placeholder={$i18n.t('Enter value')}
|
placeholder={$i18n.t('Enter value')}
|
||||||
bind:value={variableValues[variable]}
|
bind:value={variableValues[variable]}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
required
|
required={variables[variable]?.required ?? false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -256,7 +256,7 @@
|
||||||
bind:value={variableValues[variable]}
|
bind:value={variableValues[variable]}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
id="input-variable-{idx}"
|
id="input-variable-{idx}"
|
||||||
required
|
required={variables[variable]?.required ?? false}
|
||||||
{...variableAttributes}
|
{...variableAttributes}
|
||||||
/>
|
/>
|
||||||
{:else if variables[variable]?.type === 'text'}
|
{:else if variables[variable]?.type === 'text'}
|
||||||
|
|
@ -267,7 +267,7 @@
|
||||||
bind:value={variableValues[variable]}
|
bind:value={variableValues[variable]}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
id="input-variable-{idx}"
|
id="input-variable-{idx}"
|
||||||
required
|
required={variables[variable]?.required ?? false}
|
||||||
{...variableAttributes}
|
{...variableAttributes}
|
||||||
/>
|
/>
|
||||||
{:else if variables[variable]?.type === 'time'}
|
{:else if variables[variable]?.type === 'time'}
|
||||||
|
|
@ -278,7 +278,7 @@
|
||||||
bind:value={variableValues[variable]}
|
bind:value={variableValues[variable]}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
id="input-variable-{idx}"
|
id="input-variable-{idx}"
|
||||||
required
|
required={variables[variable]?.required ?? false}
|
||||||
{...variableAttributes}
|
{...variableAttributes}
|
||||||
/>
|
/>
|
||||||
{:else if variables[variable]?.type === 'url'}
|
{:else if variables[variable]?.type === 'url'}
|
||||||
|
|
@ -289,7 +289,7 @@
|
||||||
bind:value={variableValues[variable]}
|
bind:value={variableValues[variable]}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
id="input-variable-{idx}"
|
id="input-variable-{idx}"
|
||||||
required
|
required={variables[variable]?.required ?? false}
|
||||||
{...variableAttributes}
|
{...variableAttributes}
|
||||||
/>
|
/>
|
||||||
{:else if variables[variable]?.type === 'map'}
|
{:else if variables[variable]?.type === 'map'}
|
||||||
|
|
@ -311,7 +311,7 @@
|
||||||
placeholder={$i18n.t('Enter coordinates (e.g. 51.505, -0.09)')}
|
placeholder={$i18n.t('Enter coordinates (e.g. 51.505, -0.09)')}
|
||||||
bind:value={variableValues[variable]}
|
bind:value={variableValues[variable]}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
required
|
required={variables[variable]?.required ?? false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
|
|
@ -321,7 +321,7 @@
|
||||||
bind:value={variableValues[variable]}
|
bind:value={variableValues[variable]}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
id="input-variable-{idx}"
|
id="input-variable-{idx}"
|
||||||
required
|
required={variables[variable]?.required ?? false}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
import { fly } from 'svelte/transition';
|
import { fly } from 'svelte/transition';
|
||||||
import { flyAndScale } from '$lib/utils/transitions';
|
import { flyAndScale } from '$lib/utils/transitions';
|
||||||
|
|
||||||
import { config, user, tools as _tools, mobile, settings } from '$lib/stores';
|
import { config, user, tools as _tools, mobile, settings, toolServers } from '$lib/stores';
|
||||||
|
|
||||||
import { getTools } from '$lib/apis/tools';
|
import { getTools } from '$lib/apis/tools';
|
||||||
|
|
||||||
|
|
@ -55,7 +55,10 @@
|
||||||
($user?.role === 'admin' || $user?.permissions?.chat?.file_upload);
|
($user?.role === 'admin' || $user?.permissions?.chat?.file_upload);
|
||||||
|
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
|
if ($_tools === null) {
|
||||||
await _tools.set(await getTools(localStorage.token));
|
await _tools.set(await getTools(localStorage.token));
|
||||||
|
}
|
||||||
|
|
||||||
if ($_tools) {
|
if ($_tools) {
|
||||||
tools = $_tools.reduce((a, tool, i, arr) => {
|
tools = $_tools.reduce((a, tool, i, arr) => {
|
||||||
a[tool.id] = {
|
a[tool.id] = {
|
||||||
|
|
@ -65,8 +68,22 @@
|
||||||
};
|
};
|
||||||
return a;
|
return a;
|
||||||
}, {});
|
}, {});
|
||||||
selectedToolIds = selectedToolIds.filter((id) => $_tools?.some((tool) => tool.id === id));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($toolServers) {
|
||||||
|
for (const serverIdx in $toolServers) {
|
||||||
|
const server = $toolServers[serverIdx];
|
||||||
|
if (server.info) {
|
||||||
|
tools[`direct_server:${serverIdx}`] = {
|
||||||
|
name: server?.info?.title ?? server.url,
|
||||||
|
description: server.info.description ?? '',
|
||||||
|
enabled: selectedToolIds.includes(`direct_server:${serverIdx}`)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedToolIds = selectedToolIds.filter((id) => Object.keys(tools).includes(id));
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -95,7 +112,7 @@
|
||||||
{#if tools}
|
{#if tools}
|
||||||
{#if Object.keys(tools).length > 0}
|
{#if Object.keys(tools).length > 0}
|
||||||
<button
|
<button
|
||||||
class="flex w-full justify-between gap-2 items-center px-3 py-1.5 text-sm cursor-pointer rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800"
|
class="flex w-full justify-between gap-2 items-center px-3 py-1.5 text-sm cursor-pointer rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800/50"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
tab = 'tools';
|
tab = 'tools';
|
||||||
}}
|
}}
|
||||||
|
|
@ -124,7 +141,7 @@
|
||||||
{#each toggleFilters.sort( (a, b) => a.name.localeCompare( b.name, undefined, { sensitivity: 'base' } ) ) as filter, filterIdx (filter.id)}
|
{#each toggleFilters.sort( (a, b) => a.name.localeCompare( b.name, undefined, { sensitivity: 'base' } ) ) as filter, filterIdx (filter.id)}
|
||||||
<Tooltip content={filter?.description} placement="top-start">
|
<Tooltip content={filter?.description} placement="top-start">
|
||||||
<button
|
<button
|
||||||
class="flex w-full justify-between gap-2 items-center px-3 py-1.5 text-sm cursor-pointer rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800"
|
class="flex w-full justify-between gap-2 items-center px-3 py-1.5 text-sm cursor-pointer rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800/50"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
if (selectedFilterIds.includes(filter.id)) {
|
if (selectedFilterIds.includes(filter.id)) {
|
||||||
selectedFilterIds = selectedFilterIds.filter((id) => id !== filter.id);
|
selectedFilterIds = selectedFilterIds.filter((id) => id !== filter.id);
|
||||||
|
|
@ -173,7 +190,7 @@
|
||||||
{#if showWebSearchButton}
|
{#if showWebSearchButton}
|
||||||
<Tooltip content={$i18n.t('Search the internet')} placement="top-start">
|
<Tooltip content={$i18n.t('Search the internet')} placement="top-start">
|
||||||
<button
|
<button
|
||||||
class="flex w-full justify-between gap-2 items-center px-3 py-1.5 text-sm cursor-pointer rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800"
|
class="flex w-full justify-between gap-2 items-center px-3 py-1.5 text-sm cursor-pointer rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800/50"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
webSearchEnabled = !webSearchEnabled;
|
webSearchEnabled = !webSearchEnabled;
|
||||||
}}
|
}}
|
||||||
|
|
@ -204,7 +221,7 @@
|
||||||
{#if showImageGenerationButton}
|
{#if showImageGenerationButton}
|
||||||
<Tooltip content={$i18n.t('Generate an image')} placement="top-start">
|
<Tooltip content={$i18n.t('Generate an image')} placement="top-start">
|
||||||
<button
|
<button
|
||||||
class="flex w-full justify-between gap-2 items-center px-3 py-1.5 text-sm cursor-pointer rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800"
|
class="flex w-full justify-between gap-2 items-center px-3 py-1.5 text-sm cursor-pointer rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800/50"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
imageGenerationEnabled = !imageGenerationEnabled;
|
imageGenerationEnabled = !imageGenerationEnabled;
|
||||||
}}
|
}}
|
||||||
|
|
@ -235,7 +252,7 @@
|
||||||
{#if showCodeInterpreterButton}
|
{#if showCodeInterpreterButton}
|
||||||
<Tooltip content={$i18n.t('Execute code for analysis')} placement="top-start">
|
<Tooltip content={$i18n.t('Execute code for analysis')} placement="top-start">
|
||||||
<button
|
<button
|
||||||
class="flex w-full justify-between gap-2 items-center px-3 py-1.5 text-sm cursor-pointer rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800"
|
class="flex w-full justify-between gap-2 items-center px-3 py-1.5 text-sm cursor-pointer rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800/50"
|
||||||
aria-pressed={codeInterpreterEnabled}
|
aria-pressed={codeInterpreterEnabled}
|
||||||
aria-label={codeInterpreterEnabled
|
aria-label={codeInterpreterEnabled
|
||||||
? $i18n.t('Disable Code Interpreter')
|
? $i18n.t('Disable Code Interpreter')
|
||||||
|
|
@ -270,7 +287,7 @@
|
||||||
{:else if tab === 'tools' && tools}
|
{:else if tab === 'tools' && tools}
|
||||||
<div in:fly={{ x: 20, duration: 150 }}>
|
<div in:fly={{ x: 20, duration: 150 }}>
|
||||||
<button
|
<button
|
||||||
class="flex w-full justify-between gap-2 items-center px-3 py-1.5 text-sm cursor-pointer rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800"
|
class="flex w-full justify-between gap-2 items-center px-3 py-1.5 text-sm cursor-pointer rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800/50"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
tab = '';
|
tab = '';
|
||||||
}}
|
}}
|
||||||
|
|
@ -287,7 +304,7 @@
|
||||||
|
|
||||||
{#each Object.keys(tools) as toolId}
|
{#each Object.keys(tools) as toolId}
|
||||||
<button
|
<button
|
||||||
class="flex w-full justify-between gap-2 items-center px-3 py-1.5 text-sm cursor-pointer rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800"
|
class="flex w-full justify-between gap-2 items-center px-3 py-1.5 text-sm cursor-pointer rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800/50"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
tools[toolId].enabled = !tools[toolId].enabled;
|
tools[toolId].enabled = !tools[toolId].enabled;
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -454,7 +454,7 @@
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
<div class="pb-12" />
|
<div class="pb-18" />
|
||||||
{#if bottomPadding}
|
{#if bottomPadding}
|
||||||
<div class=" pb-6" />
|
<div class=" pb-6" />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -152,7 +152,7 @@
|
||||||
{#each citations as citation, idx}
|
{#each citations as citation, idx}
|
||||||
<button
|
<button
|
||||||
id={`source-${id}-${idx + 1}`}
|
id={`source-${id}-${idx + 1}`}
|
||||||
class="no-toggle outline-hidden flex dark:text-gray-300 bg-white dark:bg-gray-900 text-gray-600 rounded-xl gap-1.5 items-center"
|
class="no-toggle outline-hidden flex dark:text-gray-300 bg-transparent text-gray-600 rounded-xl gap-1.5 items-center"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
showCitationModal = true;
|
showCitationModal = true;
|
||||||
selectedCitation = citation;
|
selectedCitation = citation;
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,12 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import hljs from 'highlight.js';
|
import hljs from 'highlight.js';
|
||||||
|
|
||||||
import mermaid from 'mermaid';
|
|
||||||
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
|
|
||||||
import { getContext, onMount, tick, onDestroy } from 'svelte';
|
import { getContext, onMount, tick, onDestroy } from 'svelte';
|
||||||
import { copyToClipboard } from '$lib/utils';
|
import { copyToClipboard, renderMermaidDiagram } from '$lib/utils';
|
||||||
|
|
||||||
import 'highlight.js/styles/github-dark.min.css';
|
import 'highlight.js/styles/github-dark.min.css';
|
||||||
|
|
||||||
import PyodideWorker from '$lib/workers/pyodide.worker?worker';
|
import PyodideWorker from '$lib/workers/pyodide.worker?worker';
|
||||||
import CodeEditor from '$lib/components/common/CodeEditor.svelte';
|
|
||||||
import SvgPanZoom from '$lib/components/common/SVGPanZoom.svelte';
|
import SvgPanZoom from '$lib/components/common/SVGPanZoom.svelte';
|
||||||
import { config } from '$lib/stores';
|
import { config } from '$lib/stores';
|
||||||
import { executeCode } from '$lib/apis/utils';
|
import { executeCode } from '$lib/apis/utils';
|
||||||
|
|
@ -40,7 +35,7 @@
|
||||||
export let code = '';
|
export let code = '';
|
||||||
export let attributes = {};
|
export let attributes = {};
|
||||||
|
|
||||||
export let className = 'my-2';
|
export let className = 'mb-2';
|
||||||
export let editorClassName = '';
|
export let editorClassName = '';
|
||||||
export let stickyButtonsClassName = 'top-0';
|
export let stickyButtonsClassName = 'top-0';
|
||||||
|
|
||||||
|
|
@ -167,9 +162,9 @@
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stdout.startsWith(`${line}\n`)) {
|
if (stdout.includes(`${line}\n`)) {
|
||||||
stdout = stdout.replace(`${line}\n`, ``);
|
stdout = stdout.replace(`${line}\n`, ``);
|
||||||
} else if (stdout.startsWith(`${line}`)) {
|
} else if (stdout.includes(`${line}`)) {
|
||||||
stdout = stdout.replace(`${line}`, ``);
|
stdout = stdout.replace(`${line}`, ``);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -196,9 +191,9 @@
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.startsWith(`${line}\n`)) {
|
if (result.includes(`${line}\n`)) {
|
||||||
result = result.replace(`${line}\n`, ``);
|
result = result.replace(`${line}\n`, ``);
|
||||||
} else if (result.startsWith(`${line}`)) {
|
} else if (result.includes(`${line}`)) {
|
||||||
result = result.replace(`${line}`, ``);
|
result = result.replace(`${line}`, ``);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -275,9 +270,9 @@
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stdout.startsWith(`${line}\n`)) {
|
if (stdout.includes(`${line}\n`)) {
|
||||||
stdout = stdout.replace(`${line}\n`, ``);
|
stdout = stdout.replace(`${line}\n`, ``);
|
||||||
} else if (stdout.startsWith(`${line}`)) {
|
} else if (stdout.includes(`${line}`)) {
|
||||||
stdout = stdout.replace(`${line}`, ``);
|
stdout = stdout.replace(`${line}`, ``);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -325,27 +320,11 @@
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
let debounceTimeout;
|
|
||||||
|
|
||||||
const drawMermaidDiagram = async () => {
|
|
||||||
try {
|
|
||||||
if (await mermaid.parse(code)) {
|
|
||||||
const { svg } = await mermaid.render(`mermaid-${uuidv4()}`, code);
|
|
||||||
mermaidHtml = svg;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log('Error:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const render = async () => {
|
const render = async () => {
|
||||||
if (lang === 'mermaid' && (token?.raw ?? '').slice(-4).includes('```')) {
|
|
||||||
(async () => {
|
|
||||||
await drawMermaidDiagram();
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
|
|
||||||
onUpdate(token);
|
onUpdate(token);
|
||||||
|
if (lang === 'mermaid' && (token?.raw ?? '').slice(-4).includes('```')) {
|
||||||
|
mermaidHtml = await renderMermaidDiagram(code);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
$: if (token) {
|
$: if (token) {
|
||||||
|
|
@ -392,20 +371,6 @@
|
||||||
if (token) {
|
if (token) {
|
||||||
onUpdate(token);
|
onUpdate(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (document.documentElement.classList.contains('dark')) {
|
|
||||||
mermaid.initialize({
|
|
||||||
startOnLoad: true,
|
|
||||||
theme: 'dark',
|
|
||||||
securityLevel: 'loose'
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
mermaid.initialize({
|
|
||||||
startOnLoad: true,
|
|
||||||
theme: 'default',
|
|
||||||
securityLevel: 'loose'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
|
|
@ -416,11 +381,14 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="relative {className} flex flex-col rounded-xl pt-2" dir="ltr">
|
<div
|
||||||
|
class="relative {className} flex flex-col rounded-3xl border border-gray-100 dark:border-gray-850 my-0.5"
|
||||||
|
dir="ltr"
|
||||||
|
>
|
||||||
{#if lang === 'mermaid'}
|
{#if lang === 'mermaid'}
|
||||||
{#if mermaidHtml}
|
{#if mermaidHtml}
|
||||||
<SvgPanZoom
|
<SvgPanZoom
|
||||||
className=" border border-gray-100 dark:border-gray-850 rounded-xl max-h-fit overflow-hidden"
|
className=" rounded-3xl max-h-fit overflow-hidden"
|
||||||
svg={mermaidHtml}
|
svg={mermaidHtml}
|
||||||
content={_token.text}
|
content={_token.text}
|
||||||
/>
|
/>
|
||||||
|
|
@ -428,16 +396,18 @@
|
||||||
<pre class="mermaid">{code}</pre>
|
<pre class="mermaid">{code}</pre>
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<div class="text-text-300 absolute pl-4 text-xs font-medium dark:text-white">
|
<div
|
||||||
|
class="absolute left-0 right-0 py-2.5 pr-3 text-text-300 pl-4.5 text-xs font-medium dark:text-white"
|
||||||
|
>
|
||||||
{lang}
|
{lang}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="sticky {stickyButtonsClassName} mb-1 pr-2.5 flex items-center justify-end z-10 text-xs text-black dark:text-white"
|
class="sticky {stickyButtonsClassName} left-0 right-0 py-2 pr-3 flex items-center justify-end w-full z-10 text-xs text-black dark:text-white"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-0.5">
|
<div class="flex items-center gap-0.5">
|
||||||
<button
|
<button
|
||||||
class="flex gap-1 items-center bg-none border-none bg-gray-50 dark:bg-black transition rounded-md px-1.5 py-0.5"
|
class="flex gap-1 items-center bg-none border-none transition rounded-md px-1.5 py-0.5 bg-white dark:bg-black"
|
||||||
on:click={collapseCodeBlock}
|
on:click={collapseCodeBlock}
|
||||||
>
|
>
|
||||||
<div class=" -translate-y-[0.5px]">
|
<div class=" -translate-y-[0.5px]">
|
||||||
|
|
@ -449,39 +419,22 @@
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{#if preview && ['html', 'svg'].includes(lang)}
|
|
||||||
<button
|
|
||||||
class="flex gap-1 items-center run-code-button bg-none border-none bg-gray-50 dark:bg-black transition rounded-md px-1.5 py-0.5"
|
|
||||||
on:click={previewCode}
|
|
||||||
>
|
|
||||||
<div class=" -translate-y-[0.5px]">
|
|
||||||
<Cube className="size-3" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
{$i18n.t('Preview')}
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if ($config?.features?.enable_code_execution ?? true) && (lang.toLowerCase() === 'python' || lang.toLowerCase() === 'py' || (lang === '' && checkPythonCode(code)))}
|
{#if ($config?.features?.enable_code_execution ?? true) && (lang.toLowerCase() === 'python' || lang.toLowerCase() === 'py' || (lang === '' && checkPythonCode(code)))}
|
||||||
{#if executing}
|
{#if executing}
|
||||||
<div class="run-code-button bg-none border-none p-1 cursor-not-allowed">
|
<div
|
||||||
|
class="run-code-button bg-none border-none p-0.5 cursor-not-allowed bg-white dark:bg-black"
|
||||||
|
>
|
||||||
{$i18n.t('Running')}
|
{$i18n.t('Running')}
|
||||||
</div>
|
</div>
|
||||||
{:else if run}
|
{:else if run}
|
||||||
<button
|
<button
|
||||||
class="flex gap-1 items-center run-code-button bg-none border-none bg-gray-50 dark:bg-black transition rounded-md px-1.5 py-0.5"
|
class="flex gap-1 items-center run-code-button bg-none border-none transition rounded-md px-1.5 py-0.5 bg-white dark:bg-black"
|
||||||
on:click={async () => {
|
on:click={async () => {
|
||||||
code = _code;
|
code = _code;
|
||||||
await tick();
|
await tick();
|
||||||
executePython(code);
|
executePython(code);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class=" -translate-y-[0.5px]">
|
|
||||||
<CommandLine className="size-3" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{$i18n.t('Run')}
|
{$i18n.t('Run')}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -491,7 +444,7 @@
|
||||||
|
|
||||||
{#if save}
|
{#if save}
|
||||||
<button
|
<button
|
||||||
class="save-code-button bg-none border-none bg-gray-50 dark:bg-black transition rounded-md px-1.5 py-0.5"
|
class="save-code-button bg-none border-none transition rounded-md px-1.5 py-0.5 bg-white dark:bg-black"
|
||||||
on:click={saveCode}
|
on:click={saveCode}
|
||||||
>
|
>
|
||||||
{saved ? $i18n.t('Saved') : $i18n.t('Save')}
|
{saved ? $i18n.t('Saved') : $i18n.t('Save')}
|
||||||
|
|
@ -499,23 +452,35 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="copy-code-button bg-none border-none bg-gray-50 dark:bg-black transition rounded-md px-1.5 py-0.5"
|
class="copy-code-button bg-none border-none transition rounded-md px-1.5 py-0.5 bg-white dark:bg-black"
|
||||||
on:click={copyCode}>{copied ? $i18n.t('Copied') : $i18n.t('Copy')}</button
|
on:click={copyCode}>{copied ? $i18n.t('Copied') : $i18n.t('Copy')}</button
|
||||||
>
|
>
|
||||||
|
|
||||||
|
{#if preview && ['html', 'svg'].includes(lang)}
|
||||||
|
<button
|
||||||
|
class="flex gap-1 items-center run-code-button bg-none border-none transition rounded-md px-1.5 py-0.5 bg-white dark:bg-black"
|
||||||
|
on:click={previewCode}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
{$i18n.t('Preview')}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="language-{lang} rounded-t-xl -mt-8 {editorClassName
|
class="language-{lang} rounded-t-3xl -mt-9 {editorClassName
|
||||||
? editorClassName
|
? editorClassName
|
||||||
: executing || stdout || stderr || result
|
: executing || stdout || stderr || result
|
||||||
? ''
|
? ''
|
||||||
: 'rounded-b-xl'} overflow-hidden"
|
: 'rounded-b-3xl'} overflow-hidden"
|
||||||
>
|
>
|
||||||
<div class=" pt-8 bg-gray-50 dark:bg-black"></div>
|
<div class=" pt-8 bg-white dark:bg-black"></div>
|
||||||
|
|
||||||
{#if !collapsed}
|
{#if !collapsed}
|
||||||
{#if edit}
|
{#if edit}
|
||||||
|
{#await import('$lib/components/common/CodeEditor.svelte') then { default: CodeEditor }}
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
value={code}
|
value={code}
|
||||||
{id}
|
{id}
|
||||||
|
|
@ -527,6 +492,7 @@
|
||||||
_code = value;
|
_code = value;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{/await}
|
||||||
{:else}
|
{:else}
|
||||||
<pre
|
<pre
|
||||||
class=" hljs p-4 px-5 overflow-x-auto"
|
class=" hljs p-4 px-5 overflow-x-auto"
|
||||||
|
|
@ -542,7 +508,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<div
|
<div
|
||||||
class="bg-gray-50 dark:bg-black dark:text-white rounded-b-xl! pt-2 pb-2 px-4 flex flex-col gap-2 text-xs"
|
class="bg-white dark:bg-black dark:text-white rounded-b-3xl! pt-0.5 pb-3 px-4 flex flex-col gap-2 text-xs"
|
||||||
>
|
>
|
||||||
<span class="text-gray-500 italic">
|
<span class="text-gray-500 italic">
|
||||||
{$i18n.t('{{COUNT}} hidden lines', {
|
{$i18n.t('{{COUNT}} hidden lines', {
|
||||||
|
|
@ -561,7 +527,7 @@
|
||||||
|
|
||||||
{#if executing || stdout || stderr || result || files}
|
{#if executing || stdout || stderr || result || files}
|
||||||
<div
|
<div
|
||||||
class="bg-gray-50 dark:bg-black dark:text-white rounded-b-xl! py-4 px-4 flex flex-col gap-2"
|
class="bg-gray-50 dark:bg-black dark:text-white rounded-b-3xl! py-4 px-4 flex flex-col gap-2"
|
||||||
>
|
>
|
||||||
{#if executing}
|
{#if executing}
|
||||||
<div class=" ">
|
<div class=" ">
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,9 @@
|
||||||
|
|
||||||
marked.use(markedKatexExtension(options));
|
marked.use(markedKatexExtension(options));
|
||||||
marked.use(markedExtension(options));
|
marked.use(markedExtension(options));
|
||||||
marked.use({ extensions: [mentionExtension({ triggerChar: '@' })] });
|
marked.use({
|
||||||
|
extensions: [mentionExtension({ triggerChar: '@' }), mentionExtension({ triggerChar: '#' })]
|
||||||
|
});
|
||||||
|
|
||||||
$: (async () => {
|
$: (async () => {
|
||||||
if (content) {
|
if (content) {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,22 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import katex from 'katex';
|
import type { renderToString as katexRenderToString } from 'katex';
|
||||||
import 'katex/contrib/mhchem';
|
import { onMount } from 'svelte';
|
||||||
import 'katex/dist/katex.min.css';
|
|
||||||
|
|
||||||
export let content: string;
|
export let content: string;
|
||||||
export let displayMode: boolean = false;
|
export let displayMode: boolean = false;
|
||||||
|
|
||||||
|
let renderToString: typeof katexRenderToString | null = null;
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
const [katex] = await Promise.all([
|
||||||
|
import('katex'),
|
||||||
|
import('katex/contrib/mhchem'),
|
||||||
|
import('katex/dist/katex.min.css')
|
||||||
|
]);
|
||||||
|
renderToString = katex.renderToString;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{@html katex.renderToString(content, { displayMode, throwOnError: false })}
|
{#if renderToString}
|
||||||
|
{@html renderToString(content, { displayMode, throwOnError: false })}
|
||||||
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,108 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Token } from 'marked';
|
import type { Token } from 'marked';
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
import { LinkPreview } from 'bits-ui';
|
||||||
|
|
||||||
|
import { getContext } from 'svelte';
|
||||||
|
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { channels, models } from '$lib/stores';
|
||||||
|
import UserStatus from '$lib/components/channel/Messages/Message/UserStatus.svelte';
|
||||||
|
import UserStatusLinkPreview from '$lib/components/channel/Messages/Message/UserStatusLinkPreview.svelte';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
export let token: Token;
|
export let token: Token;
|
||||||
|
|
||||||
|
let triggerChar = '';
|
||||||
|
let label = '';
|
||||||
|
|
||||||
|
let idType = null;
|
||||||
|
let id = '';
|
||||||
|
|
||||||
|
$: if (token) {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
const init = () => {
|
||||||
|
const _id = token?.id;
|
||||||
|
// split by : and take first part as idType and second part as id
|
||||||
|
|
||||||
|
const parts = _id?.split(':');
|
||||||
|
if (parts) {
|
||||||
|
idType = parts[0];
|
||||||
|
id = parts.slice(1).join(':'); // in case id contains ':'
|
||||||
|
} else {
|
||||||
|
idType = null;
|
||||||
|
id = _id;
|
||||||
|
}
|
||||||
|
|
||||||
|
label = token?.label ?? id;
|
||||||
|
triggerChar = token?.triggerChar ?? '@';
|
||||||
|
|
||||||
|
if (triggerChar === '#') {
|
||||||
|
if (idType === 'C') {
|
||||||
|
// Channel
|
||||||
|
const channel = $channels.find((c) => c.id === id);
|
||||||
|
if (channel) {
|
||||||
|
label = channel.name;
|
||||||
|
} else {
|
||||||
|
label = $i18n.t('Unknown');
|
||||||
|
}
|
||||||
|
} else if (idType === 'T') {
|
||||||
|
// Thread
|
||||||
|
}
|
||||||
|
} else if (triggerChar === '@') {
|
||||||
|
if (idType === 'U') {
|
||||||
|
// User
|
||||||
|
} else if (idType === 'M') {
|
||||||
|
// Model
|
||||||
|
const model = $models.find((m) => m.id === id);
|
||||||
|
if (model) {
|
||||||
|
label = model.name;
|
||||||
|
} else {
|
||||||
|
label = $i18n.t('Unknown');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Tooltip as="span" className="mention" content={token.id} placement="top">
|
<LinkPreview.Root openDelay={0} closeDelay={0}>
|
||||||
{token?.triggerChar ?? '@'}{token?.label ?? token?.id}
|
<LinkPreview.Trigger class=" cursor-pointer no-underline! font-normal! ">
|
||||||
</Tooltip>
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
|
|
||||||
|
<span
|
||||||
|
class="mention"
|
||||||
|
on:click={async () => {
|
||||||
|
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}
|
||||||
|
</span>
|
||||||
|
</LinkPreview.Trigger>
|
||||||
|
|
||||||
|
{#if triggerChar === '@' && idType === 'U'}
|
||||||
|
<UserStatusLinkPreview {id} />
|
||||||
|
{/if}
|
||||||
|
</LinkPreview.Root>
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
import AlertRenderer, { alertComponent } from './AlertRenderer.svelte';
|
import AlertRenderer, { alertComponent } from './AlertRenderer.svelte';
|
||||||
import Collapsible from '$lib/components/common/Collapsible.svelte';
|
import Collapsible from '$lib/components/common/Collapsible.svelte';
|
||||||
import Tooltip from '$lib/components/common/Tooltip.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 Source from './Source.svelte';
|
||||||
import { settings } from '$lib/stores';
|
import { settings } from '$lib/stores';
|
||||||
|
|
@ -109,7 +109,7 @@
|
||||||
{save}
|
{save}
|
||||||
{preview}
|
{preview}
|
||||||
edit={editCodeBlock}
|
edit={editCodeBlock}
|
||||||
stickyButtonsClassName={topPadding ? 'top-8' : 'top-0'}
|
stickyButtonsClassName={topPadding ? 'top-7' : 'top-0'}
|
||||||
onSave={(value) => {
|
onSave={(value) => {
|
||||||
onSave({
|
onSave({
|
||||||
raw: token.raw,
|
raw: token.raw,
|
||||||
|
|
@ -124,19 +124,19 @@
|
||||||
{token.text}
|
{token.text}
|
||||||
{/if}
|
{/if}
|
||||||
{:else if token.type === 'table'}
|
{:else if token.type === 'table'}
|
||||||
<div class="relative w-full group">
|
<div class="relative w-full group mb-2">
|
||||||
<div class="scrollbar-hidden relative overflow-x-auto max-w-full rounded-lg">
|
<div class="scrollbar-hidden relative overflow-x-auto max-w-full">
|
||||||
<table
|
<table
|
||||||
class=" w-full text-sm text-left text-gray-500 dark:text-gray-400 max-w-full rounded-xl"
|
class=" w-full text-sm text-left text-gray-500 dark:text-gray-400 max-w-full rounded-xl"
|
||||||
>
|
>
|
||||||
<thead
|
<thead
|
||||||
class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-850 dark:text-gray-400 border-none"
|
class="text-xs text-gray-700 uppercase bg-white dark:bg-gray-900 dark:text-gray-400 border-none"
|
||||||
>
|
>
|
||||||
<tr class="">
|
<tr class="">
|
||||||
{#each token.header as header, headerIdx}
|
{#each token.header as header, headerIdx}
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
class="px-3! py-1.5! cursor-pointer border border-gray-100 dark:border-gray-850"
|
class="px-2.5! py-2! cursor-pointer border-b border-gray-100! dark:border-gray-800!"
|
||||||
style={token.align[headerIdx] ? '' : `text-align: ${token.align[headerIdx]}`}
|
style={token.align[headerIdx] ? '' : `text-align: ${token.align[headerIdx]}`}
|
||||||
>
|
>
|
||||||
<div class="gap-1.5 text-left">
|
<div class="gap-1.5 text-left">
|
||||||
|
|
@ -155,10 +155,14 @@
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each token.rows as row, rowIdx}
|
{#each token.rows as row, rowIdx}
|
||||||
<tr class="bg-white dark:bg-gray-900 dark:border-gray-850 text-xs">
|
<tr class="bg-white dark:bg-gray-900 text-xs">
|
||||||
{#each row ?? [] as cell, cellIdx}
|
{#each row ?? [] as cell, cellIdx}
|
||||||
<td
|
<td
|
||||||
class="px-3! py-1.5! text-gray-900 dark:text-white w-max border border-gray-100 dark:border-gray-850"
|
class="px-3! py-2! text-gray-900 dark:text-white w-max {token.rows.length -
|
||||||
|
1 ===
|
||||||
|
rowIdx
|
||||||
|
? ''
|
||||||
|
: 'border-b border-gray-50! dark:border-gray-850!'}"
|
||||||
style={token.align[cellIdx] ? `text-align: ${token.align[cellIdx]}` : ''}
|
style={token.align[cellIdx] ? `text-align: ${token.align[cellIdx]}` : ''}
|
||||||
>
|
>
|
||||||
<div class="break-normal">
|
<div class="break-normal">
|
||||||
|
|
@ -186,7 +190,7 @@
|
||||||
exportTableToCSVHandler(token, tokenIdx);
|
exportTableToCSVHandler(token, tokenIdx);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ArrowDownTray className=" size-3.5" strokeWidth="1.5" />
|
<Download className=" size-3.5" strokeWidth="1.5" />
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@
|
||||||
|
|
||||||
<div slot="content">
|
<div slot="content">
|
||||||
<DropdownMenu.Content
|
<DropdownMenu.Content
|
||||||
class="w-full max-w-[190px] rounded-xl px-1 py-1 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg"
|
class="w-full max-w-[200px] rounded-2xl px-1 py-1 border border-gray-100 dark:border-gray-850 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg transition"
|
||||||
sideOffset={-2}
|
sideOffset={-2}
|
||||||
side="bottom"
|
side="bottom"
|
||||||
align="start"
|
align="start"
|
||||||
|
|
@ -78,7 +78,7 @@
|
||||||
</div>
|
</div>
|
||||||
<hr class="border-gray-50 dark:border-gray-800 my-1 mx-2" />
|
<hr class="border-gray-50 dark:border-gray-800 my-1 mx-2" />
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
class="flex gap-2 items-center px-3 py-1.5 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-lg"
|
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
onRegenerate();
|
onRegenerate();
|
||||||
show = false;
|
show = false;
|
||||||
|
|
@ -103,7 +103,7 @@
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
|
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
class="flex gap-2 items-center px-3 py-1.5 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-lg"
|
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
onRegenerate($i18n.t('Add Details'));
|
onRegenerate($i18n.t('Add Details'));
|
||||||
}}
|
}}
|
||||||
|
|
@ -113,7 +113,7 @@
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
|
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
class="flex gap-2 items-center px-3 py-1.5 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-lg"
|
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
onRegenerate($i18n.t('More Concise'));
|
onRegenerate($i18n.t('More Concise'));
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,21 @@
|
||||||
export let size = 'md';
|
export let size = 'md';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<span class="relative flex {size === 'md' ? 'size-3 my-2' : 'size-2 my-1'} mx-1">
|
<span
|
||||||
|
class="relative flex {size === 'md'
|
||||||
|
? 'size-3 my-2'
|
||||||
|
: size === 'xs'
|
||||||
|
? 'size-1.5 my-1'
|
||||||
|
: 'size-2 my-1'} mx-1"
|
||||||
|
>
|
||||||
<span
|
<span
|
||||||
class="absolute inline-flex h-full w-full animate-pulse rounded-full bg-gray-700 dark:bg-gray-200 opacity-75"
|
class="absolute inline-flex h-full w-full animate-pulse rounded-full bg-gray-700 dark:bg-gray-200 opacity-75"
|
||||||
></span>
|
></span>
|
||||||
<span
|
<span
|
||||||
class="relative inline-flex {size === 'md'
|
class="relative inline-flex {size === 'md'
|
||||||
? 'size-3'
|
? 'size-3'
|
||||||
|
: size === 'xs'
|
||||||
|
? 'size-1.5'
|
||||||
: 'size-2'} rounded-full bg-black dark:bg-white animate-size"
|
: 'size-2'} rounded-full bg-black dark:bg-white animate-size"
|
||||||
></span>
|
></span>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@
|
||||||
<button
|
<button
|
||||||
aria-roledescription="model-item"
|
aria-roledescription="model-item"
|
||||||
aria-label={item.label}
|
aria-label={item.label}
|
||||||
class="flex group/item w-full text-left font-medium line-clamp-1 select-none items-center rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-hidden transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg cursor-pointer data-highlighted:bg-muted {index ===
|
class="flex group/item w-full text-left font-medium line-clamp-1 select-none items-center rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-hidden transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-xl cursor-pointer data-highlighted:bg-muted {index ===
|
||||||
selectedModelIdx
|
selectedModelIdx
|
||||||
? 'bg-gray-100 dark:bg-gray-800 group-hover:bg-transparent'
|
? 'bg-gray-100 dark:bg-gray-800 group-hover:bg-transparent'
|
||||||
: ''}"
|
: ''}"
|
||||||
|
|
@ -64,7 +64,7 @@
|
||||||
{#each item.model?.tags.sort((a, b) => a.name.localeCompare(b.name)) as tag}
|
{#each item.model?.tags.sort((a, b) => a.name.localeCompare(b.name)) as tag}
|
||||||
<Tooltip content={tag.name} className="flex-shrink-0">
|
<Tooltip content={tag.name} className="flex-shrink-0">
|
||||||
<div
|
<div
|
||||||
class=" text-xs font-bold px-1 rounded-sm uppercase bg-gray-500/20 text-gray-700 dark:text-gray-200"
|
class=" text-xs font-semibold px-1 rounded-sm uppercase bg-gray-500/20 text-gray-700 dark:text-gray-200"
|
||||||
>
|
>
|
||||||
{tag.name}
|
{tag.name}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -144,7 +144,7 @@
|
||||||
<div slot="tooltip" id="tags-{item.model.id}">
|
<div slot="tooltip" id="tags-{item.model.id}">
|
||||||
{#each item.model?.tags.sort((a, b) => a.name.localeCompare(b.name)) as tag}
|
{#each item.model?.tags.sort((a, b) => a.name.localeCompare(b.name)) as tag}
|
||||||
<Tooltip content={tag.name} className="flex-shrink-0">
|
<Tooltip content={tag.name} className="flex-shrink-0">
|
||||||
<div class=" text-xs font-semibold rounded-sm uppercase text-white">
|
<div class=" text-xs font-medium rounded-sm uppercase text-white">
|
||||||
{tag.name}
|
{tag.name}
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@
|
||||||
|
|
||||||
<DropdownMenu.Content
|
<DropdownMenu.Content
|
||||||
strategy="fixed"
|
strategy="fixed"
|
||||||
class="w-full max-w-[180px] text-sm rounded-2xl px-1 py-1.5 z-[9999999] bg-white dark:bg-gray-850 dark:text-white shadow-lg border border-gray-100 dark:border-gray-800"
|
class="w-full max-w-[180px] text-sm rounded-2xl p-1 z-[9999999] bg-white dark:bg-gray-850 dark:text-white shadow-lg border border-gray-100 dark:border-gray-800"
|
||||||
sideOffset={-2}
|
sideOffset={-2}
|
||||||
side="bottom"
|
side="bottom"
|
||||||
align="end"
|
align="end"
|
||||||
|
|
|
||||||
|
|
@ -286,9 +286,11 @@
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onMount(async () => {
|
const setOllamaVersion = async () => {
|
||||||
ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => false);
|
ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => false);
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
if (items) {
|
if (items) {
|
||||||
tags = items
|
tags = items
|
||||||
.filter((item) => !(item.model?.info?.meta?.hidden ?? false))
|
.filter((item) => !(item.model?.info?.meta?.hidden ?? false))
|
||||||
|
|
@ -300,6 +302,10 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$: if (show) {
|
||||||
|
setOllamaVersion();
|
||||||
|
}
|
||||||
|
|
||||||
const cancelModelPullHandler = async (model: string) => {
|
const cancelModelPullHandler = async (model: string) => {
|
||||||
const { reader, abortController } = $MODEL_DOWNLOAD_POOL[model];
|
const { reader, abortController } = $MODEL_DOWNLOAD_POOL[model];
|
||||||
if (abortController) {
|
if (abortController) {
|
||||||
|
|
@ -344,7 +350,7 @@
|
||||||
closeFocus={false}
|
closeFocus={false}
|
||||||
>
|
>
|
||||||
<DropdownMenu.Trigger
|
<DropdownMenu.Trigger
|
||||||
class="relative w-full font-primary {($settings?.highContrastMode ?? false)
|
class="relative w-full {($settings?.highContrastMode ?? false)
|
||||||
? ''
|
? ''
|
||||||
: 'outline-hidden focus:outline-hidden'}"
|
: 'outline-hidden focus:outline-hidden'}"
|
||||||
aria-label={placeholder}
|
aria-label={placeholder}
|
||||||
|
|
@ -376,14 +382,15 @@
|
||||||
<DropdownMenu.Content
|
<DropdownMenu.Content
|
||||||
class=" z-40 {$mobile
|
class=" z-40 {$mobile
|
||||||
? `w-full`
|
? `w-full`
|
||||||
: `${className}`} max-w-[calc(100vw-1rem)] justify-start rounded-xl bg-white dark:bg-gray-850 dark:text-white shadow-lg outline-hidden"
|
: `${className}`} max-w-[calc(100vw-1rem)] justify-start rounded-2xl bg-white dark:bg-gray-850 dark:text-white shadow-lg outline-hidden"
|
||||||
transition={flyAndScale}
|
transition={flyAndScale}
|
||||||
side={$mobile ? 'bottom' : 'bottom-start'}
|
side={$mobile ? 'bottom' : 'bottom-start'}
|
||||||
sideOffset={3}
|
sideOffset={2}
|
||||||
|
alignOffset={-1}
|
||||||
>
|
>
|
||||||
<slot>
|
<slot>
|
||||||
{#if searchEnabled}
|
{#if searchEnabled}
|
||||||
<div class="flex items-center gap-2.5 px-4 mt-3.5 mb-1.5">
|
<div class="flex items-center gap-2.5 px-4.5 mt-3.5 mb-1.5">
|
||||||
<Search className="size-4" strokeWidth="2.5" />
|
<Search className="size-4" strokeWidth="2.5" />
|
||||||
|
|
||||||
<input
|
<input
|
||||||
|
|
@ -419,7 +426,7 @@
|
||||||
<div class="px-2">
|
<div class="px-2">
|
||||||
{#if tags && items.filter((item) => !(item.model?.info?.meta?.hidden ?? false)).length > 0}
|
{#if tags && items.filter((item) => !(item.model?.info?.meta?.hidden ?? false)).length > 0}
|
||||||
<div
|
<div
|
||||||
class=" flex w-full bg-white dark:bg-gray-850 overflow-x-auto scrollbar-none mb-0.5"
|
class=" flex w-full bg-white dark:bg-gray-850 overflow-x-auto scrollbar-none font-[450] mb-0.5"
|
||||||
on:wheel={(e) => {
|
on:wheel={(e) => {
|
||||||
if (e.deltaY !== 0) {
|
if (e.deltaY !== 0) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -428,12 +435,12 @@
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex gap-1 w-fit text-center text-sm font-medium rounded-full bg-transparent px-1.5 pb-0.5"
|
class="flex gap-1 w-fit text-center text-sm rounded-full bg-transparent px-1.5"
|
||||||
bind:this={tagsContainerElement}
|
bind:this={tagsContainerElement}
|
||||||
>
|
>
|
||||||
{#if items.find((item) => item.model?.connection_type === 'local') || items.find((item) => item.model?.connection_type === 'external') || items.find((item) => item.model?.direct) || tags.length > 0}
|
{#if items.find((item) => item.model?.connection_type === 'local') || items.find((item) => item.model?.connection_type === 'external') || items.find((item) => item.model?.direct) || tags.length > 0}
|
||||||
<button
|
<button
|
||||||
class="min-w-fit outline-none px-1.5 {selectedTag === '' &&
|
class="min-w-fit outline-none px-1.5 py-0.5 {selectedTag === '' &&
|
||||||
selectedConnectionType === ''
|
selectedConnectionType === ''
|
||||||
? ''
|
? ''
|
||||||
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition capitalize"
|
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition capitalize"
|
||||||
|
|
@ -511,7 +518,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="px-2 max-h-64 overflow-y-auto group relative">
|
<div class="px-2.5 max-h-64 overflow-y-auto group relative">
|
||||||
{#each filteredItems as item, index}
|
{#each filteredItems as item, index}
|
||||||
<ModelItem
|
<ModelItem
|
||||||
{selectedModelIdx}
|
{selectedModelIdx}
|
||||||
|
|
@ -543,7 +550,7 @@
|
||||||
placement="top-start"
|
placement="top-start"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="flex w-full font-medium line-clamp-1 select-none items-center rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-hidden transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg cursor-pointer data-highlighted:bg-muted"
|
class="flex w-full font-medium line-clamp-1 select-none items-center rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-hidden transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-xl cursor-pointer data-highlighted:bg-muted"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
pullModelHandler();
|
pullModelHandler();
|
||||||
}}
|
}}
|
||||||
|
|
@ -557,10 +564,10 @@
|
||||||
|
|
||||||
{#each Object.keys($MODEL_DOWNLOAD_POOL) as model}
|
{#each Object.keys($MODEL_DOWNLOAD_POOL) as model}
|
||||||
<div
|
<div
|
||||||
class="flex w-full justify-between font-medium select-none rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-hidden transition-all duration-75 rounded-lg cursor-pointer data-highlighted:bg-muted"
|
class="flex w-full justify-between font-medium select-none rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-hidden transition-all duration-75 rounded-xl cursor-pointer data-highlighted:bg-muted"
|
||||||
>
|
>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<div class="-ml-2 mr-2.5 translate-y-0.5">
|
<div class="mr-2.5 translate-y-0.5">
|
||||||
<Spinner />
|
<Spinner />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -617,7 +624,7 @@
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3"></div>
|
<div class="mb-2.5"></div>
|
||||||
|
|
||||||
<div class="hidden w-[42rem]" />
|
<div class="hidden w-[42rem]" />
|
||||||
<div class="hidden w-[32rem]" />
|
<div class="hidden w-[32rem]" />
|
||||||
|
|
|
||||||
|
|
@ -36,11 +36,11 @@
|
||||||
|
|
||||||
import EllipsisHorizontal from '../icons/EllipsisHorizontal.svelte';
|
import EllipsisHorizontal from '../icons/EllipsisHorizontal.svelte';
|
||||||
import ChatPlus from '../icons/ChatPlus.svelte';
|
import ChatPlus from '../icons/ChatPlus.svelte';
|
||||||
|
import ChatCheck from '../icons/ChatCheck.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
export let initNewChat: Function;
|
export let initNewChat: Function;
|
||||||
export let title: string = $WEBUI_NAME;
|
|
||||||
export let shareEnabled: boolean = false;
|
export let shareEnabled: boolean = false;
|
||||||
|
|
||||||
export let chat;
|
export let chat;
|
||||||
|
|
@ -72,7 +72,7 @@
|
||||||
<nav class="sticky top-0 z-30 w-full py-1 -mb-8 flex flex-col items-center drag-region">
|
<nav class="sticky top-0 z-30 w-full py-1 -mb-8 flex flex-col items-center drag-region">
|
||||||
<div class="flex items-center w-full pl-1.5 pr-1">
|
<div class="flex items-center w-full pl-1.5 pr-1">
|
||||||
<div
|
<div
|
||||||
class=" bg-linear-to-b via-50% from-white via-white to-transparent dark:from-gray-900 dark:via-gray-900 dark:to-transparent pointer-events-none absolute inset-0 -bottom-7 z-[-1]"
|
class=" bg-linear-to-b via-40% to-97% from-white via-white to-transparent dark:from-gray-900 dark:via-gray-900 dark:to-transparent pointer-events-none absolute inset-0 -bottom-7 z-[-1]"
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
<div class=" flex max-w-full w-full mx-auto px-1.5 md:px-2 pt-0.5 bg-transparent">
|
<div class=" flex max-w-full w-full mx-auto px-1.5 md:px-2 pt-0.5 bg-transparent">
|
||||||
|
|
@ -152,13 +152,31 @@
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class=" m-auto self-center">
|
<div class=" m-auto self-center">
|
||||||
<ChatPlus className=" size-4.5" strokeWidth="1.5" />
|
<ChatCheck className=" size-4.5" strokeWidth="1.5" />
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if $mobile && !$temporaryChatEnabled && chat && chat.id}
|
||||||
|
<Tooltip content={$i18n.t('New Chat')}>
|
||||||
|
<button
|
||||||
|
class=" flex {$showSidebar
|
||||||
|
? 'md:hidden'
|
||||||
|
: ''} cursor-pointer px-2 py-2 rounded-xl text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-850 transition"
|
||||||
|
on:click={() => {
|
||||||
|
initNewChat();
|
||||||
|
}}
|
||||||
|
aria-label="New Chat"
|
||||||
|
>
|
||||||
|
<div class=" m-auto self-center">
|
||||||
|
<ChatPlus className=" size-4.5" strokeWidth="1.5" />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if shareEnabled && chat && (chat.id || $temporaryChatEnabled)}
|
{#if shareEnabled && chat && (chat.id || $temporaryChatEnabled)}
|
||||||
<Menu
|
<Menu
|
||||||
{chat}
|
{chat}
|
||||||
|
|
@ -198,24 +216,6 @@
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if $mobile}
|
|
||||||
<Tooltip content={$i18n.t('New Chat')}>
|
|
||||||
<button
|
|
||||||
class=" flex {$showSidebar
|
|
||||||
? 'md:hidden'
|
|
||||||
: ''} cursor-pointer px-2 py-2 rounded-xl text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-850 transition"
|
|
||||||
on:click={() => {
|
|
||||||
initNewChat();
|
|
||||||
}}
|
|
||||||
aria-label="New Chat"
|
|
||||||
>
|
|
||||||
<div class=" m-auto self-center">
|
|
||||||
<PencilSquare className=" size-5" strokeWidth="2" />
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if $user !== undefined && $user !== null}
|
{#if $user !== undefined && $user !== null}
|
||||||
<UserMenu
|
<UserMenu
|
||||||
className="max-w-[240px]"
|
className="max-w-[240px]"
|
||||||
|
|
@ -255,7 +255,7 @@
|
||||||
|
|
||||||
<div class="absolute top-[100%] left-0 right-0 h-fit">
|
<div class="absolute top-[100%] left-0 right-0 h-fit">
|
||||||
{#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 !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))}
|
||||||
<div class=" w-full z-30 mt-5">
|
<div class=" w-full z-30 mt-4">
|
||||||
<div class=" flex flex-col gap-1 w-full">
|
<div class=" flex flex-col gap-1 w-full">
|
||||||
{#if ($config?.license_metadata?.type ?? null) === 'trial'}
|
{#if ($config?.license_metadata?.type ?? null) === 'trial'}
|
||||||
<Banner
|
<Banner
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@
|
||||||
className="w-full flex justify-center mb-0.5"
|
className="w-full flex justify-center mb-0.5"
|
||||||
placement="top"
|
placement="top"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-2 text-gray-500 font-medium text-base my-2 w-fit">
|
<div class="flex items-center gap-2 text-gray-500 text-base my-2 w-fit">
|
||||||
<EyeSlash strokeWidth="2.5" className="size-4" />{$i18n.t('Temporary Chat')}
|
<EyeSlash strokeWidth="2.5" className="size-4" />{$i18n.t('Temporary Chat')}
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@
|
||||||
let showFolderModal = false;
|
let showFolderModal = false;
|
||||||
let showDeleteConfirm = false;
|
let showDeleteConfirm = false;
|
||||||
|
|
||||||
const updateHandler = async ({ name, data }) => {
|
const updateHandler = async ({ name, meta, data }) => {
|
||||||
if (name === '') {
|
if (name === '') {
|
||||||
toast.error($i18n.t('Folder name cannot be empty.'));
|
toast.error($i18n.t('Folder name cannot be empty.'));
|
||||||
return;
|
return;
|
||||||
|
|
@ -45,6 +45,7 @@
|
||||||
|
|
||||||
const res = await updateFolderById(localStorage.token, folder.id, {
|
const res = await updateFolderById(localStorage.token, folder.id, {
|
||||||
name,
|
name,
|
||||||
|
...(meta ? { meta } : {}),
|
||||||
...(data ? { data } : {})
|
...(data ? { data } : {})
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
toast.error(`${error}`);
|
toast.error(`${error}`);
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@
|
||||||
import { getVoices as _getVoices } from '$lib/apis/audio';
|
import { getVoices as _getVoices } from '$lib/apis/audio';
|
||||||
|
|
||||||
import Switch from '$lib/components/common/Switch.svelte';
|
import Switch from '$lib/components/common/Switch.svelte';
|
||||||
import { round } from '@huggingface/transformers';
|
|
||||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
import Plus from '$lib/components/icons/Plus.svelte';
|
import Plus from '$lib/components/icons/Plus.svelte';
|
||||||
import Switch from '$lib/components/common/Switch.svelte';
|
import Switch from '$lib/components/common/Switch.svelte';
|
||||||
import ManageFloatingActionButtonsModal from './Interface/ManageFloatingActionButtonsModal.svelte';
|
import ManageFloatingActionButtonsModal from './Interface/ManageFloatingActionButtonsModal.svelte';
|
||||||
|
import ManageImageCompressionModal from './Interface/ManageImageCompressionModal.svelte';
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
@ -93,6 +94,7 @@
|
||||||
let iframeSandboxAllowForms = false;
|
let iframeSandboxAllowForms = false;
|
||||||
|
|
||||||
let showManageFloatingActionButtonsModal = false;
|
let showManageFloatingActionButtonsModal = false;
|
||||||
|
let showManageImageCompressionModal = false;
|
||||||
|
|
||||||
const toggleLandingPageMode = async () => {
|
const toggleLandingPageMode = async () => {
|
||||||
landingPageMode = landingPageMode === '' ? 'chat' : '';
|
landingPageMode = landingPageMode === '' ? 'chat' : '';
|
||||||
|
|
@ -260,6 +262,14 @@
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ManageImageCompressionModal
|
||||||
|
bind:show={showManageImageCompressionModal}
|
||||||
|
size={imageCompressionSize}
|
||||||
|
onSave={(size) => {
|
||||||
|
saveSettings({ imageCompressionSize: size });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<form
|
<form
|
||||||
id="tab-interface"
|
id="tab-interface"
|
||||||
class="flex flex-col h-full justify-between space-y-3 text-sm"
|
class="flex flex-col h-full justify-between space-y-3 text-sm"
|
||||||
|
|
@ -1154,7 +1164,20 @@
|
||||||
{$i18n.t('Image Compression')}
|
{$i18n.t('Image Compression')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-2 p-1">
|
<div class="flex items-center gap-3 p-1">
|
||||||
|
{#if imageCompression}
|
||||||
|
<button
|
||||||
|
class="text-xs text-gray-700 dark:text-gray-400 underline"
|
||||||
|
type="button"
|
||||||
|
aria-label={$i18n.t('Open Modal To Manage Image Compression')}
|
||||||
|
on:click={() => {
|
||||||
|
showManageImageCompressionModal = true;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{$i18n.t('Manage')}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<Switch
|
<Switch
|
||||||
ariaLabelledbyId="image-compression-label"
|
ariaLabelledbyId="image-compression-label"
|
||||||
tooltip={true}
|
tooltip={true}
|
||||||
|
|
@ -1168,39 +1191,6 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if imageCompression}
|
{#if imageCompression}
|
||||||
<div>
|
|
||||||
<div class=" py-0.5 flex w-full justify-between text-xs">
|
|
||||||
<div id="image-compression-size-label" class=" self-center text-xs">
|
|
||||||
{$i18n.t('Image Max Compression Size')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="p-1">
|
|
||||||
<label class="sr-only" for="image-comp-width"
|
|
||||||
>{$i18n.t('Image Max Compression Size width')}</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
bind:value={imageCompressionSize.width}
|
|
||||||
type="number"
|
|
||||||
aria-labelledby="image-comp-width"
|
|
||||||
class="w-20 bg-transparent outline-hidden text-center"
|
|
||||||
min="0"
|
|
||||||
placeholder={$i18n.t('Width')}
|
|
||||||
/>x
|
|
||||||
<label class="sr-only" for="image-comp-height"
|
|
||||||
>{$i18n.t('Image Max Compression Size height')}</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
bind:value={imageCompressionSize.height}
|
|
||||||
type="number"
|
|
||||||
aria-labelledby="image-comp-height"
|
|
||||||
class="w-20 bg-transparent outline-hidden text-center"
|
|
||||||
min="0"
|
|
||||||
placeholder={$i18n.t('Height')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class=" py-0.5 flex w-full justify-between">
|
<div class=" py-0.5 flex w-full justify-between">
|
||||||
<div id="image-compression-in-channels-label" class=" self-center text-xs">
|
<div id="image-compression-in-channels-label" class=" self-center text-xs">
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,108 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
import { getContext, onMount } from 'svelte';
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
import Modal from '$lib/components/common/Modal.svelte';
|
||||||
|
import Plus from '$lib/components/icons/Plus.svelte';
|
||||||
|
import Minus from '$lib/components/icons/Minus.svelte';
|
||||||
|
import XMark from '$lib/components/icons/XMark.svelte';
|
||||||
|
import Textarea from '$lib/components/common/Textarea.svelte';
|
||||||
|
import Switch from '$lib/components/common/Switch.svelte';
|
||||||
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
|
|
||||||
|
export let show = false;
|
||||||
|
export let size = null;
|
||||||
|
export let onSave = () => {};
|
||||||
|
|
||||||
|
const submitHandler = async () => {
|
||||||
|
onSave(size);
|
||||||
|
show = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(() => {});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal size="sm" bind:show>
|
||||||
|
<div>
|
||||||
|
<div class=" flex justify-between dark:text-gray-100 px-5 pt-4 pb-1.5">
|
||||||
|
<h1 class="text-lg font-medium self-center font-primary">
|
||||||
|
{$i18n.t('Manage')}
|
||||||
|
</h1>
|
||||||
|
<button
|
||||||
|
class="self-center"
|
||||||
|
aria-label={$i18n.t('Close modal')}
|
||||||
|
on:click={() => {
|
||||||
|
show = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<XMark className={'size-5'} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col md:flex-row w-full px-4 pb-4 md:space-x-4 dark:text-gray-200">
|
||||||
|
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
|
||||||
|
<form
|
||||||
|
class="flex flex-col w-full px-1"
|
||||||
|
on:submit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
submitHandler();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<div class=" py-0.5 flex flex-col w-full text-sm">
|
||||||
|
<div id="image-compression-size-label" class=" text-xs mb-2">
|
||||||
|
{$i18n.t('Image Max Compression Size')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-1 flex-1 flex gap-2">
|
||||||
|
<div class=" flex-1">
|
||||||
|
<label class="sr-only" for="image-comp-width"
|
||||||
|
>{$i18n.t('Image Max Compression Size width')}</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
bind:value={size.width}
|
||||||
|
type="number"
|
||||||
|
aria-labelledby="image-comp-width"
|
||||||
|
class="w-full bg-transparent outline-hidden text-center"
|
||||||
|
min="0"
|
||||||
|
placeholder={$i18n.t('Width')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="self-center text-gray-500 dark:text-gray-400">
|
||||||
|
<XMark />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1">
|
||||||
|
<label class="sr-only" for="image-comp-height"
|
||||||
|
>{$i18n.t('Image Max Compression Size height')}</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
bind:value={size.height}
|
||||||
|
type="number"
|
||||||
|
aria-labelledby="image-comp-height"
|
||||||
|
class="w-full bg-transparent outline-hidden text-center"
|
||||||
|
min="0"
|
||||||
|
placeholder={$i18n.t('Height')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end text-sm font-medium">
|
||||||
|
<button
|
||||||
|
class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
{$i18n.t('Save')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
|
@ -12,13 +12,22 @@
|
||||||
import General from './Settings/General.svelte';
|
import General from './Settings/General.svelte';
|
||||||
import Interface from './Settings/Interface.svelte';
|
import Interface from './Settings/Interface.svelte';
|
||||||
import Audio from './Settings/Audio.svelte';
|
import Audio from './Settings/Audio.svelte';
|
||||||
import Chats from './Settings/Chats.svelte';
|
import DataControls from './Settings/DataControls.svelte';
|
||||||
import User from '../icons/User.svelte';
|
|
||||||
import Personalization from './Settings/Personalization.svelte';
|
import Personalization from './Settings/Personalization.svelte';
|
||||||
import Search from '../icons/Search.svelte';
|
import Search from '../icons/Search.svelte';
|
||||||
import XMark from '../icons/XMark.svelte';
|
import XMark from '../icons/XMark.svelte';
|
||||||
import Connections from './Settings/Connections.svelte';
|
import Connections from './Settings/Connections.svelte';
|
||||||
import Tools from './Settings/Tools.svelte';
|
import Tools from './Settings/Tools.svelte';
|
||||||
|
import DatabaseSettings from '../icons/DatabaseSettings.svelte';
|
||||||
|
import SettingsAlt from '../icons/SettingsAlt.svelte';
|
||||||
|
import Link from '../icons/Link.svelte';
|
||||||
|
import UserCircle from '../icons/UserCircle.svelte';
|
||||||
|
import SoundHigh from '../icons/SoundHigh.svelte';
|
||||||
|
import InfoCircle from '../icons/InfoCircle.svelte';
|
||||||
|
import WrenchAlt from '../icons/WrenchAlt.svelte';
|
||||||
|
import Face from '../icons/Face.svelte';
|
||||||
|
import AppNotification from '../icons/AppNotification.svelte';
|
||||||
|
import UserBadgeCheck from '../icons/UserBadgeCheck.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
|
@ -323,8 +332,8 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'chats',
|
id: 'data_controls',
|
||||||
title: 'Chats',
|
title: 'Data Controls',
|
||||||
keywords: [
|
keywords: [
|
||||||
'archive all chats',
|
'archive all chats',
|
||||||
'archive chats',
|
'archive chats',
|
||||||
|
|
@ -560,9 +569,9 @@
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal size="lg" bind:show>
|
<Modal size="xl" bind:show>
|
||||||
<div class="text-gray-700 dark:text-gray-100">
|
<div class="text-gray-700 dark:text-gray-100 mx-1">
|
||||||
<div class=" flex justify-between dark:text-gray-300 px-5 pt-4 pb-1">
|
<div class=" flex justify-between dark:text-gray-300 px-4 md:px-4.5 pt-4.5 pb-0.5 md:pb-2.5">
|
||||||
<div class=" text-lg font-medium self-center">{$i18n.t('Settings')}</div>
|
<div class=" text-lg font-medium self-center">{$i18n.t('Settings')}</div>
|
||||||
<button
|
<button
|
||||||
aria-label={$i18n.t('Close settings modal')}
|
aria-label={$i18n.t('Close settings modal')}
|
||||||
|
|
@ -575,13 +584,16 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col md:flex-row w-full px-4 pt-1 pb-4 md:space-x-4">
|
<div class="flex flex-col md:flex-row w-full pt-1 pb-4">
|
||||||
<div
|
<div
|
||||||
role="tablist"
|
role="tablist"
|
||||||
id="settings-tabs-container"
|
id="settings-tabs-container"
|
||||||
class="tabs flex flex-row overflow-x-auto gap-2.5 md:gap-1 md:flex-col flex-1 md:flex-none md:w-40 md:min-h-[32rem] md:max-h-[32rem] dark:text-gray-200 text-sm font-medium text-left mb-1 md:mb-0 -translate-y-1"
|
class="tabs flex flex-row overflow-x-auto gap-2.5 mx-3 md:pr-4 md:gap-1 md:flex-col flex-1 md:flex-none md:w-50 md:min-h-[36rem] md:max-h-[36rem] dark:text-gray-200 text-sm text-left mb-1 md:mb-0 -translate-y-1"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="hidden md:flex w-full rounded-full px-2.5 gap-2 bg-gray-50 dark:bg-gray-850 my-1 mb-1.5"
|
||||||
|
id="settings-search"
|
||||||
>
|
>
|
||||||
<div class="hidden md:flex w-full rounded-xl -mb-1 px-0.5 gap-2" id="settings-search">
|
|
||||||
<div class="self-center rounded-l-xl bg-transparent">
|
<div class="self-center rounded-l-xl bg-transparent">
|
||||||
<Search
|
<Search
|
||||||
className="size-3.5"
|
className="size-3.5"
|
||||||
|
|
@ -590,7 +602,7 @@
|
||||||
</div>
|
</div>
|
||||||
<label class="sr-only" for="search-input-settings-modal">{$i18n.t('Search')}</label>
|
<label class="sr-only" for="search-input-settings-modal">{$i18n.t('Search')}</label>
|
||||||
<input
|
<input
|
||||||
class={`w-full py-1.5 text-sm bg-transparent dark:text-gray-300 outline-hidden
|
class={`w-full py-1 text-sm bg-transparent dark:text-gray-300 outline-hidden
|
||||||
${($settings?.highContrastMode ?? false) ? 'placeholder-gray-800' : ''}`}
|
${($settings?.highContrastMode ?? false) ? 'placeholder-gray-800' : ''}`}
|
||||||
bind:value={search}
|
bind:value={search}
|
||||||
id="search-input-settings-modal"
|
id="search-input-settings-modal"
|
||||||
|
|
@ -605,7 +617,7 @@
|
||||||
role="tab"
|
role="tab"
|
||||||
aria-controls="tab-general"
|
aria-controls="tab-general"
|
||||||
aria-selected={selectedTab === 'general'}
|
aria-selected={selectedTab === 'general'}
|
||||||
class={`px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition
|
class={`px-0.5 md:px-2.5 py-1 min-w-fit rounded-xl flex-1 md:flex-none flex text-left transition
|
||||||
${
|
${
|
||||||
selectedTab === 'general'
|
selectedTab === 'general'
|
||||||
? ($settings?.highContrastMode ?? false)
|
? ($settings?.highContrastMode ?? false)
|
||||||
|
|
@ -620,19 +632,7 @@
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class=" self-center mr-2">
|
<div class=" self-center mr-2">
|
||||||
<svg
|
<SettingsAlt strokeWidth="2" />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
aria-hidden="true"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-4 h-4"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M8.34 1.804A1 1 0 019.32 1h1.36a1 1 0 01.98.804l.295 1.473c.497.144.971.342 1.416.587l1.25-.834a1 1 0 011.262.125l.962.962a1 1 0 01.125 1.262l-.834 1.25c.245.445.443.919.587 1.416l1.473.294a1 1 0 01.804.98v1.361a1 1 0 01-.804.98l-1.473.295a6.95 6.95 0 01-.587 1.416l.834 1.25a1 1 0 01-.125 1.262l-.962.962a1 1 0 01-1.262.125l-1.25-.834a6.953 6.953 0 01-1.416.587l-.294 1.473a1 1 0 01-.98.804H9.32a1 1 0 01-.98-.804l-.295-1.473a6.957 6.957 0 01-1.416-.587l-1.25.834a1 1 0 01-1.262-.125l-.962-.962a1 1 0 01-.125-1.262l.834-1.25a6.957 6.957 0 01-.587-1.416l-1.473-.294A1 1 0 011 10.68V9.32a1 1 0 01.804-.98l1.473-.295c.144-.497.342-.971.587-1.416l-.834-1.25a1 1 0 01.125-1.262l.962-.962A1 1 0 015.38 3.03l1.25.834a6.957 6.957 0 011.416-.587l.294-1.473zM13 10a3 3 0 11-6 0 3 3 0 016 0z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
<div class=" self-center">{$i18n.t('General')}</div>
|
<div class=" self-center">{$i18n.t('General')}</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -641,7 +641,7 @@
|
||||||
role="tab"
|
role="tab"
|
||||||
aria-controls="tab-interface"
|
aria-controls="tab-interface"
|
||||||
aria-selected={selectedTab === 'interface'}
|
aria-selected={selectedTab === 'interface'}
|
||||||
class={`px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition
|
class={`px-0.5 md:px-2.5 py-1 min-w-fit rounded-xl flex-1 md:flex-none flex text-left transition
|
||||||
${
|
${
|
||||||
selectedTab === 'interface'
|
selectedTab === 'interface'
|
||||||
? ($settings?.highContrastMode ?? false)
|
? ($settings?.highContrastMode ?? false)
|
||||||
|
|
@ -656,19 +656,7 @@
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class=" self-center mr-2">
|
<div class=" self-center mr-2">
|
||||||
<svg
|
<AppNotification strokeWidth="2" />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
aria-hidden="true"
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-4 h-4"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M2 4.25A2.25 2.25 0 0 1 4.25 2h7.5A2.25 2.25 0 0 1 14 4.25v5.5A2.25 2.25 0 0 1 11.75 12h-1.312c.1.128.21.248.328.36a.75.75 0 0 1 .234.545v.345a.75.75 0 0 1-.75.75h-4.5a.75.75 0 0 1-.75-.75v-.345a.75.75 0 0 1 .234-.545c.118-.111.228-.232.328-.36H4.25A2.25 2.25 0 0 1 2 9.75v-5.5Zm2.25-.75a.75.75 0 0 0-.75.75v4.5c0 .414.336.75.75.75h7.5a.75.75 0 0 0 .75-.75v-4.5a.75.75 0 0 0-.75-.75h-7.5Z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
<div class=" self-center">{$i18n.t('Interface')}</div>
|
<div class=" self-center">{$i18n.t('Interface')}</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -678,7 +666,7 @@
|
||||||
role="tab"
|
role="tab"
|
||||||
aria-controls="tab-connections"
|
aria-controls="tab-connections"
|
||||||
aria-selected={selectedTab === 'connections'}
|
aria-selected={selectedTab === 'connections'}
|
||||||
class={`px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition
|
class={`px-0.5 md:px-2.5 py-1 min-w-fit rounded-xl flex-1 md:flex-none flex text-left transition
|
||||||
${
|
${
|
||||||
selectedTab === 'connections'
|
selectedTab === 'connections'
|
||||||
? ($settings?.highContrastMode ?? false)
|
? ($settings?.highContrastMode ?? false)
|
||||||
|
|
@ -693,17 +681,7 @@
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class=" self-center mr-2">
|
<div class=" self-center mr-2">
|
||||||
<svg
|
<Link strokeWidth="2" />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
aria-hidden="true"
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-4 h-4"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M1 9.5A3.5 3.5 0 0 0 4.5 13H12a3 3 0 0 0 .917-5.857 2.503 2.503 0 0 0-3.198-3.019 3.5 3.5 0 0 0-6.628 2.171A3.5 3.5 0 0 0 1 9.5Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
<div class=" self-center">{$i18n.t('Connections')}</div>
|
<div class=" self-center">{$i18n.t('Connections')}</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -714,7 +692,7 @@
|
||||||
role="tab"
|
role="tab"
|
||||||
aria-controls="tab-tools"
|
aria-controls="tab-tools"
|
||||||
aria-selected={selectedTab === 'tools'}
|
aria-selected={selectedTab === 'tools'}
|
||||||
class={`px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition
|
class={`px-0.5 md:px-2.5 py-1 min-w-fit rounded-xl flex-1 md:flex-none flex text-left transition
|
||||||
${
|
${
|
||||||
selectedTab === 'tools'
|
selectedTab === 'tools'
|
||||||
? ($settings?.highContrastMode ?? false)
|
? ($settings?.highContrastMode ?? false)
|
||||||
|
|
@ -729,19 +707,7 @@
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class=" self-center mr-2">
|
<div class=" self-center mr-2">
|
||||||
<svg
|
<WrenchAlt strokeWidth="2" />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
aria-hidden="true"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="currentColor"
|
|
||||||
class="size-4"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M12 6.75a5.25 5.25 0 0 1 6.775-5.025.75.75 0 0 1 .313 1.248l-3.32 3.319c.063.475.276.934.641 1.299.365.365.824.578 1.3.64l3.318-3.319a.75.75 0 0 1 1.248.313 5.25 5.25 0 0 1-5.472 6.756c-1.018-.086-1.87.1-2.309.634L7.344 21.3A3.298 3.298 0 1 1 2.7 16.657l8.684-7.151c.533-.44.72-1.291.634-2.309A5.342 5.342 0 0 1 12 6.75ZM4.117 19.125a.75.75 0 0 1 .75-.75h.008a.75.75 0 0 1 .75.75v.008a.75.75 0 0 1-.75.75h-.008a.75.75 0 0 1-.75-.75v-.008Z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
<div class=" self-center">{$i18n.t('External Tools')}</div>
|
<div class=" self-center">{$i18n.t('External Tools')}</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -751,7 +717,7 @@
|
||||||
role="tab"
|
role="tab"
|
||||||
aria-controls="tab-personalization"
|
aria-controls="tab-personalization"
|
||||||
aria-selected={selectedTab === 'personalization'}
|
aria-selected={selectedTab === 'personalization'}
|
||||||
class={`px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition
|
class={`px-0.5 md:px-2.5 py-1 min-w-fit rounded-xl flex-1 md:flex-none flex text-left transition
|
||||||
${
|
${
|
||||||
selectedTab === 'personalization'
|
selectedTab === 'personalization'
|
||||||
? ($settings?.highContrastMode ?? false)
|
? ($settings?.highContrastMode ?? false)
|
||||||
|
|
@ -766,7 +732,7 @@
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class=" self-center mr-2">
|
<div class=" self-center mr-2">
|
||||||
<User />
|
<Face strokeWidth="2" />
|
||||||
</div>
|
</div>
|
||||||
<div class=" self-center">{$i18n.t('Personalization')}</div>
|
<div class=" self-center">{$i18n.t('Personalization')}</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -775,7 +741,7 @@
|
||||||
role="tab"
|
role="tab"
|
||||||
aria-controls="tab-audio"
|
aria-controls="tab-audio"
|
||||||
aria-selected={selectedTab === 'audio'}
|
aria-selected={selectedTab === 'audio'}
|
||||||
class={`px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition
|
class={`px-0.5 md:px-2.5 py-1 min-w-fit rounded-xl flex-1 md:flex-none flex text-left transition
|
||||||
${
|
${
|
||||||
selectedTab === 'audio'
|
selectedTab === 'audio'
|
||||||
? ($settings?.highContrastMode ?? false)
|
? ($settings?.highContrastMode ?? false)
|
||||||
|
|
@ -790,31 +756,18 @@
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class=" self-center mr-2">
|
<div class=" self-center mr-2">
|
||||||
<svg
|
<SoundHigh strokeWidth="2" />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
aria-hidden="true"
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-4 h-4"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M7.557 2.066A.75.75 0 0 1 8 2.75v10.5a.75.75 0 0 1-1.248.56L3.59 11H2a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1h1.59l3.162-2.81a.75.75 0 0 1 .805-.124ZM12.95 3.05a.75.75 0 1 0-1.06 1.06 5.5 5.5 0 0 1 0 7.78.75.75 0 1 0 1.06 1.06 7 7 0 0 0 0-9.9Z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M10.828 5.172a.75.75 0 1 0-1.06 1.06 2.5 2.5 0 0 1 0 3.536.75.75 0 1 0 1.06 1.06 4 4 0 0 0 0-5.656Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
<div class=" self-center">{$i18n.t('Audio')}</div>
|
<div class=" self-center">{$i18n.t('Audio')}</div>
|
||||||
</button>
|
</button>
|
||||||
{:else if tabId === 'chats'}
|
{:else if tabId === 'data_controls'}
|
||||||
<button
|
<button
|
||||||
role="tab"
|
role="tab"
|
||||||
aria-controls="tab-chats"
|
aria-controls="tab-data-controls"
|
||||||
aria-selected={selectedTab === 'chats'}
|
aria-selected={selectedTab === 'data_controls'}
|
||||||
class={`px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition
|
class={`px-0.5 md:px-2.5 py-1 min-w-fit rounded-xl flex-1 md:flex-none flex text-left transition
|
||||||
${
|
${
|
||||||
selectedTab === 'chats'
|
selectedTab === 'data_controls'
|
||||||
? ($settings?.highContrastMode ?? false)
|
? ($settings?.highContrastMode ?? false)
|
||||||
? 'dark:bg-gray-800 bg-gray-200'
|
? 'dark:bg-gray-800 bg-gray-200'
|
||||||
: ''
|
: ''
|
||||||
|
|
@ -823,32 +776,20 @@
|
||||||
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'
|
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'
|
||||||
}`}
|
}`}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
selectedTab = 'chats';
|
selectedTab = 'data_controls';
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class=" self-center mr-2">
|
<div class=" self-center mr-2">
|
||||||
<svg
|
<DatabaseSettings strokeWidth="2" />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
aria-hidden="true"
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-4 h-4"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M8 2C4.262 2 1 4.57 1 8c0 1.86.98 3.486 2.455 4.566a3.472 3.472 0 0 1-.469 1.26.75.75 0 0 0 .713 1.14 6.961 6.961 0 0 0 3.06-1.06c.403.062.818.094 1.241.094 3.738 0 7-2.57 7-6s-3.262-6-7-6ZM5 9a1 1 0 1 0 0-2 1 1 0 0 0 0 2Zm7-1a1 1 0 1 1-2 0 1 1 0 0 1 2 0ZM8 9a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
<div class=" self-center">{$i18n.t('Chats')}</div>
|
<div class=" self-center">{$i18n.t('Data Controls')}</div>
|
||||||
</button>
|
</button>
|
||||||
{:else if tabId === 'account'}
|
{:else if tabId === 'account'}
|
||||||
<button
|
<button
|
||||||
role="tab"
|
role="tab"
|
||||||
aria-controls="tab-account"
|
aria-controls="tab-account"
|
||||||
aria-selected={selectedTab === 'account'}
|
aria-selected={selectedTab === 'account'}
|
||||||
class={`px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition
|
class={`px-0.5 md:px-2.5 py-1 min-w-fit rounded-xl flex-1 md:flex-none flex text-left transition
|
||||||
${
|
${
|
||||||
selectedTab === 'account'
|
selectedTab === 'account'
|
||||||
? ($settings?.highContrastMode ?? false)
|
? ($settings?.highContrastMode ?? false)
|
||||||
|
|
@ -863,19 +804,7 @@
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class=" self-center mr-2">
|
<div class=" self-center mr-2">
|
||||||
<svg
|
<UserCircle strokeWidth="2" />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
aria-hidden="true"
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-4 h-4"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M15 8A7 7 0 1 1 1 8a7 7 0 0 1 14 0Zm-5-2a2 2 0 1 1-4 0 2 2 0 0 1 4 0ZM8 9c-1.825 0-3.422.977-4.295 2.437A5.49 5.49 0 0 0 8 13.5a5.49 5.49 0 0 0 4.294-2.063A4.997 4.997 0 0 0 8 9Z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
<div class=" self-center">{$i18n.t('Account')}</div>
|
<div class=" self-center">{$i18n.t('Account')}</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -884,7 +813,7 @@
|
||||||
role="tab"
|
role="tab"
|
||||||
aria-controls="tab-about"
|
aria-controls="tab-about"
|
||||||
aria-selected={selectedTab === 'about'}
|
aria-selected={selectedTab === 'about'}
|
||||||
class={`px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition
|
class={`px-0.5 md:px-2.5 py-1 min-w-fit rounded-xl flex-1 md:flex-none flex text-left transition
|
||||||
${
|
${
|
||||||
selectedTab === 'about'
|
selectedTab === 'about'
|
||||||
? ($settings?.highContrastMode ?? false)
|
? ($settings?.highContrastMode ?? false)
|
||||||
|
|
@ -899,19 +828,7 @@
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class=" self-center mr-2">
|
<div class=" self-center mr-2">
|
||||||
<svg
|
<InfoCircle strokeWidth="2" />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
aria-hidden="true"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-4 h-4"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
<div class=" self-center">{$i18n.t('About')}</div>
|
<div class=" self-center">{$i18n.t('About')}</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -925,7 +842,7 @@
|
||||||
{#if $user?.role === 'admin'}
|
{#if $user?.role === 'admin'}
|
||||||
<a
|
<a
|
||||||
href="/admin/settings"
|
href="/admin/settings"
|
||||||
class="px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none md:mt-auto flex text-left transition {$settings?.highContrastMode
|
class="px-0.5 md:px-2.5 py-1 min-w-fit rounded-xl flex-1 md:flex-none md:mt-auto flex text-left transition {$settings?.highContrastMode
|
||||||
? 'hover:bg-gray-200 dark:hover:bg-gray-800'
|
? 'hover:bg-gray-200 dark:hover:bg-gray-800'
|
||||||
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}"
|
: 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}"
|
||||||
on:click={async (e) => {
|
on:click={async (e) => {
|
||||||
|
|
@ -935,25 +852,13 @@
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class=" self-center mr-2">
|
<div class=" self-center mr-2">
|
||||||
<svg
|
<UserBadgeCheck strokeWidth="2" />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
aria-hidden="true"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="currentColor"
|
|
||||||
class="size-4"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M4.5 3.75a3 3 0 0 0-3 3v10.5a3 3 0 0 0 3 3h15a3 3 0 0 0 3-3V6.75a3 3 0 0 0-3-3h-15Zm4.125 3a2.25 2.25 0 1 0 0 4.5 2.25 2.25 0 0 0 0-4.5Zm-3.873 8.703a4.126 4.126 0 0 1 7.746 0 .75.75 0 0 1-.351.92 7.47 7.47 0 0 1-3.522.877 7.47 7.47 0 0 1-3.522-.877.75.75 0 0 1-.351-.92ZM15 8.25a.75.75 0 0 0 0 1.5h3.75a.75.75 0 0 0 0-1.5H15ZM14.25 12a.75.75 0 0 1 .75-.75h3.75a.75.75 0 0 1 0 1.5H15a.75.75 0 0 1-.75-.75Zm.75 2.25a.75.75 0 0 0 0 1.5h3.75a.75.75 0 0 0 0-1.5H15Z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
<div class=" self-center">{$i18n.t('Admin Settings')}</div>
|
<div class=" self-center">{$i18n.t('Admin Settings')}</div>
|
||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 md:min-h-[32rem] max-h-[32rem]">
|
<div class="flex-1 px-3.5 md:pl-0 md:pr-4.5 md:min-h-[36rem] max-h-[36rem]">
|
||||||
{#if selectedTab === 'general'}
|
{#if selectedTab === 'general'}
|
||||||
<General
|
<General
|
||||||
{getModels}
|
{getModels}
|
||||||
|
|
@ -997,8 +902,8 @@
|
||||||
toast.success($i18n.t('Settings saved successfully!'));
|
toast.success($i18n.t('Settings saved successfully!'));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{:else if selectedTab === 'chats'}
|
{:else if selectedTab === 'data_controls'}
|
||||||
<Chats {saveSettings} />
|
<DataControls {saveSettings} />
|
||||||
{:else if selectedTab === 'account'}
|
{:else if selectedTab === 'account'}
|
||||||
<Account
|
<Account
|
||||||
{saveSettings}
|
{saveSettings}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class=" text-xs font-bold {classNames[type] ??
|
class=" text-xs font-semibold {classNames[type] ??
|
||||||
classNames['info']} w-fit px-2 rounded-sm uppercase line-clamp-1 mr-0.5"
|
classNames['info']} w-fit px-2 rounded-sm uppercase line-clamp-1 mr-0.5"
|
||||||
>
|
>
|
||||||
{content}
|
{content}
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@
|
||||||
<div class=" flex flex-col md:flex-row md:items-center flex-1 text-sm w-fit gap-1.5">
|
<div class=" flex flex-col md:flex-row md:items-center flex-1 text-sm w-fit gap-1.5">
|
||||||
<div class="flex justify-between self-start">
|
<div class="flex justify-between self-start">
|
||||||
<div
|
<div
|
||||||
class=" text-xs font-bold {classNames[banner.type] ??
|
class=" text-xs font-semibold {classNames[banner.type] ??
|
||||||
classNames['info']} w-fit px-2 rounded-sm uppercase line-clamp-1 mr-0.5"
|
classNames['info']} w-fit px-2 rounded-sm uppercase line-clamp-1 mr-0.5"
|
||||||
>
|
>
|
||||||
{#if banner.type.toLowerCase() === 'info'}
|
{#if banner.type.toLowerCase() === 'info'}
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,8 @@
|
||||||
import CodeBlock from '../chat/Messages/CodeBlock.svelte';
|
import CodeBlock from '../chat/Messages/CodeBlock.svelte';
|
||||||
import Markdown from '../chat/Messages/Markdown.svelte';
|
import Markdown from '../chat/Messages/Markdown.svelte';
|
||||||
import Image from './Image.svelte';
|
import Image from './Image.svelte';
|
||||||
|
import FullHeightIframe from './FullHeightIframe.svelte';
|
||||||
|
import { settings } from '$lib/stores';
|
||||||
|
|
||||||
export let open = false;
|
export let open = false;
|
||||||
|
|
||||||
|
|
@ -87,6 +89,125 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div {id} class={className}>
|
<div {id} class={className}>
|
||||||
|
{#if attributes?.type === 'tool_calls'}
|
||||||
|
{@const args = decode(attributes?.arguments)}
|
||||||
|
{@const result = decode(attributes?.result ?? '')}
|
||||||
|
{@const files = parseJSONString(decode(attributes?.files ?? ''))}
|
||||||
|
{@const embeds = parseJSONString(decode(attributes?.embeds ?? ''))}
|
||||||
|
|
||||||
|
{#if embeds && Array.isArray(embeds) && embeds.length > 0}
|
||||||
|
<div class="py-1 w-full cursor-pointer">
|
||||||
|
<div class=" w-full text-xs text-gray-500">
|
||||||
|
<div class="">
|
||||||
|
{attributes.name}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#each embeds as embed, idx}
|
||||||
|
<div class="my-2" id={`${collapsibleId}-tool-calls-${attributes?.id}-embed-${idx}`}>
|
||||||
|
<FullHeightIframe
|
||||||
|
src={embed}
|
||||||
|
{args}
|
||||||
|
allowScripts={true}
|
||||||
|
allowForms={true}
|
||||||
|
allowSameOrigin={true}
|
||||||
|
allowPopups={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div
|
||||||
|
class="{buttonClassName} cursor-pointer"
|
||||||
|
on:pointerup={() => {
|
||||||
|
if (!disabled) {
|
||||||
|
open = !open;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class=" w-full font-medium flex items-center justify-between gap-2 {attributes?.done &&
|
||||||
|
attributes?.done !== 'true'
|
||||||
|
? 'shimmer'
|
||||||
|
: ''}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{#if attributes?.done && attributes?.done !== 'true'}
|
||||||
|
<div>
|
||||||
|
<Spinner className="size-4" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="">
|
||||||
|
{#if attributes?.done === 'true'}
|
||||||
|
<Markdown
|
||||||
|
id={`${collapsibleId}-tool-calls-${attributes?.id}`}
|
||||||
|
content={$i18n.t('View Result from **{{NAME}}**', {
|
||||||
|
NAME: attributes.name
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<Markdown
|
||||||
|
id={`${collapsibleId}-tool-calls-${attributes?.id}-executing`}
|
||||||
|
content={$i18n.t('Executing **{{NAME}}**...', {
|
||||||
|
NAME: attributes.name
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex self-center translate-y-[1px]">
|
||||||
|
{#if open}
|
||||||
|
<ChevronUp strokeWidth="3.5" className="size-3.5" />
|
||||||
|
{:else}
|
||||||
|
<ChevronDown strokeWidth="3.5" className="size-3.5" />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if !grow}
|
||||||
|
{#if open && !hide}
|
||||||
|
<div transition:slide={{ duration: 300, easing: quintOut, axis: 'y' }}>
|
||||||
|
{#if attributes?.type === 'tool_calls'}
|
||||||
|
{#if attributes?.done === 'true'}
|
||||||
|
<Markdown
|
||||||
|
id={`${collapsibleId}-tool-calls-${attributes?.id}-result`}
|
||||||
|
content={`> \`\`\`json
|
||||||
|
> ${formatJSONString(args)}
|
||||||
|
> ${formatJSONString(result)}
|
||||||
|
> \`\`\``}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<Markdown
|
||||||
|
id={`${collapsibleId}-tool-calls-${attributes?.id}-result`}
|
||||||
|
content={`> \`\`\`json
|
||||||
|
> ${formatJSONString(args)}
|
||||||
|
> \`\`\``}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<slot name="content" />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if attributes?.done === 'true'}
|
||||||
|
{#if typeof files === 'object'}
|
||||||
|
{#each files ?? [] as file, idx}
|
||||||
|
{#if file.startsWith('data:image/')}
|
||||||
|
<Image
|
||||||
|
id={`${collapsibleId}-tool-calls-${attributes?.id}-result-${idx}`}
|
||||||
|
src={file}
|
||||||
|
alt="Image"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
{#if title !== null}
|
{#if title !== null}
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
|
@ -134,22 +255,6 @@
|
||||||
{:else}
|
{:else}
|
||||||
{$i18n.t('Analyzing...')}
|
{$i18n.t('Analyzing...')}
|
||||||
{/if}
|
{/if}
|
||||||
{:else if attributes?.type === 'tool_calls'}
|
|
||||||
{#if attributes?.done === 'true'}
|
|
||||||
<Markdown
|
|
||||||
id={`${collapsibleId}-tool-calls-${attributes?.id}`}
|
|
||||||
content={$i18n.t('View Result from **{{NAME}}**', {
|
|
||||||
NAME: attributes.name
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
<Markdown
|
|
||||||
id={`${collapsibleId}-tool-calls-${attributes?.id}-executing`}
|
|
||||||
content={$i18n.t('Executing **{{NAME}}**...', {
|
|
||||||
NAME: attributes.name
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
{:else}
|
{:else}
|
||||||
{title}
|
{title}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -209,56 +314,12 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if attributes?.type === 'tool_calls'}
|
|
||||||
{@const args = decode(attributes?.arguments)}
|
|
||||||
{@const result = decode(attributes?.result ?? '')}
|
|
||||||
{@const files = parseJSONString(decode(attributes?.files ?? ''))}
|
|
||||||
|
|
||||||
{#if !grow}
|
{#if !grow}
|
||||||
{#if open && !hide}
|
|
||||||
<div transition:slide={{ duration: 300, easing: quintOut, axis: 'y' }}>
|
|
||||||
{#if attributes?.type === 'tool_calls'}
|
|
||||||
{#if attributes?.done === 'true'}
|
|
||||||
<Markdown
|
|
||||||
id={`${collapsibleId}-tool-calls-${attributes?.id}-result`}
|
|
||||||
content={`> \`\`\`json
|
|
||||||
> ${formatJSONString(args)}
|
|
||||||
> ${formatJSONString(result)}
|
|
||||||
> \`\`\``}
|
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
<Markdown
|
|
||||||
id={`${collapsibleId}-tool-calls-${attributes?.id}-result`}
|
|
||||||
content={`> \`\`\`json
|
|
||||||
> ${formatJSONString(args)}
|
|
||||||
> \`\`\``}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
{:else}
|
|
||||||
<slot name="content" />
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if attributes?.done === 'true'}
|
|
||||||
{#if typeof files === 'object'}
|
|
||||||
{#each files ?? [] as file, idx}
|
|
||||||
{#if file.startsWith('data:image/')}
|
|
||||||
<Image
|
|
||||||
id={`${collapsibleId}-tool-calls-${attributes?.id}-result-${idx}`}
|
|
||||||
src={file}
|
|
||||||
alt="Image"
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
{:else if !grow}
|
|
||||||
{#if open && !hide}
|
{#if open && !hide}
|
||||||
<div transition:slide={{ duration: 300, easing: quintOut, axis: 'y' }}>
|
<div transition:slide={{ duration: 300, easing: quintOut, axis: 'y' }}>
|
||||||
<slot name="content" />
|
<slot name="content" />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@
|
||||||
|
|
||||||
<slot name="content">
|
<slot name="content">
|
||||||
<DropdownMenu.Content
|
<DropdownMenu.Content
|
||||||
class="w-full max-w-[130px] rounded-lg px-1 py-1.5 border border-gray-900 z-50 bg-gray-850 text-white"
|
class="w-full max-w-[130px] rounded-lg p-1 border border-gray-900 z-50 bg-gray-850 text-white"
|
||||||
sideOffset={8}
|
sideOffset={8}
|
||||||
{side}
|
{side}
|
||||||
{align}
|
{align}
|
||||||
|
|
|
||||||
|
|
@ -236,7 +236,7 @@
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="max-h-96 overflow-scroll scrollbar-hidden text-xs whitespace-pre-wrap">
|
<div class="max-h-96 overflow-scroll scrollbar-hidden text-xs whitespace-pre-wrap">
|
||||||
{item?.file?.data?.content ?? 'No content'}
|
{(item?.file?.data?.content ?? '').trim() || 'No content'}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
|
|
@ -251,7 +251,7 @@
|
||||||
|
|
||||||
{#if item?.file?.data}
|
{#if item?.file?.data}
|
||||||
<div class="max-h-96 overflow-scroll scrollbar-hidden text-xs whitespace-pre-wrap">
|
<div class="max-h-96 overflow-scroll scrollbar-hidden text-xs whitespace-pre-wrap">
|
||||||
{item?.file?.data?.content ?? 'No content'}
|
{(item?.file?.data?.content ?? '').trim() || 'No content'}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -16,14 +16,15 @@
|
||||||
export let name = '';
|
export let name = '';
|
||||||
export let collapsible = true;
|
export let collapsible = true;
|
||||||
|
|
||||||
|
export let className = '';
|
||||||
|
export let buttonClassName = 'text-gray-600 dark:text-gray-400';
|
||||||
|
|
||||||
export let chevron = true;
|
export let chevron = true;
|
||||||
export let onAddLabel: string = '';
|
export let onAddLabel: string = '';
|
||||||
export let onAdd: null | Function = null;
|
export let onAdd: null | Function = null;
|
||||||
|
|
||||||
export let dragAndDrop = true;
|
export let dragAndDrop = true;
|
||||||
|
|
||||||
export let className = '';
|
|
||||||
|
|
||||||
let folderElement;
|
let folderElement;
|
||||||
|
|
||||||
let draggedOver = false;
|
let draggedOver = false;
|
||||||
|
|
@ -138,20 +139,20 @@
|
||||||
>
|
>
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
<div
|
<div
|
||||||
class="w-full group rounded-lg relative flex items-center justify-between hover:bg-gray-100 dark:hover:bg-gray-900 text-gray-500 dark:text-gray-500 transition"
|
class="w-full group rounded-xl relative flex items-center justify-between hover:bg-gray-100 dark:hover:bg-gray-900 transition {buttonClassName}"
|
||||||
>
|
>
|
||||||
<button class="w-full py-1.5 pl-2 flex items-center gap-1.5 text-xs font-medium">
|
<button class="w-full py-1.5 pl-2 flex items-center gap-1.5 text-xs font-medium">
|
||||||
{#if chevron}
|
{#if chevron}
|
||||||
<div class="text-gray-300 dark:text-gray-600 p-[1px]">
|
<div class=" p-[1px]">
|
||||||
{#if open}
|
{#if open}
|
||||||
<ChevronDown className=" size-3.5" strokeWidth="2.5" />
|
<ChevronDown className=" size-3" strokeWidth="2" />
|
||||||
{:else}
|
{:else}
|
||||||
<ChevronRight className=" size-3.5" strokeWidth="2.5" />
|
<ChevronRight className=" size-3" strokeWidth="2" />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="translate-y-[0.5px]">
|
<div class="translate-y-[0.5px] {chevron ? '' : 'pl-0.5'}">
|
||||||
{name}
|
{name}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
199
src/lib/components/common/FullHeightIframe.svelte
Normal file
199
src/lib/components/common/FullHeightIframe.svelte
Normal file
|
|
@ -0,0 +1,199 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { onDestroy, onMount, tick } from 'svelte';
|
||||||
|
|
||||||
|
// Props
|
||||||
|
export let src: string | null = null; // URL or raw HTML (auto-detected)
|
||||||
|
export let title = 'Embedded Content';
|
||||||
|
export let initialHeight: number | null = null; // initial height in px, null = auto
|
||||||
|
|
||||||
|
export let args = null;
|
||||||
|
|
||||||
|
export let allowScripts = true;
|
||||||
|
export let allowForms = false;
|
||||||
|
|
||||||
|
export let allowSameOrigin = false; // set to true only when you trust the content
|
||||||
|
export let allowPopups = false;
|
||||||
|
export let allowDownloads = true;
|
||||||
|
|
||||||
|
export let referrerPolicy: HTMLIFrameElement['referrerPolicy'] =
|
||||||
|
'strict-origin-when-cross-origin';
|
||||||
|
export let allowFullscreen = true;
|
||||||
|
|
||||||
|
let iframe: HTMLIFrameElement | null = null;
|
||||||
|
let iframeSrc: string | null = null;
|
||||||
|
let iframeDoc: string | null = null;
|
||||||
|
|
||||||
|
// Derived: build sandbox attribute from flags
|
||||||
|
$: sandbox =
|
||||||
|
[
|
||||||
|
allowScripts && 'allow-scripts',
|
||||||
|
allowForms && 'allow-forms',
|
||||||
|
allowSameOrigin && 'allow-same-origin',
|
||||||
|
allowPopups && 'allow-popups',
|
||||||
|
allowDownloads && 'allow-downloads'
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ') || undefined;
|
||||||
|
|
||||||
|
// Detect URL vs raw HTML and prep src/srcdoc
|
||||||
|
$: isUrl = typeof src === 'string' && /^(https?:)?\/\//i.test(src);
|
||||||
|
$: if (src) {
|
||||||
|
setIframeSrc();
|
||||||
|
}
|
||||||
|
|
||||||
|
const setIframeSrc = async () => {
|
||||||
|
await tick();
|
||||||
|
if (isUrl) {
|
||||||
|
iframeSrc = src as string;
|
||||||
|
iframeDoc = null;
|
||||||
|
} else {
|
||||||
|
iframeDoc = await processHtmlForDeps(src as string);
|
||||||
|
iframeSrc = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Alpine directives detection
|
||||||
|
const alpineDirectives = [
|
||||||
|
'x-data',
|
||||||
|
'x-init',
|
||||||
|
'x-show',
|
||||||
|
'x-bind',
|
||||||
|
'x-on',
|
||||||
|
'x-text',
|
||||||
|
'x-html',
|
||||||
|
'x-model',
|
||||||
|
'x-modelable',
|
||||||
|
'x-ref',
|
||||||
|
'x-for',
|
||||||
|
'x-if',
|
||||||
|
'x-effect',
|
||||||
|
'x-transition',
|
||||||
|
'x-cloak',
|
||||||
|
'x-ignore',
|
||||||
|
'x-teleport',
|
||||||
|
'x-id'
|
||||||
|
];
|
||||||
|
|
||||||
|
async function processHtmlForDeps(html: string): Promise<string> {
|
||||||
|
if (!allowSameOrigin) return html;
|
||||||
|
|
||||||
|
const scriptTags: string[] = [];
|
||||||
|
|
||||||
|
// --- Alpine.js detection & injection ---
|
||||||
|
const hasAlpineDirectives = alpineDirectives.some((dir) => html.includes(dir));
|
||||||
|
if (hasAlpineDirectives) {
|
||||||
|
try {
|
||||||
|
const { default: alpineCode } = await import('alpinejs/dist/cdn.min.js?raw');
|
||||||
|
const alpineBlob = new Blob([alpineCode], { type: 'text/javascript' });
|
||||||
|
const alpineUrl = URL.createObjectURL(alpineBlob);
|
||||||
|
const alpineTag = `<script src="${alpineUrl}" defer><\/script>`;
|
||||||
|
scriptTags.push(alpineTag);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error processing Alpine for iframe:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Chart.js detection & injection ---
|
||||||
|
const chartJsDirectives = ['new Chart(', 'Chart.'];
|
||||||
|
const hasChartJsDirectives = chartJsDirectives.some((dir) => html.includes(dir));
|
||||||
|
if (hasChartJsDirectives) {
|
||||||
|
try {
|
||||||
|
// import chartUrl from 'chart.js/auto?url';
|
||||||
|
const { default: Chart } = await import('chart.js/auto');
|
||||||
|
(window as any).Chart = Chart;
|
||||||
|
|
||||||
|
const chartTag = `<script>
|
||||||
|
window.Chart = parent.Chart; // Chart previously assigned on parent
|
||||||
|
<\/script>`;
|
||||||
|
scriptTags.push(chartTag);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error processing Chart.js for iframe:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If nothing to inject, return original HTML
|
||||||
|
if (scriptTags.length === 0) return html;
|
||||||
|
|
||||||
|
const tags = scriptTags.join('\n');
|
||||||
|
|
||||||
|
// Prefer injecting into <head>, then before </body>, otherwise prepend
|
||||||
|
if (html.includes('</head>')) {
|
||||||
|
return html.replace('</head>', `${tags}\n</head>`);
|
||||||
|
}
|
||||||
|
if (html.includes('</body>')) {
|
||||||
|
return html.replace('</body>', `${tags}\n</body>`);
|
||||||
|
}
|
||||||
|
return `${tags}\n${html}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to measure same-origin content safely
|
||||||
|
function resizeSameOrigin() {
|
||||||
|
if (!iframe) return;
|
||||||
|
try {
|
||||||
|
const doc = iframe.contentDocument || iframe.contentWindow?.document;
|
||||||
|
console.log('iframe doc:', doc);
|
||||||
|
if (!doc) return;
|
||||||
|
const h = Math.max(doc.documentElement?.scrollHeight ?? 0, doc.body?.scrollHeight ?? 0);
|
||||||
|
if (h > 0) iframe.style.height = h + 20 + 'px';
|
||||||
|
} catch {
|
||||||
|
// Cross-origin → rely on postMessage from inside the iframe
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle height messages from the iframe (we also verify the sender)
|
||||||
|
function onMessage(e: MessageEvent) {
|
||||||
|
if (!iframe || e.source !== iframe.contentWindow) return;
|
||||||
|
const data = e.data as { type?: string; height?: number };
|
||||||
|
if (data?.type === 'iframe:height' && typeof data.height === 'number') {
|
||||||
|
iframe.style.height = Math.max(0, data.height) + 'px';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// When the iframe loads, try same-origin resize (cross-origin will noop)
|
||||||
|
const onLoad = async () => {
|
||||||
|
requestAnimationFrame(resizeSameOrigin);
|
||||||
|
|
||||||
|
// if arguments are provided, inject them into the iframe window
|
||||||
|
if (args && iframe?.contentWindow) {
|
||||||
|
(iframe.contentWindow as any).args = args;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ensure event listener bound only while component lives
|
||||||
|
onMount(() => {
|
||||||
|
window.addEventListener('message', onMessage);
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
window.removeEventListener('message', onMessage);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if iframeDoc}
|
||||||
|
<iframe
|
||||||
|
bind:this={iframe}
|
||||||
|
srcdoc={iframeDoc}
|
||||||
|
{title}
|
||||||
|
class="w-full rounded-2xl"
|
||||||
|
style={`${initialHeight ? `height:${initialHeight}px;` : ''}`}
|
||||||
|
width="100%"
|
||||||
|
frameborder="0"
|
||||||
|
{sandbox}
|
||||||
|
{allowFullscreen}
|
||||||
|
on:load={onLoad}
|
||||||
|
/>
|
||||||
|
{:else if iframeSrc}
|
||||||
|
<iframe
|
||||||
|
bind:this={iframe}
|
||||||
|
src={iframeSrc}
|
||||||
|
{title}
|
||||||
|
class="w-full rounded-2xl"
|
||||||
|
style={`${initialHeight ? `height:${initialHeight}px;` : ''}`}
|
||||||
|
width="100%"
|
||||||
|
frameborder="0"
|
||||||
|
{sandbox}
|
||||||
|
referrerpolicy={referrerPolicy}
|
||||||
|
{allowFullscreen}
|
||||||
|
on:load={onLoad}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
export let show = true;
|
export let show = true;
|
||||||
export let size = 'md';
|
export let size = 'md';
|
||||||
export let containerClassName = 'p-3';
|
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-4xl';
|
||||||
|
|
||||||
let modalElement = null;
|
let modalElement = null;
|
||||||
let mounted = false;
|
let mounted = false;
|
||||||
|
|
@ -87,7 +87,7 @@
|
||||||
bind:this={modalElement}
|
bind:this={modalElement}
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
class="modal fixed top-0 right-0 left-0 bottom-0 bg-black/60 w-full h-screen max-h-[100dvh] {containerClassName} flex justify-center z-9999 overflow-y-auto overscroll-contain"
|
class="modal fixed top-0 right-0 left-0 bottom-0 bg-black/30 dark:bg-black/60 w-full h-screen max-h-[100dvh] {containerClassName} flex justify-center z-9999 overflow-y-auto overscroll-contain"
|
||||||
in:fade={{ duration: 10 }}
|
in:fade={{ duration: 10 }}
|
||||||
on:mousedown={() => {
|
on:mousedown={() => {
|
||||||
show = false;
|
show = false;
|
||||||
|
|
@ -96,7 +96,7 @@
|
||||||
<div
|
<div
|
||||||
class="m-auto max-w-full {sizeToWidth(size)} {size !== 'full'
|
class="m-auto max-w-full {sizeToWidth(size)} {size !== 'full'
|
||||||
? 'mx-2'
|
? 'mx-2'
|
||||||
: ''} shadow-3xl min-h-fit scrollbar-hidden {className}"
|
: ''} shadow-3xl min-h-fit scrollbar-hidden {className} border border-white dark:border-gray-850"
|
||||||
in:flyAndScale
|
in:flyAndScale
|
||||||
on:mousedown={(e) => {
|
on:mousedown={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue