diff --git a/Dockerfile b/Dockerfile index 2a7e9bd20c..d5c40f15e9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -135,29 +135,30 @@ RUN apt-get update && \ # install python dependencies COPY --chown=$UID:$GID ./backend/requirements.txt ./requirements.txt -RUN pip3 install --no-cache-dir uv && \ +RUN set -e; \ + pip3 install --no-cache-dir uv; \ if [ "$USE_CUDA" = "true" ]; then \ # If you use CUDA the whisper and embedding model will be downloaded on first use # fix: pin torch<=2.9.1 - torch 2.10.0 aarch64 wheels cause SIGILL on ARM devices (RPi 4 Cortex-A72) #21349 - pip3 install 'torch<=2.9.1' torchvision torchaudio --index-url https://download.pytorch.org/whl/$USE_CUDA_DOCKER_VER --no-cache-dir && \ - uv pip install --system -r requirements.txt --no-cache-dir && \ - python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \ - python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ.get('AUXILIARY_EMBEDDING_MODEL', 'TaylorAI/bge-micro-v2'), device='cpu')" && \ + pip3 install 'torch<=2.9.1' torchvision torchaudio --index-url https://download.pytorch.org/whl/$USE_CUDA_DOCKER_VER --no-cache-dir; \ + uv pip install --system -r requirements.txt --no-cache-dir; \ + python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')"; \ + python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ.get('AUXILIARY_EMBEDDING_MODEL', 'TaylorAI/bge-micro-v2'), device='cpu')"; \ python -c "import os; from faster_whisper import WhisperModel; WhisperModel(os.environ['WHISPER_MODEL'], device='cpu', compute_type='int8', download_root=os.environ['WHISPER_MODEL_DIR'])"; \ python -c "import os; import tiktoken; tiktoken.get_encoding(os.environ['TIKTOKEN_ENCODING_NAME'])"; \ python -c "import nltk; nltk.download('punkt_tab')"; \ else \ - pip3 install 'torch<=2.9.1' torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu --no-cache-dir && \ - uv pip install --system -r requirements.txt --no-cache-dir && \ + pip3 install 'torch<=2.9.1' torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu --no-cache-dir; \ + uv pip install --system -r requirements.txt --no-cache-dir; \ if [ "$USE_SLIM" != "true" ]; then \ - python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \ - python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ.get('AUXILIARY_EMBEDDING_MODEL', 'TaylorAI/bge-micro-v2'), device='cpu')" && \ + python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')"; \ + python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ.get('AUXILIARY_EMBEDDING_MODEL', 'TaylorAI/bge-micro-v2'), device='cpu')"; \ python -c "import os; from faster_whisper import WhisperModel; WhisperModel(os.environ['WHISPER_MODEL'], device='cpu', compute_type='int8', download_root=os.environ['WHISPER_MODEL_DIR'])"; \ python -c "import os; import tiktoken; tiktoken.get_encoding(os.environ['TIKTOKEN_ENCODING_NAME'])"; \ python -c "import nltk; nltk.download('punkt_tab')"; \ fi; \ fi; \ - mkdir -p /app/backend/data && chown -R $UID:$GID /app/backend/data/ && \ + mkdir -p /app/backend/data; chown -R $UID:$GID /app/backend/data/; \ rm -rf /var/lib/apt/lists/*; # Install Ollama if requested diff --git a/backend/open_webui/env.py b/backend/open_webui/env.py index 30334d9c08..f05f16591e 100644 --- a/backend/open_webui/env.py +++ b/backend/open_webui/env.py @@ -964,6 +964,12 @@ AUDIT_EXCLUDED_PATHS = os.getenv("AUDIT_EXCLUDED_PATHS", "/chats,/chat,/folders" AUDIT_EXCLUDED_PATHS = [path.strip() for path in AUDIT_EXCLUDED_PATHS] AUDIT_EXCLUDED_PATHS = [path.lstrip("/") for path in AUDIT_EXCLUDED_PATHS] +# Comma separated list of urls to include in audit (whitelist mode) +# When set, only these paths are audited and AUDIT_EXCLUDED_PATHS is ignored +AUDIT_INCLUDED_PATHS = os.getenv("AUDIT_INCLUDED_PATHS", "").split(",") +AUDIT_INCLUDED_PATHS = [path.strip() for path in AUDIT_INCLUDED_PATHS] +AUDIT_INCLUDED_PATHS = [path.lstrip("/") for path in AUDIT_INCLUDED_PATHS if path] + #################################### # OPENTELEMETRY @@ -1007,6 +1013,9 @@ OTEL_TRACES_SAMPLER = os.environ.get( ).lower() OTEL_BASIC_AUTH_USERNAME = os.environ.get("OTEL_BASIC_AUTH_USERNAME", "") OTEL_BASIC_AUTH_PASSWORD = os.environ.get("OTEL_BASIC_AUTH_PASSWORD", "") +OTEL_METRICS_EXPORT_INTERVAL_MILLIS = int( + os.environ.get("OTEL_METRICS_EXPORT_INTERVAL_MILLIS", "10000") +) OTEL_METRICS_BASIC_AUTH_USERNAME = os.environ.get( "OTEL_METRICS_BASIC_AUTH_USERNAME", OTEL_BASIC_AUTH_USERNAME diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index 6f4d867e5e..0b583d7ab8 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -469,6 +469,7 @@ from open_webui.env import ( ENABLE_CUSTOM_MODEL_FALLBACK, LICENSE_KEY, AUDIT_EXCLUDED_PATHS, + AUDIT_INCLUDED_PATHS, AUDIT_LOG_LEVEL, CHANGELOG, REDIS_URL, @@ -1585,6 +1586,7 @@ if audit_level != AuditLevel.NONE: AuditLoggingMiddleware, audit_level=audit_level, excluded_paths=AUDIT_EXCLUDED_PATHS, + included_paths=AUDIT_INCLUDED_PATHS, max_body_size=MAX_BODY_LOG_SIZE, ) ################################## diff --git a/backend/open_webui/migrations/versions/8452d01d26d7_add_chat_message_table.py b/backend/open_webui/migrations/versions/8452d01d26d7_add_chat_message_table.py index aa9b0a4c26..fda33d17bf 100644 --- a/backend/open_webui/migrations/versions/8452d01d26d7_add_chat_message_table.py +++ b/backend/open_webui/migrations/versions/8452d01d26d7_add_chat_message_table.py @@ -151,7 +151,12 @@ def upgrade() -> None: continue history = chat_data.get("history", {}) + if not isinstance(history, dict): + continue + messages = history.get("messages", {}) + if not isinstance(messages, dict): + continue for message_id, message in messages.items(): if not isinstance(message, dict): diff --git a/backend/open_webui/routers/tools.py b/backend/open_webui/routers/tools.py index 351c491bdc..0569414d4d 100644 --- a/backend/open_webui/routers/tools.py +++ b/backend/open_webui/routers/tools.py @@ -88,9 +88,15 @@ async def get_tools( # OpenAPI Tool Servers server_access_grants = {} for server in await get_tool_servers(request): - connection = request.app.state.config.TOOL_SERVER_CONNECTIONS[ - server.get("idx", 0) - ] + server_idx = server.get("idx", 0) + connections = request.app.state.config.TOOL_SERVER_CONNECTIONS + if server_idx >= len(connections): + log.warning( + f"Tool server index {server_idx} out of range " + f"(have {len(connections)} connections), skipping server {server.get('id')}" + ) + continue + connection = connections[server_idx] server_config = connection.get("config", {}) server_id = f"server:{server.get('id')}" diff --git a/backend/open_webui/tools/builtin.py b/backend/open_webui/tools/builtin.py index b438759e10..594a7d24ac 100644 --- a/backend/open_webui/tools/builtin.py +++ b/backend/open_webui/tools/builtin.py @@ -168,8 +168,8 @@ async def search_web( engine = __request__.app.state.config.WEB_SEARCH_ENGINE user = UserModel(**__user__) if __user__ else None - # Use admin-configured result count if configured, falling back to model-provided count of provided, else default to 5 - count = __request__.app.state.config.WEB_SEARCH_RESULT_COUNT or count + # Enforce maximum result count from config to prevent abuse + count = count if count < __request__.app.state.config.WEB_SEARCH_RESULT_COUNT else __request__.app.state.config.WEB_SEARCH_RESULT_COUNT results = await asyncio.to_thread(_search_web, __request__, engine, query, user) diff --git a/backend/open_webui/utils/access_control/__init__.py b/backend/open_webui/utils/access_control/__init__.py index a228bbd6a8..b2fb9ece17 100644 --- a/backend/open_webui/utils/access_control/__init__.py +++ b/backend/open_webui/utils/access_control/__init__.py @@ -1,15 +1,21 @@ -from typing import Optional, Set, Union, List, Dict, Any -from open_webui.models.users import Users, UserModel -from open_webui.models.groups import Groups - - -from open_webui.config import DEFAULT_USER_PERMISSIONS import json +from typing import Any + +from open_webui.models.users import UserModel +from open_webui.models.groups import Groups +from open_webui.models.access_grants import ( + has_public_read_access_grant, + has_user_access_grant, + strip_user_access_grants, +) +from open_webui.config import DEFAULT_USER_PERMISSIONS + +from sqlalchemy.orm import Session def fill_missing_permissions( - permissions: Dict[str, Any], default_permissions: Dict[str, Any] -) -> Dict[str, Any]: + permissions: dict[str, Any], default_permissions: dict[str, Any] +) -> dict[str, Any]: """ Recursively fills in missing properties in the permissions dictionary using the default permissions as a template. @@ -27,9 +33,9 @@ def fill_missing_permissions( def get_permissions( user_id: str, - default_permissions: Dict[str, Any], - db: Optional[Any] = None, -) -> Dict[str, Any]: + default_permissions: dict[str, Any], + db: Session | None = None, +) -> dict[str, Any]: """ Get all permissions for a user by combining the permissions of all groups the user is a member of. If a permission is defined in multiple groups, the most permissive value is used (True > False). @@ -37,8 +43,8 @@ def get_permissions( """ def combine_permissions( - permissions: Dict[str, Any], group_permissions: Dict[str, Any] - ) -> Dict[str, Any]: + permissions: dict[str, Any], group_permissions: dict[str, Any] + ) -> dict[str, Any]: """Combine permissions from multiple groups by taking the most permissive value.""" for key, value in group_permissions.items(): if isinstance(value, dict): @@ -72,8 +78,8 @@ def get_permissions( def has_permission( user_id: str, permission_key: str, - default_permissions: Dict[str, Any] = {}, - db: Optional[Any] = None, + default_permissions: dict[str, Any] = {}, + db: Session | None = None, ) -> bool: """ Check if a user has a specific permission by checking the group permissions @@ -82,7 +88,7 @@ def has_permission( Permission keys can be hierarchical and separated by dots ('.'). """ - def get_permission(permissions: Dict[str, Any], keys: List[str]) -> bool: + def get_permission(permissions: dict[str, Any], keys: list[str]) -> bool: """Traverse permissions dict using a list of keys (from dot-split permission_key).""" for key in keys: if key not in permissions: @@ -110,9 +116,9 @@ def has_permission( def has_access( user_id: str, permission: str = "read", - access_grants: Optional[list] = None, - user_group_ids: Optional[Set[str]] = None, - db: Optional[Any] = None, + access_grants: list | None = None, + user_group_ids: set[str] | None = None, + db: Session | None = None, ) -> bool: """ Check if a user has the specified permission using an in-memory access_grants list. @@ -156,7 +162,7 @@ def has_access( def has_connection_access( user: UserModel, connection: dict, - user_group_ids: Optional[Set[str]] = None, + user_group_ids: set[str] | None = None, ) -> bool: """ Check if a user can access a server connection (tool server, terminal, etc.) @@ -194,7 +200,7 @@ def migrate_access_control( if access_control is None and ac_key not in data: return - grants: List[Dict[str, str]] = [] + grants: list[dict[str, str]] = [] if access_control and isinstance(access_control, dict): for perm in ["read", "write"]: perm_data = access_control.get(perm, {}) @@ -221,20 +227,13 @@ def migrate_access_control( data.pop(ac_key, None) -from open_webui.models.access_grants import ( - has_public_read_access_grant, - has_user_access_grant, - strip_user_access_grants, -) - - def filter_allowed_access_grants( - default_permissions: Dict[str, Any], + default_permissions: dict[str, Any], user_id: str, user_role: str, access_grants: list, public_permission_key: str, - db: Optional[Any] = None, + db: Session | None = None, ) -> list: """ Checks if the user has the required permissions to grant access to a resource. diff --git a/backend/open_webui/utils/access_control/files.py b/backend/open_webui/utils/access_control/files.py index e3d52b0f55..c69f3dfa9d 100644 --- a/backend/open_webui/utils/access_control/files.py +++ b/backend/open_webui/utils/access_control/files.py @@ -1,5 +1,4 @@ import logging -from typing import Optional, Any from open_webui.models.users import UserModel from open_webui.models.files import Files @@ -10,14 +9,16 @@ from open_webui.models.groups import Groups from open_webui.models.models import Models from open_webui.models.access_grants import AccessGrants +from sqlalchemy.orm import Session + log = logging.getLogger(__name__) def has_access_to_file( - file_id: Optional[str], + file_id: str | None, access_type: str, user: UserModel, - db: Optional[Any] = None, + db: Session | None = None, ) -> bool: """ Check if a user has the specified access to a file through any of: diff --git a/backend/open_webui/utils/audit.py b/backend/open_webui/utils/audit.py index c4abb445b9..bf3c33774c 100644 --- a/backend/open_webui/utils/audit.py +++ b/backend/open_webui/utils/audit.py @@ -24,7 +24,7 @@ from asgiref.typing import ( from loguru import logger from starlette.requests import Request -from open_webui.env import AUDIT_LOG_LEVEL, MAX_BODY_LOG_SIZE +from open_webui.env import AUDIT_LOG_LEVEL, AUDIT_INCLUDED_PATHS, MAX_BODY_LOG_SIZE from open_webui.utils.auth import get_current_user, get_http_authorization_cred from open_webui.models.users import UserModel @@ -129,15 +129,23 @@ class AuditLoggingMiddleware: app: ASGI3Application, *, excluded_paths: Optional[list[str]] = None, + included_paths: Optional[list[str]] = None, max_body_size: int = MAX_BODY_LOG_SIZE, audit_level: AuditLevel = AuditLevel.NONE, ) -> None: self.app = app self.audit_logger = AuditLogger(logger) self.excluded_paths = excluded_paths or [] + self.included_paths = included_paths or [] self.max_body_size = max_body_size self.audit_level = audit_level + if self.included_paths and self.excluded_paths: + logger.warning( + "Both AUDIT_INCLUDED_PATHS and AUDIT_EXCLUDED_PATHS are set. " + "AUDIT_INCLUDED_PATHS (whitelist) takes precedence." + ) + async def __call__( self, scope: ASGIScope, @@ -226,7 +234,16 @@ class AuditLoggingMiddleware: ): return True - # match either /api//...(for the endpoint /api/chat case) or /api/v1//... + # Whitelist mode: only log paths that match included_paths + if self.included_paths: + pattern = re.compile( + r"^/api(?:/v1)?/(" + "|".join(self.included_paths) + r")\b" + ) + if not pattern.match(request.url.path): + return True # Skip: path not in whitelist + return False # Do NOT skip: path is in whitelist + + # Blacklist mode: skip paths that match excluded_paths pattern = re.compile( r"^/api(?:/v1)?/(" + "|".join(self.excluded_paths) + r")\b" ) diff --git a/backend/open_webui/utils/misc.py b/backend/open_webui/utils/misc.py index 643648862b..809a982e6b 100644 --- a/backend/open_webui/utils/misc.py +++ b/backend/open_webui/utils/misc.py @@ -762,18 +762,22 @@ def parse_ollama_modelfile(model_text): return data -def convert_logit_bias_input_to_json(user_input) -> Optional[str]: - if user_input: - logit_bias_pairs = user_input.split(",") - logit_bias_json = {} - for pair in logit_bias_pairs: - token, bias = pair.split(":") - token = str(token.strip()) - bias = int(bias.strip()) - bias = 100 if bias > 100 else -100 if bias < -100 else bias - logit_bias_json[token] = bias - return json.dumps(logit_bias_json) - return None +def convert_logit_bias_input_to_json(logit_bias_input) -> Optional[str]: + if not logit_bias_input: + return None + + if isinstance(logit_bias_input, dict): + return json.dumps(logit_bias_input) + + logit_bias_pairs = logit_bias_input.split(",") + logit_bias_json = {} + for pair in logit_bias_pairs: + token, bias = pair.split(":") + token = str(token.strip()) + bias = int(bias.strip()) + bias = 100 if bias > 100 else -100 if bias < -100 else bias + logit_bias_json[token] = bias + return json.dumps(logit_bias_json) def freeze(value): diff --git a/backend/open_webui/utils/telemetry/metrics.py b/backend/open_webui/utils/telemetry/metrics.py index cafe779d80..5ff1129b02 100644 --- a/backend/open_webui/utils/telemetry/metrics.py +++ b/backend/open_webui/utils/telemetry/metrics.py @@ -44,11 +44,10 @@ from open_webui.env import ( OTEL_METRICS_BASIC_AUTH_PASSWORD, OTEL_METRICS_OTLP_SPAN_EXPORTER, OTEL_METRICS_EXPORTER_OTLP_INSECURE, + OTEL_METRICS_EXPORT_INTERVAL_MILLIS, ) from open_webui.models.users import Users -_EXPORT_INTERVAL_MILLIS = 10_000 # 10 seconds - def _build_meter_provider(resource: Resource) -> MeterProvider: """Return a configured MeterProvider.""" @@ -67,7 +66,7 @@ def _build_meter_provider(resource: Resource) -> MeterProvider: OTLPHttpMetricExporter( endpoint=OTEL_METRICS_EXPORTER_OTLP_ENDPOINT, headers=headers ), - export_interval_millis=_EXPORT_INTERVAL_MILLIS, + export_interval_millis=OTEL_METRICS_EXPORT_INTERVAL_MILLIS, ) ] else: @@ -78,7 +77,7 @@ def _build_meter_provider(resource: Resource) -> MeterProvider: insecure=OTEL_METRICS_EXPORTER_OTLP_INSECURE, headers=headers, ), - export_interval_millis=_EXPORT_INTERVAL_MILLIS, + export_interval_millis=OTEL_METRICS_EXPORT_INTERVAL_MILLIS, ) ] diff --git a/backend/open_webui/utils/tools.py b/backend/open_webui/utils/tools.py index e525a8284c..e605442612 100644 --- a/backend/open_webui/utils/tools.py +++ b/backend/open_webui/utils/tools.py @@ -275,11 +275,14 @@ async def get_tools( continue tool_server_idx = tool_server_data.get("idx", 0) - tool_server_connection = ( - request.app.state.config.TOOL_SERVER_CONNECTIONS[ - tool_server_idx - ] - ) + connections = request.app.state.config.TOOL_SERVER_CONNECTIONS + if tool_server_idx >= len(connections): + log.warning( + f"Tool server index {tool_server_idx} out of range " + f"(have {len(connections)} connections), skipping server {server_id}" + ) + continue + tool_server_connection = connections[tool_server_idx] # Check access control for tool server if not has_connection_access( diff --git a/backend/requirements.txt b/backend/requirements.txt index 295b6cfa89..490ccdc1dc 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -97,7 +97,7 @@ youtube-transcript-api==1.2.4 pytube==15.0.0 pydub -ddgs==9.11.2 +ddgs==9.11.3 azure-ai-documentintelligence==1.0.2 azure-identity==1.25.2 @@ -119,7 +119,7 @@ pgvector==0.4.2 PyMySQL==1.1.2 boto3==1.42.62 -mariadb==1.1.14 +# mariadb==1.1.14 should be added if you want to support MariaDB pymilvus==2.6.9 qdrant-client==1.17.0 diff --git a/pyproject.toml b/pyproject.toml index 2ec9d033b9..489bba8725 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -105,7 +105,7 @@ dependencies = [ "pytube==15.0.0", "pydub", - "ddgs==9.11.2", + "ddgs==9.11.3", "google-api-python-client", "google-auth-httplib2", diff --git a/src/app.css b/src/app.css index 5eea66bebc..b7b70aeeb3 100644 --- a/src/app.css +++ b/src/app.css @@ -187,31 +187,45 @@ select { @keyframes shimmer { 0% { - background-position: 200% 0; + background-position: 100% 0; } 100% { - background-position: -200% 0; + background-position: -100% 0; } } .shimmer { - background: linear-gradient(90deg, #9a9b9e 25%, #2a2929 50%, #9a9b9e 75%); + background: linear-gradient( + 110deg, + #b4b4b4 0%, + #b4b4b4 43%, + #e8e8e8 50%, + #b4b4b4 57%, + #b4b4b4 100% + ); background-size: 200% 100%; background-clip: text; -webkit-background-clip: text; -webkit-text-fill-color: transparent; - animation: shimmer 4s linear infinite; - color: #818286; /* Fallback color */ + animation: shimmer 1.5s cubic-bezier(0.7, 0, 1, 0.4) infinite; + color: #b4b4b4; } :global(.dark) .shimmer { - background: linear-gradient(90deg, #818286 25%, #eae5e5 50%, #818286 75%); + background: linear-gradient( + 110deg, + #9a9a9a 0%, + #9a9a9a 43%, + #5e5e5e 50%, + #9a9a9a 57%, + #9a9a9a 100% + ); background-size: 200% 100%; background-clip: text; -webkit-background-clip: text; -webkit-text-fill-color: transparent; - animation: shimmer 4s linear infinite; - color: #a1a3a7; /* Darker fallback color for dark mode */ + animation: shimmer 1.5s cubic-bezier(0.7, 0, 1, 0.4) infinite; + color: #9a9a9a; } @keyframes smoothFadeIn { diff --git a/src/lib/components/AddTerminalServerModal.svelte b/src/lib/components/AddTerminalServerModal.svelte index c26460549e..6a29e1fba1 100644 --- a/src/lib/components/AddTerminalServerModal.svelte +++ b/src/lib/components/AddTerminalServerModal.svelte @@ -10,6 +10,8 @@ import XMark from '$lib/components/icons/XMark.svelte'; import AccessControlModal from '$lib/components/workspace/common/AccessControlModal.svelte'; import LockClosed from '$lib/components/icons/LockClosed.svelte'; + import Tooltip from '$lib/components/common/Tooltip.svelte'; + import { getTerminalConfig } from '$lib/apis/terminal'; export let show = false; export let edit = false; @@ -56,6 +58,22 @@ init(); } + const verifyHandler = async () => { + const _url = url.replace(/\/$/, ''); + if (!_url) { + toast.error($i18n.t('Please enter a valid URL')); + return; + } + + const res = await getTerminalConfig(_url, key); + + if (res) { + toast.success($i18n.t('Server connection verified')); + } else { + toast.error($i18n.t('Server connection failed')); + } + }; + const submitHandler = () => { if (url === '') { toast.error($i18n.t('Please enter a valid URL')); @@ -176,6 +194,31 @@ /> + + + +
diff --git a/src/lib/components/channel/MessageInput/MentionList.svelte b/src/lib/components/channel/MessageInput/MentionList.svelte index a74aec5dac..6d7209f450 100644 --- a/src/lib/components/channel/MessageInput/MentionList.svelte +++ b/src/lib/components/channel/MessageInput/MentionList.svelte @@ -122,7 +122,11 @@ } if (modelSuggestions) { - _models = [...$models.map((m) => ({ type: 'model', id: m.id, label: m.name, data: m }))]; + _models = [ + ...$models + .filter((m) => !m?.direct) + .map((m) => ({ type: 'model', id: m.id, label: m.name, data: m })) + ]; } } diff --git a/src/lib/components/chat/FileNav.svelte b/src/lib/components/chat/FileNav.svelte index 81ee750a22..6b0b13872b 100644 --- a/src/lib/components/chat/FileNav.svelte +++ b/src/lib/components/chat/FileNav.svelte @@ -30,7 +30,8 @@ import Folder from '../icons/Folder.svelte'; import Document from '../icons/Document.svelte'; import PenAlt from '../icons/PenAlt.svelte'; - import Reset from '../icons/Reset.svelte'; + import ZoomReset from '../icons/ZoomReset.svelte'; + import Spinner from '../common/Spinner.svelte'; import Tooltip from '../common/Tooltip.svelte'; import ConfirmDialog from '../common/ConfirmDialog.svelte'; @@ -172,6 +173,9 @@ if (terminal && terminal.url !== prevTerminalUrl) { prevTerminalUrl = terminal.url; + loading = true; + error = null; + entries = []; (async () => { // Discover server features (terminal enabled/disabled) const config = await getTerminalConfig(terminal.url, terminal.key); @@ -531,6 +535,7 @@ }); if (!handledDisplayFile) { + loading = true; if (savedPath === '/') { const rawCwd = await getCwd(terminal.url, terminal.key); const cwd = rawCwd ? normalizePath(rawCwd) : null; @@ -652,7 +657,7 @@ on:click={() => filePreviewRef?.resetImageView()} aria-label={$i18n.t('Reset view')} > - + {/if} @@ -663,7 +668,7 @@ on:click={() => filePreviewRef?.resetPdfView()} aria-label={$i18n.t('Reset view')} > - + {/if} diff --git a/src/lib/components/chat/MessageInput.svelte b/src/lib/components/chat/MessageInput.svelte index 4664940d3a..9bd8f13d94 100644 --- a/src/lib/components/chat/MessageInput.svelte +++ b/src/lib/components/chat/MessageInput.svelte @@ -82,7 +82,7 @@ import InputVariablesModal from './MessageInput/InputVariablesModal.svelte'; import Voice from '../icons/Voice.svelte'; - import Cloud from '../icons/Cloud.svelte'; + import Terminal from '../icons/Terminal.svelte'; import IntegrationsMenu from './MessageInput/IntegrationsMenu.svelte'; import TerminalMenu from './MessageInput/TerminalMenu.svelte'; import Component from '../icons/Component.svelte'; @@ -1756,7 +1756,7 @@ ? 'm-1' : 'focus:outline-hidden rounded-full'}" > - +