diff --git a/CHANGELOG.md b/CHANGELOG.md index 72ffba1114..8cacf29521 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,36 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.6.40] - 2025-11-25 + +### Fixed + +- 🗄️ A critical PostgreSQL user listing performance issue was resolved by removing a redundant count operation that caused severe database slowdowns and potential timeouts when viewing user lists in admin panels. + +## [0.6.39] - 2025-11-25 + +### Added + +- 💬 A user list modal was added to channels, displaying all users with access and featuring search, sorting, and pagination capabilities. [Commit](https://github.com/open-webui/open-webui/commit/c0e120353824be00a2ef63cbde8be5d625bd6fd0) +- 💬 Channel navigation now displays the total number of users with access to the channel. [Commit](https://github.com/open-webui/open-webui/commit/3b5710d0cd445cf86423187f5ee7c40472a0df0b) +- 🔌 Tool servers and MCP connections now support function name filtering, allowing administrators to selectively enable or block specific functions using allow/block lists. [Commit](https://github.com/open-webui/open-webui/commit/743199f2d097ae1458381bce450d9025a0ab3f3d) +- ⚡ A toggle to disable parallel embedding processing was added via "ENABLE_ASYNC_EMBEDDING", allowing sequential processing for rate-limited or resource-constrained local embedding setups. [#19444](https://github.com/open-webui/open-webui/pull/19444) +- 🔄 Various improvements were implemented across the frontend and backend to enhance performance, stability, and security. +- 🌐 Localization improvements were made for German (de-DE) and Portuguese (Brazil) translations. + +### Fixed + +- 📝 Inline citations now render correctly within markdown lists and nested elements instead of displaying as "undefined" values. [#19452](https://github.com/open-webui/open-webui/issues/19452) +- 👥 Group member selection now works correctly without randomly selecting other users or causing the user list to jump around. [#19426](https://github.com/open-webui/open-webui/issues/19426) +- 👥 Admin panel user list now displays the correct total user count and properly paginates 30 items per page after fixing database query issues with group member joins. [#19429](https://github.com/open-webui/open-webui/issues/19429) +- 🔍 Knowledge base reindexing now works correctly after resolving async execution chain issues by implementing threadpool workers for embedding operations. [#19434](https://github.com/open-webui/open-webui/pull/19434) +- 🖼️ OpenAI image generation now works correctly after fixing a connection adapter error caused by incorrect URL formatting. [#19435](https://github.com/open-webui/open-webui/pull/19435) + +### Changed + +- 🔧 BREAKING: Docling configuration has been consolidated from individual environment variables into a single "DOCLING_PARAMS" JSON configuration and now supports API key authentication via "DOCLING_API_KEY", requiring users to migrate existing Docling settings to the new format. [#16841](https://github.com/open-webui/open-webui/issues/16841), [#19427](https://github.com/open-webui/open-webui/pull/19427) +- 🔧 The environment variable "REPLACE_IMAGE_URLS_IN_CHAT_RESPONSE" has been renamed to "ENABLE_CHAT_RESPONSE_BASE64_IMAGE_URL_CONVERSION" for naming consistency. + ## [0.6.38] - 2025-11-24 ### Fixed diff --git a/backend/open_webui/env.py b/backend/open_webui/env.py index 651629b950..e3c50ea8d1 100644 --- a/backend/open_webui/env.py +++ b/backend/open_webui/env.py @@ -561,7 +561,8 @@ else: #################################### ENABLE_CHAT_RESPONSE_BASE64_IMAGE_URL_CONVERSION = ( - os.environ.get("REPLACE_IMAGE_URLS_IN_CHAT_RESPONSE", "False").lower() == "true" + os.environ.get("ENABLE_CHAT_RESPONSE_BASE64_IMAGE_URL_CONVERSION", "False").lower() + == "true" ) CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE = os.environ.get( diff --git a/backend/open_webui/models/channels.py b/backend/open_webui/models/channels.py index 2a14e7a2d5..5f4d1436d9 100644 --- a/backend/open_webui/models/channels.py +++ b/backend/open_webui/models/channels.py @@ -59,6 +59,7 @@ class ChannelModel(BaseModel): class ChannelResponse(ChannelModel): write_access: bool = False + user_count: Optional[int] = None class ChannelForm(BaseModel): diff --git a/backend/open_webui/models/groups.py b/backend/open_webui/models/groups.py index 1d96f5cfaa..e5c0612639 100644 --- a/backend/open_webui/models/groups.py +++ b/backend/open_webui/models/groups.py @@ -177,6 +177,23 @@ class GroupTable: return [m[0] for m in members] + def get_group_user_ids_by_ids(self, group_ids: list[str]) -> dict[str, list[str]]: + with get_db() as db: + members = ( + db.query(GroupMember.group_id, GroupMember.user_id) + .filter(GroupMember.group_id.in_(group_ids)) + .all() + ) + + group_user_ids: dict[str, list[str]] = { + group_id: [] for group_id in group_ids + } + + for group_id, user_id in members: + group_user_ids[group_id].append(user_id) + + return group_user_ids + def set_group_user_ids_by_id(self, group_id: str, user_ids: list[str]) -> None: with get_db() as db: # Delete existing members diff --git a/backend/open_webui/models/models.py b/backend/open_webui/models/models.py index e902a978d1..329b87a91f 100755 --- a/backend/open_webui/models/models.py +++ b/backend/open_webui/models/models.py @@ -220,6 +220,34 @@ class ModelsTable: or has_access(user_id, permission, model.access_control, user_group_ids) ] + def _has_write_permission(self, query, filter: dict): + if filter.get("group_ids") or filter.get("user_id"): + conditions = [] + + # --- ANY group_ids match ("write".group_ids) --- + if filter.get("group_ids"): + group_ids = filter["group_ids"] + like_clauses = [] + + for gid in group_ids: + like_clauses.append( + cast(Model.access_control, String).like( + f'%"write"%"group_ids"%"{gid}"%' + ) + ) + + # ANY → OR + conditions.append(or_(*like_clauses)) + + # --- user_id match (owner) --- + if filter.get("user_id"): + conditions.append(Model.user_id == filter["user_id"]) + + # Apply OR across the two groups of conditions + query = query.filter(or_(*conditions)) + + return query + def search_models( self, user_id: str, filter: dict = {}, skip: int = 0, limit: int = 30 ) -> ModelListResponse: @@ -238,11 +266,10 @@ class ModelsTable: ) ) - if filter.get("user_id"): - query = query.filter(Model.user_id == filter.get("user_id")) + # Apply access control filtering + query = self._has_write_permission(query, filter) view_option = filter.get("view_option") - if view_option == "created": query = query.filter(Model.user_id == user_id) elif view_option == "shared": diff --git a/backend/open_webui/models/users.py b/backend/open_webui/models/users.py index 9779731a47..d93f7ddeb3 100644 --- a/backend/open_webui/models/users.py +++ b/backend/open_webui/models/users.py @@ -99,7 +99,16 @@ class UserGroupIdsModel(UserModel): group_ids: list[str] = [] +class UserModelResponse(UserModel): + model_config = ConfigDict(extra="allow") + + class UserListResponse(BaseModel): + users: list[UserModelResponse] + total: int + + +class UserGroupIdsListResponse(BaseModel): users: list[UserGroupIdsModel] total: int @@ -239,6 +248,37 @@ class UsersTable: ) ) + user_ids = filter.get("user_ids") + group_ids = filter.get("group_ids") + + if isinstance(user_ids, list) and isinstance(group_ids, list): + # If both are empty lists, return no users + if not user_ids and not group_ids: + return {"users": [], "total": 0} + + if user_ids: + query = query.filter(User.id.in_(user_ids)) + + if group_ids: + query = query.filter( + exists( + select(GroupMember.id).where( + GroupMember.user_id == User.id, + GroupMember.group_id.in_(group_ids), + ) + ) + ) + + roles = filter.get("roles") + if roles: + include_roles = [role for role in roles if not role.startswith("!")] + exclude_roles = [role[1:] for role in roles if role.startswith("!")] + + if include_roles: + query = query.filter(User.role.in_(include_roles)) + if exclude_roles: + query = query.filter(~User.role.in_(exclude_roles)) + order_by = filter.get("order_by") direction = filter.get("direction") @@ -300,7 +340,6 @@ class UsersTable: query = query.order_by(User.created_at.desc()) # Count BEFORE pagination - query = query.distinct(User.id) total = query.count() # correct pagination logic diff --git a/backend/open_webui/routers/channels.py b/backend/open_webui/routers/channels.py index fda0879594..e47c98554e 100644 --- a/backend/open_webui/routers/channels.py +++ b/backend/open_webui/routers/channels.py @@ -7,8 +7,17 @@ from fastapi import APIRouter, Depends, HTTPException, Request, status, Backgrou from pydantic import BaseModel -from open_webui.socket.main import sio, get_user_ids_from_room -from open_webui.models.users import Users, UserNameResponse +from open_webui.socket.main import ( + sio, + get_user_ids_from_room, + get_active_status_by_user_id, +) +from open_webui.models.users import ( + UserListResponse, + UserModelResponse, + Users, + UserNameResponse, +) from open_webui.models.groups import Groups from open_webui.models.channels import ( @@ -38,7 +47,11 @@ from open_webui.utils.chat import generate_chat_completion from open_webui.utils.auth import get_admin_user, get_verified_user -from open_webui.utils.access_control import has_access, get_users_with_access +from open_webui.utils.access_control import ( + has_access, + get_users_with_access, + get_permitted_group_and_user_ids, +) from open_webui.utils.webhook import post_webhook from open_webui.utils.channels import extract_mentions, replace_mentions @@ -105,14 +118,73 @@ async def get_channel_by_id(id: str, user=Depends(get_verified_user)): user.id, type="write", access_control=channel.access_control, strict=False ) + user_count = len(get_users_with_access("read", channel.access_control)) + return ChannelResponse( **{ **channel.model_dump(), "write_access": write_access or user.role == "admin", + "user_count": user_count, } ) +PAGE_ITEM_COUNT = 30 + + +@router.get("/{id}/users", response_model=UserListResponse) +async def get_channel_users_by_id( + id: str, + query: Optional[str] = None, + order_by: Optional[str] = None, + direction: Optional[str] = None, + page: Optional[int] = 1, + user=Depends(get_verified_user), +): + + channel = Channels.get_channel_by_id(id) + if not channel: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND + ) + + limit = PAGE_ITEM_COUNT + + page = max(1, page) + skip = (page - 1) * limit + + filter = { + "roles": ["!pending"], + } + + if query: + filter["query"] = query + if order_by: + filter["order_by"] = order_by + if direction: + filter["direction"] = direction + + permitted_ids = get_permitted_group_and_user_ids("read", channel.access_control) + if permitted_ids: + filter["user_ids"] = permitted_ids.get("user_ids") + filter["group_ids"] = permitted_ids.get("group_ids") + + result = Users.get_users(filter=filter, skip=skip, limit=limit) + + users = result["users"] + total = result["total"] + + return { + "users": [ + UserModelResponse( + **user.model_dump(), is_active=get_active_status_by_user_id(user.id) + ) + for user in users + ], + "total": total, + } + + ############################ # UpdateChannelById ############################ diff --git a/backend/open_webui/routers/models.py b/backend/open_webui/routers/models.py index 93d8cb8bf7..df5a7377dc 100644 --- a/backend/open_webui/routers/models.py +++ b/backend/open_webui/routers/models.py @@ -5,6 +5,7 @@ import json import asyncio import logging +from open_webui.models.groups import Groups from open_webui.models.models import ( ModelForm, ModelModel, @@ -78,6 +79,10 @@ async def get_models( filter["direction"] = direction if not user.role == "admin" or not BYPASS_ADMIN_ACCESS_CONTROL: + groups = Groups.get_groups_by_member_id(user.id) + if groups: + filter["group_ids"] = [group.id for group in groups] + filter["user_id"] = user.id return Models.search_models(user.id, filter=filter, skip=skip, limit=limit) diff --git a/backend/open_webui/routers/retrieval.py b/backend/open_webui/routers/retrieval.py index 600c33afa1..6080337250 100644 --- a/backend/open_webui/routers/retrieval.py +++ b/backend/open_webui/routers/retrieval.py @@ -1441,6 +1441,9 @@ def process_file( form_data: ProcessFileForm, user=Depends(get_verified_user), ): + """ + Process a file and save its content to the vector database. + """ if user.role == "admin": file = Files.get_file_by_id(form_data.file_id) else: @@ -1667,7 +1670,7 @@ class ProcessTextForm(BaseModel): @router.post("/process/text") -def process_text( +async def process_text( request: Request, form_data: ProcessTextForm, user=Depends(get_verified_user), @@ -1685,7 +1688,9 @@ def process_text( text_content = form_data.content log.debug(f"text_content: {text_content}") - result = save_docs_to_vector_db(request, docs, collection_name, user=user) + result = await run_in_threadpool( + save_docs_to_vector_db, request, docs, collection_name, user + ) if result: return { "status": True, @@ -1701,7 +1706,7 @@ def process_text( @router.post("/process/youtube") @router.post("/process/web") -def process_web( +async def process_web( request: Request, form_data: ProcessUrlForm, user=Depends(get_verified_user) ): try: @@ -1709,16 +1714,14 @@ def process_web( if not collection_name: collection_name = calculate_sha256_string(form_data.url)[:63] - content, docs = get_content_from_url(request, form_data.url) + content, docs = await run_in_threadpool( + get_content_from_url, request, form_data.url + ) log.debug(f"text_content: {content}") if not request.app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL: - save_docs_to_vector_db( - request, - docs, - collection_name, - overwrite=True, - user=user, + await run_in_threadpool( + save_docs_to_vector_db, request, docs, collection_name, True, user ) else: collection_name = None @@ -2405,7 +2408,7 @@ class BatchProcessFilesResponse(BaseModel): @router.post("/process/files/batch") -def process_files_batch( +async def process_files_batch( request: Request, form_data: BatchProcessFilesForm, user=Depends(get_verified_user), @@ -2460,12 +2463,8 @@ def process_files_batch( # Save all documents in one batch if all_docs: try: - save_docs_to_vector_db( - request=request, - docs=all_docs, - collection_name=collection_name, - add=True, - user=user, + await run_in_threadpool( + save_docs_to_vector_db, request, all_docs, collection_name, True, user ) # Update all files with collection name diff --git a/backend/open_webui/routers/users.py b/backend/open_webui/routers/users.py index f53b0e2749..f9e1c220a9 100644 --- a/backend/open_webui/routers/users.py +++ b/backend/open_webui/routers/users.py @@ -6,7 +6,7 @@ import io from fastapi import APIRouter, Depends, HTTPException, Request, status from fastapi.responses import Response, StreamingResponse, FileResponse -from pydantic import BaseModel +from pydantic import BaseModel, ConfigDict from open_webui.models.auths import Auths @@ -17,7 +17,7 @@ from open_webui.models.chats import Chats from open_webui.models.users import ( UserModel, UserGroupIdsModel, - UserListResponse, + UserGroupIdsListResponse, UserInfoListResponse, UserIdNameListResponse, UserRoleUpdateForm, @@ -76,7 +76,7 @@ async def get_active_users( PAGE_ITEM_COUNT = 30 -@router.get("/", response_model=UserListResponse) +@router.get("/", response_model=UserGroupIdsListResponse) async def get_users( query: Optional[str] = None, order_by: Optional[str] = None, @@ -363,6 +363,7 @@ class UserResponse(BaseModel): name: str profile_image_url: str active: Optional[bool] = None + model_config = ConfigDict(extra="allow") @router.get("/{user_id}", response_model=UserResponse) @@ -385,6 +386,7 @@ async def get_user_by_id(user_id: str, user=Depends(get_verified_user)): if user: return UserResponse( **{ + "id": user.id, "name": user.name, "profile_image_url": user.profile_image_url, "active": get_active_status_by_user_id(user_id), diff --git a/backend/open_webui/utils/access_control.py b/backend/open_webui/utils/access_control.py index af48bebfb4..97d0b41491 100644 --- a/backend/open_webui/utils/access_control.py +++ b/backend/open_webui/utils/access_control.py @@ -105,6 +105,22 @@ def has_permission( return get_permission(default_permissions, permission_hierarchy) +def get_permitted_group_and_user_ids( + type: str = "write", access_control: Optional[dict] = None +) -> Union[Dict[str, List[str]], None]: + if access_control is None: + return None + + permission_access = access_control.get(type, {}) + permitted_group_ids = permission_access.get("group_ids", []) + permitted_user_ids = permission_access.get("user_ids", []) + + return { + "group_ids": permitted_group_ids, + "user_ids": permitted_user_ids, + } + + def has_access( user_id: str, type: str = "write", @@ -122,9 +138,12 @@ def has_access( user_groups = Groups.get_groups_by_member_id(user_id) user_group_ids = {group.id for group in user_groups} - permission_access = access_control.get(type, {}) - permitted_group_ids = permission_access.get("group_ids", []) - permitted_user_ids = permission_access.get("user_ids", []) + permitted_ids = get_permitted_group_and_user_ids(type, access_control) + if permitted_ids is None: + return False + + permitted_group_ids = permitted_ids.get("group_ids", []) + permitted_user_ids = permitted_ids.get("user_ids", []) return user_id in permitted_user_ids or any( group_id in permitted_group_ids for group_id in user_group_ids @@ -136,18 +155,20 @@ def get_users_with_access( type: str = "write", access_control: Optional[dict] = None ) -> list[UserModel]: if access_control is None: - result = Users.get_users() + result = Users.get_users(filter={"roles": ["!pending"]}) return result.get("users", []) - permission_access = access_control.get(type, {}) - permitted_group_ids = permission_access.get("group_ids", []) - permitted_user_ids = permission_access.get("user_ids", []) + permitted_ids = get_permitted_group_and_user_ids(type, access_control) + if permitted_ids is None: + return [] + + permitted_group_ids = permitted_ids.get("group_ids", []) + permitted_user_ids = permitted_ids.get("user_ids", []) user_ids_with_access = set(permitted_user_ids) - for group_id in permitted_group_ids: - group_user_ids = Groups.get_group_user_ids_by_id(group_id) - if group_user_ids: - user_ids_with_access.update(group_user_ids) + group_user_ids_map = Groups.get_group_user_ids_by_ids(permitted_group_ids) + for user_ids in group_user_ids_map.values(): + user_ids_with_access.update(user_ids) return Users.get_users_by_user_ids(list(user_ids_with_access)) diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index 323f93f450..efa187a382 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -1409,9 +1409,12 @@ async def process_chat_payload(request, form_data, user, metadata, model): headers=headers if headers else None, ) - function_name_filter_list = mcp_server_connection.get( - "function_name_filter_list", None + function_name_filter_list = ( + mcp_server_connection.get("config", {}) + .get("function_name_filter_list", "") + .split(",") ) + tool_specs = await mcp_clients[server_id].list_tool_specs() for tool_spec in tool_specs: @@ -1424,9 +1427,7 @@ async def process_chat_payload(request, form_data, user, metadata, model): return tool_function - if function_name_filter_list and isinstance( - function_name_filter_list, list - ): + if function_name_filter_list: if not is_string_allowed( tool_spec["name"], function_name_filter_list ): diff --git a/backend/open_webui/utils/misc.py b/backend/open_webui/utils/misc.py index 466e235598..5591fcdb3f 100644 --- a/backend/open_webui/utils/misc.py +++ b/backend/open_webui/utils/misc.py @@ -35,10 +35,10 @@ def get_allow_block_lists(filter_list): for d in filter_list: if d.startswith("!"): # Domains starting with "!" → blocked - block_list.append(d[1:]) + block_list.append(d[1:].strip()) else: # Domains starting without "!" → allowed - allow_list.append(d) + allow_list.append(d.strip()) return allow_list, block_list @@ -54,6 +54,8 @@ def is_string_allowed(string: str, filter_list: Optional[list[str]] = None) -> b return True allow_list, block_list = get_allow_block_lists(filter_list) + print(string, allow_list, block_list) + # If allow list is non-empty, require domain to match one of them if allow_list: if not any(string.endswith(allowed) for allowed in allow_list): diff --git a/backend/open_webui/utils/tools.py b/backend/open_webui/utils/tools.py index ecdf7187e4..268624135d 100644 --- a/backend/open_webui/utils/tools.py +++ b/backend/open_webui/utils/tools.py @@ -150,15 +150,15 @@ async def get_tools( ) specs = tool_server_data.get("specs", []) - function_name_filter_list = tool_server_connection.get( - "function_name_filter_list", None + function_name_filter_list = ( + tool_server_connection.get("config", {}) + .get("function_name_filter_list", "") + .split(",") ) for spec in specs: function_name = spec["name"] - if function_name_filter_list and isinstance( - function_name_filter_list, list - ): + if function_name_filter_list: if not is_string_allowed( function_name, function_name_filter_list ): diff --git a/backend/requirements-min.txt b/backend/requirements-min.txt index bc4732fc1d..c09f1af820 100644 --- a/backend/requirements-min.txt +++ b/backend/requirements-min.txt @@ -7,7 +7,7 @@ pydantic==2.11.9 python-multipart==0.0.20 itsdangerous==2.2.0 -python-socketio==5.13.0 +python-socketio==5.14.0 python-jose==3.5.0 cryptography bcrypt==5.0.0 @@ -47,4 +47,5 @@ fake-useragent==2.2.0 chromadb==1.1.0 black==25.9.0 -pydub \ No newline at end of file +pydub +chardet==5.2.0 diff --git a/backend/requirements.txt b/backend/requirements.txt index db32255a89..658e249090 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -4,7 +4,7 @@ pydantic==2.11.9 python-multipart==0.0.20 itsdangerous==2.2.0 -python-socketio==5.13.0 +python-socketio==5.14.0 python-jose==3.5.0 cryptography bcrypt==5.0.0 @@ -59,6 +59,7 @@ pyarrow==20.0.0 # fix: pin pyarrow version to 20 for rpi compatibility #15897 einops==0.8.1 ftfy==6.2.3 +chardet==5.2.0 pypdf==6.0.0 fpdf2==2.8.2 pymdown-extensions==10.14.2 diff --git a/package-lock.json b/package-lock.json index 94cde05b1b..a422bb732e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "open-webui", - "version": "0.6.38", + "version": "0.6.40", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "open-webui", - "version": "0.6.38", + "version": "0.6.40", "dependencies": { "@azure/msal-browser": "^4.5.0", "@codemirror/lang-javascript": "^6.2.2", diff --git a/package.json b/package.json index 3ee4b5680d..97bdda0871 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "open-webui", - "version": "0.6.38", + "version": "0.6.40", "private": true, "scripts": { "dev": "npm run pyodide:fetch && vite dev --host", diff --git a/pyproject.toml b/pyproject.toml index 85a8044e3c..f0568a4237 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ dependencies = [ "python-multipart==0.0.20", "itsdangerous==2.2.0", - "python-socketio==5.13.0", + "python-socketio==5.14.0", "python-jose==3.5.0", "cryptography", "bcrypt==5.0.0", @@ -67,6 +67,7 @@ dependencies = [ "einops==0.8.1", "ftfy==6.2.3", + "chardet==5.2.0", "pypdf==6.0.0", "fpdf2==2.8.2", "pymdown-extensions==10.14.2", diff --git a/src/lib/apis/channels/index.ts b/src/lib/apis/channels/index.ts index ac51e5a5d0..2872bd89f8 100644 --- a/src/lib/apis/channels/index.ts +++ b/src/lib/apis/channels/index.ts @@ -101,6 +101,60 @@ export const getChannelById = async (token: string = '', channel_id: string) => return res; }; +export const getChannelUsersById = async ( + token: string, + channel_id: string, + query?: string, + orderBy?: string, + direction?: string, + page = 1 +) => { + let error = null; + let res = null; + + const searchParams = new URLSearchParams(); + + searchParams.set('page', `${page}`); + + if (query) { + searchParams.set('query', query); + } + + if (orderBy) { + searchParams.set('order_by', orderBy); + } + + if (direction) { + searchParams.set('direction', direction); + } + + res = await fetch( + `${WEBUI_API_BASE_URL}/channels/${channel_id}/users?${searchParams.toString()}`, + { + 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 updateChannelById = async ( token: string = '', channel_id: string, diff --git a/src/lib/apis/retrieval/index.ts b/src/lib/apis/retrieval/index.ts index 6df927fec6..5cb0f60a72 100644 --- a/src/lib/apis/retrieval/index.ts +++ b/src/lib/apis/retrieval/index.ts @@ -295,42 +295,6 @@ export interface SearchDocument { filenames: string[]; } -export const processFile = async ( - token: string, - file_id: string, - collection_name: string | null = null -) => { - let error = null; - - const res = await fetch(`${RETRIEVAL_API_BASE_URL}/process/file`, { - method: 'POST', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - authorization: `Bearer ${token}` - }, - body: JSON.stringify({ - file_id: file_id, - collection_name: collection_name ? collection_name : undefined - }) - }) - .then(async (res) => { - if (!res.ok) throw await res.json(); - return res.json(); - }) - .catch((err) => { - error = err.detail; - console.error(err); - return null; - }); - - if (error) { - throw error; - } - - return res; -}; - export const processYoutubeVideo = async (token: string, url: string) => { let error = null; diff --git a/src/lib/components/admin/Settings/Documents.svelte b/src/lib/components/admin/Settings/Documents.svelte index 8186430a92..548583ee8a 100644 --- a/src/lib/components/admin/Settings/Documents.svelte +++ b/src/lib/components/admin/Settings/Documents.svelte @@ -135,11 +135,6 @@ if (res) { console.debug('embeddingModelUpdateHandler:', res); - if (res.status === true) { - toast.success($i18n.t('Embedding model set to "{{embedding_model}}"', res), { - duration: 1000 * 10 - }); - } } }; @@ -1355,6 +1350,7 @@
+
+ +
+
+
{ + e.preventDefault(); + submitHandler(); + }} + > +
+ +
+
+
+
+ + +{/if} diff --git a/src/lib/components/channel/ChannelInfoModal/UserList.svelte b/src/lib/components/channel/ChannelInfoModal/UserList.svelte new file mode 100644 index 0000000000..a38ad352f9 --- /dev/null +++ b/src/lib/components/channel/ChannelInfoModal/UserList.svelte @@ -0,0 +1,236 @@ + + +
+ {#if users === null || total === null} +
+ +
+ {:else} +
+
+
+
+ + + +
+ +
+
+
+ + {#if users.length > 0} +
+
+
+
+ + + +
+
+
+ {#each users as user, userIdx} +
+
+
+ + user + + +
{user.name}
+
+ + {#if user?.is_active} +
+ + + + +
+ {/if} +
+
+ +
+
+ +
+
+
+ {/each} +
+
+
+ + {#if total > 30} + + {/if} + {:else} +
+ {$i18n.t('No users were found.')} +
+ {/if} + {/if} +
diff --git a/src/lib/components/channel/Messages/Message/ProfilePreview.svelte b/src/lib/components/channel/Messages/Message/ProfilePreview.svelte index 620905e5ff..8ed95e63e8 100644 --- a/src/lib/components/channel/Messages/Message/ProfilePreview.svelte +++ b/src/lib/components/channel/Messages/Message/ProfilePreview.svelte @@ -7,6 +7,10 @@ import UserStatusLinkPreview from './UserStatusLinkPreview.svelte'; export let user = null; + + export let align = 'center'; + export let side = 'right'; + export let sideOffset = 8; @@ -14,5 +18,5 @@ - + diff --git a/src/lib/components/channel/Messages/Message/UserStatusLinkPreview.svelte b/src/lib/components/channel/Messages/Message/UserStatusLinkPreview.svelte index 0660548891..93472226ed 100644 --- a/src/lib/components/channel/Messages/Message/UserStatusLinkPreview.svelte +++ b/src/lib/components/channel/Messages/Message/UserStatusLinkPreview.svelte @@ -27,7 +27,7 @@ {#if user} -