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 @@