diff --git a/backend/open_webui/models/chats.py b/backend/open_webui/models/chats.py index f96ec76e2a..ca1a2dcd67 100644 --- a/backend/open_webui/models/chats.py +++ b/backend/open_webui/models/chats.py @@ -984,6 +984,69 @@ class ChatTable: result = await session.execute(stmt) return [meta for meta in result.scalars().all() if isinstance(meta, dict)] + async def get_chats_by_model_id( + self, + model_id: str, + filter: dict | None = None, + skip: int = 0, + limit: int = 50, + db: AsyncSession | None = None, + ) -> dict: + from open_webui.models.users import User + + async with get_async_db_context(db) as session: + chat_ids = select(ChatMessage.chat_id).filter(ChatMessage.model_id == model_id).group_by(ChatMessage.chat_id) + + if filter: + if filter.get('start_date'): + chat_ids = chat_ids.filter(ChatMessage.created_at >= filter.get('start_date')) + if filter.get('end_date'): + chat_ids = chat_ids.filter(ChatMessage.created_at <= filter.get('end_date')) + + chat_ids = chat_ids.subquery() + + stmt = ( + select(Chat.id, Chat.user_id, Chat.title, Chat.updated_at, User.name.label('user_name')) + .join(chat_ids, chat_ids.c.chat_id == Chat.id) + .outerjoin(User, User.id == Chat.user_id) + ) + + order_by = filter.get('order_by') if filter else None + direction = filter.get('direction') if filter else None + is_asc = direction == 'asc' + + if order_by == 'title': + primary_sort = Chat.title.asc() if is_asc else Chat.title.desc() + elif order_by == 'user_name': + primary_sort = User.name.asc() if is_asc else User.name.desc() + else: + primary_sort = Chat.updated_at.asc() if is_asc else Chat.updated_at.desc() + + stmt = stmt.order_by(primary_sort, Chat.id.asc()) + + count_result = await session.execute(select(func.count()).select_from(stmt.subquery())) + total = count_result.scalar() + + if skip: + stmt = stmt.offset(skip) + if limit: + stmt = stmt.limit(limit) + + result = await session.execute(stmt) + return { + 'items': [ + { + 'chat_id': chat.id, + 'user_id': chat.user_id, + 'user_name': chat.user_name, + 'first_message': chat.title, + 'updated_at': chat.updated_at, + } + for chat in result.all() + ], + 'total': total, + } + # retrieve conversation async def get_chat_by_id( self, diff --git a/backend/open_webui/routers/analytics.py b/backend/open_webui/routers/analytics.py index 6567c7540a..d9cf8e0a61 100644 --- a/backend/open_webui/routers/analytics.py +++ b/backend/open_webui/routers/analytics.py @@ -279,6 +279,9 @@ class ModelChatsResponse(BaseModel): total: int +MODEL_CHAT_ORDER_FIELDS = {'title', 'updated_at', 'user_name'} + + @router.get('/models/{model_id:path}/chats', response_model=ModelChatsResponse) async def get_model_chats( model_id: str, @@ -286,65 +289,34 @@ async def get_model_chats( end_date: Optional[int] = Query(None), skip: int = Query(0), limit: int = Query(50, le=100), + order_by: str = Query('updated_at'), + direction: str = Query('desc'), user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session), ): """Get chats that used a specific model, with preview and feedback info.""" + filter = {} + if start_date: + filter['start_date'] = start_date + if end_date: + filter['end_date'] = end_date + if order_by in MODEL_CHAT_ORDER_FIELDS: + filter['order_by'] = order_by + if direction in {'asc', 'desc'}: + filter['direction'] = direction - # Get chat IDs that used this model - chat_ids = await ChatMessages.get_chat_ids_by_model_id( + result = await Chats.get_chats_by_model_id( model_id=model_id, - start_date=start_date, - end_date=end_date, + filter=filter, skip=skip, limit=limit, db=db, ) - if not chat_ids: - return ModelChatsResponse(chats=[], total=0) - - # Get chat details from messages only - chats_data = [] - for chat_id in chat_ids: - messages = await ChatMessages.get_messages_by_chat_id(chat_id, db=db) - if not messages: - continue - - # Get user_id from first user message - first_user_msg = next((m for m in messages if m.role == 'user'), None) - user_id = first_user_msg.user_id if first_user_msg else None - - # Extract first message content as preview - first_message = None - if first_user_msg and first_user_msg.content: - content = first_user_msg.content - if isinstance(content, str): - first_message = content[:200] - elif isinstance(content, list): - text_parts = [b.get('text', '') for b in content if isinstance(b, dict)] - first_message = ' '.join(text_parts)[:200] - - # Get user info - user_name = None - if user_id: - user_info = await Users.get_user_by_id(user_id, db=db) - user_name = user_info.name if user_info else None - - # Timestamps from messages - updated_at = max(m.created_at for m in messages) if messages else 0 - - chats_data.append( - ModelChatEntry( - chat_id=chat_id, - user_id=user_id, - user_name=user_name, - first_message=first_message, - updated_at=updated_at, - ) - ) - - return ModelChatsResponse(chats=chats_data, total=len(chats_data)) + return ModelChatsResponse( + chats=[ModelChatEntry.model_validate(chat) for chat in result['items']], + total=result['total'] or 0, + ) #################### diff --git a/src/lib/components/common/ChatList.svelte b/src/lib/components/common/ChatList.svelte index 2ff240019d..b93ad09d83 100644 --- a/src/lib/components/common/ChatList.svelte +++ b/src/lib/components/common/ChatList.svelte @@ -5,10 +5,13 @@ import { WEBUI_API_BASE_URL } from '$lib/constants'; import Spinner from '$lib/components/common/Spinner.svelte'; import Loader from '$lib/components/common/Loader.svelte'; + import ChevronUp from '$lib/components/icons/ChevronUp.svelte'; + import ChevronDown from '$lib/components/icons/ChevronDown.svelte'; dayjs.extend(calendar); const i18n = getContext('i18n'); + type SortKey = 'title' | 'updated_at' | 'user_name'; export let chatList: Array<{ id: string; @@ -25,22 +28,89 @@ export let emptyMessage = 'No chats found'; export let onLoadMore: (() => void) | null = null; export let onChatClick: ((chatId: string) => void) | null = null; + export let orderBy: SortKey | null = null; + export let direction: 'asc' | 'desc' = 'desc'; + export let onSort: ((key: SortKey) => void) | null = null;