From 743b9fd3ceef5a3efada16b922ac9ecaf3a2fcf8 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 16 Jul 2026 21:57:43 -0400 Subject: [PATCH] refac --- backend/open_webui/models/chat_messages.py | 15 +++++ backend/open_webui/models/chats.py | 1 + backend/open_webui/routers/chats.py | 49 +++++++++++---- backend/open_webui/routers/tasks.py | 13 ---- backend/open_webui/tasks.py | 9 --- src/lib/apis/tasks/index.ts | 14 ----- src/lib/components/layout/Sidebar.svelte | 62 ++++++++----------- .../components/layout/Sidebar/ChatItem.svelte | 6 +- .../layout/Sidebar/RecursiveFolder.svelte | 1 + .../layout/Sidebar/SharedFolderItem.svelte | 1 + src/lib/stores/chatList.ts | 17 ++++- src/lib/stores/index.ts | 1 - 12 files changed, 98 insertions(+), 91 deletions(-) delete mode 100644 src/lib/apis/tasks/index.ts diff --git a/backend/open_webui/models/chat_messages.py b/backend/open_webui/models/chat_messages.py index ff05d5fc21..0988928fd2 100644 --- a/backend/open_webui/models/chat_messages.py +++ b/backend/open_webui/models/chat_messages.py @@ -248,6 +248,21 @@ class ChatMessageTable: message = await db.get(ChatMessage, id) return ChatMessageModel.model_validate(message) if message else None + async def has_unfinished_assistant_by_chat_id( + self, + chat_id: str, + db: Optional[AsyncSession] = None, + ) -> bool: + async with get_async_db_context(db) as db: + result = await db.execute( + select(ChatMessage.id) + .where(ChatMessage.chat_id == chat_id) + .where(ChatMessage.role == 'assistant') + .where(ChatMessage.done.is_(False)) + .limit(1) + ) + return result.scalar_one_or_none() is not None + async def get_messages_by_chat_id(self, chat_id: str, db: Optional[AsyncSession] = None) -> list[ChatMessageModel]: async with get_async_db_context(db) as db: result = await db.execute( diff --git a/backend/open_webui/models/chats.py b/backend/open_webui/models/chats.py index 771883e377..049f1fe704 100644 --- a/backend/open_webui/models/chats.py +++ b/backend/open_webui/models/chats.py @@ -186,6 +186,7 @@ class ChatTitleIdResponse(BaseModel): created_at: int last_read_at: int | None = None snippet: str | None = None + active: bool = False class SharedChatResponse(BaseModel): diff --git a/backend/open_webui/routers/chats.py b/backend/open_webui/routers/chats.py index 752c5f3468..5a565407a6 100644 --- a/backend/open_webui/routers/chats.py +++ b/backend/open_webui/routers/chats.py @@ -14,6 +14,7 @@ from open_webui.events import EVENTS, publish_event from open_webui.internal.db import get_async_session from open_webui.models.access_grants import AccessGrants from open_webui.models.config import Config +from open_webui.models.chat_messages import ChatMessages from open_webui.models.chats import ( AggregateChatStats, ChatBody, @@ -57,6 +58,19 @@ CHAT_CONFIG_KEYS = { } +async def add_active_state_to_chat_list( + request: Request, chat_list: list[ChatTitleIdResponse] +) -> list[ChatTitleIdResponse]: + for chat in chat_list: + chat.active = False + if not await has_active_tasks(request.app.state.redis, chat.id): + continue + + chat.active = await ChatMessages.has_unfinished_assistant_by_chat_id(chat.id) + + return chat_list + + class ChatConfigForm(BaseModel): ENABLE_CONTEXT_COMPACTION: bool CONTEXT_COMPACTION_TOKEN_THRESHOLD: int @@ -137,6 +151,7 @@ async def require_chat_import_permission(request: Request, user, db: AsyncSessio @router.get('/', response_model=list[ChatTitleIdResponse]) @router.get('/list', response_model=list[ChatTitleIdResponse]) async def get_session_user_chat_list( + request: Request, user=Depends(get_verified_user), page: int | None = None, include_pinned: bool | None = False, @@ -148,7 +163,7 @@ async def get_session_user_chat_list( limit = 60 skip = (page - 1) * limit - return await Chats.get_chat_title_id_list_by_user_id( + chats = await Chats.get_chat_title_id_list_by_user_id( user.id, include_folders=include_folders, include_pinned=include_pinned, @@ -157,12 +172,13 @@ async def get_session_user_chat_list( db=db, ) else: - return await Chats.get_chat_title_id_list_by_user_id( + chats = await Chats.get_chat_title_id_list_by_user_id( user.id, include_folders=include_folders, include_pinned=include_pinned, db=db, ) + return await add_active_state_to_chat_list(request, chats) except Exception as e: log.exception(e) raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()) @@ -605,6 +621,7 @@ async def delete_all_user_chats( @router.get('/list/user/{user_id}', response_model=list[ChatTitleIdResponse]) async def get_user_chat_list_by_user_id( + request: Request, user_id: str, page: int | None = None, query: str | None = None, @@ -629,9 +646,10 @@ async def get_user_chat_list_by_user_id( if direction: filter['direction'] = direction - return await Chats.get_chat_list_by_user_id( + chats = await Chats.get_chat_list_by_user_id( user_id, include_archived=True, filter=filter, skip=skip, limit=limit, db=db ) + return await add_active_state_to_chat_list(request, chats) ############################ @@ -738,6 +756,7 @@ async def set_chat_config(form_data: ChatConfigForm, user=Depends(get_admin_user @router.get('/search', response_model=list[ChatTitleIdResponse]) async def search_user_chats( + request: Request, text: str, page: int | None = None, user=Depends(get_verified_user), @@ -763,7 +782,7 @@ async def search_user_chats( log.debug(f'deleting tag: {tag_id}') await Tags.delete_tag_by_name_and_user_id(tag_id, user.id, db=db) - return chat_list + return await add_active_state_to_chat_list(request, chat_list) ############################ @@ -786,8 +805,9 @@ async def get_chats_by_folder_id( ] -@router.get('/folder/{folder_id}/list') +@router.get('/folder/{folder_id}/list', response_model=list[ChatTitleIdResponse]) async def get_chat_list_by_folder_id( + request: Request, folder_id: str, page: int | None = 1, user=Depends(get_verified_user), @@ -798,10 +818,7 @@ async def get_chat_list_by_folder_id( skip = (page - 1) * limit chats = await Chats.get_chats_by_folder_id_and_user_id(folder_id, user.id, skip=skip, limit=limit, db=db) - return [ - {'title': chat.title, 'id': chat.id, 'updated_at': chat.updated_at, 'last_read_at': chat.last_read_at} - for chat in chats - ] + return await add_active_state_to_chat_list(request, chats) except Exception as e: log.exception(e) @@ -814,8 +831,11 @@ async def get_chat_list_by_folder_id( @router.get('/pinned', response_model=list[ChatTitleIdResponse]) -async def get_user_pinned_chats(user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): - return await Chats.get_pinned_chats_by_user_id(user.id, db=db) +async def get_user_pinned_chats( + request: Request, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session) +): + chats = await Chats.get_pinned_chats_by_user_id(user.id, db=db) + return await add_active_state_to_chat_list(request, chats) ############################ @@ -908,6 +928,7 @@ async def get_all_user_chats_in_db(user=Depends(get_admin_user), db: AsyncSessio @router.get('/archived', response_model=list[ChatTitleIdResponse]) async def get_archived_session_user_chat_list( + request: Request, page: int | None = None, query: str | None = None, order_by: str | None = None, @@ -929,13 +950,14 @@ async def get_archived_session_user_chat_list( if direction: filter['direction'] = direction - return await Chats.get_archived_chat_list_by_user_id( + chats = await Chats.get_archived_chat_list_by_user_id( user.id, filter=filter, skip=skip, limit=limit, db=db, ) + return await add_active_state_to_chat_list(request, chats) ############################ @@ -1103,6 +1125,7 @@ class TagFilterForm(TagForm): @router.post('/tags', response_model=list[ChatTitleIdResponse]) async def get_user_chat_list_by_tag_name( + request: Request, form_data: TagFilterForm, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session), @@ -1113,7 +1136,7 @@ async def get_user_chat_list_by_tag_name( if len(chats) == 0: await Tags.delete_tag_by_name_and_user_id(form_data.name, user.id, db=db) - return chats + return await add_active_state_to_chat_list(request, chats) ############################ diff --git a/backend/open_webui/routers/tasks.py b/backend/open_webui/routers/tasks.py index 7cc7449b4b..4e377e2b78 100644 --- a/backend/open_webui/routers/tasks.py +++ b/backend/open_webui/routers/tasks.py @@ -75,19 +75,6 @@ def config_updates(data: dict, key_map: dict[str, str]) -> dict: ################################## -class ActiveChatsForm(BaseModel): - chat_ids: list[str] - - -@router.post('/active/chats') -async def check_active_chats(request: Request, form_data: ActiveChatsForm, user=Depends(get_verified_user)): - """Check which chat IDs have active tasks.""" - from open_webui.tasks import get_active_chat_ids - - active = await get_active_chat_ids(request.app.state.redis, form_data.chat_ids) - return {'active_chat_ids': active} - - @router.get('/config') async def get_task_config(request: Request, user=Depends(get_verified_user)): return await get_config_values(TASK_CONFIG_KEYS) diff --git a/backend/open_webui/tasks.py b/backend/open_webui/tasks.py index 6475e5a239..d6b2c00419 100644 --- a/backend/open_webui/tasks.py +++ b/backend/open_webui/tasks.py @@ -199,12 +199,3 @@ async def has_active_tasks(redis, chat_id: str) -> bool: """Check if a chat has any active tasks.""" task_ids = await list_task_ids_by_item_id(redis, chat_id) return len(task_ids) > 0 - - -async def get_active_chat_ids(redis, chat_ids: List[str]) -> List[str]: - """Filter a list of chat_ids to only those with active tasks.""" - active = [] - for chat_id in chat_ids: - if await has_active_tasks(redis, chat_id): - active.append(chat_id) - return active diff --git a/src/lib/apis/tasks/index.ts b/src/lib/apis/tasks/index.ts deleted file mode 100644 index dab6090fde..0000000000 --- a/src/lib/apis/tasks/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { WEBUI_API_BASE_URL } from '$lib/constants'; - -export const checkActiveChats = async (token: string, chatIds: string[]) => { - const res = await fetch(`${WEBUI_API_BASE_URL}/tasks/active/chats`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${token}` - }, - body: JSON.stringify({ chat_ids: chatIds }) - }); - if (!res.ok) throw await res.json(); - return res.json(); -}; diff --git a/src/lib/components/layout/Sidebar.svelte b/src/lib/components/layout/Sidebar.svelte index bb9d0ec9b5..c7a905b48f 100644 --- a/src/lib/components/layout/Sidebar.svelte +++ b/src/lib/components/layout/Sidebar.svelte @@ -25,10 +25,9 @@ models, selectedFolder, WEBUI_NAME, - sidebarWidth, - activeChatIds + sidebarWidth } from '$lib/stores'; - import { loadNextChatListPage, refreshChatList } from '$lib/stores/chatList'; + import { loadNextChatListPage, refreshChatList, setChatActive } from '$lib/stores/chatList'; import { onMount, getContext, tick, onDestroy } from 'svelte'; const i18n = getContext('i18n'); @@ -52,7 +51,6 @@ } from '$lib/apis/folders'; import { createNewNote, getPinnedNoteList, toggleNotePinnedStatusById } from '$lib/apis/notes'; import { updateUserSettings } from '$lib/apis/users'; - import { checkActiveChats } from '$lib/apis/tasks'; import { createNoteHandler } from '$lib/components/notes/utils'; import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants'; @@ -110,7 +108,7 @@ let showSharedFolders = false; let folders = {}; - let folderRegistry = {}; + let folderRegistry: Record unknown }> = {}; let newFolderId = null; @@ -366,18 +364,20 @@ })(), (async () => { console.log('Init chat list'); - const result = await refreshChatList(localStorage.token, { refreshPinned: true }); - if (result.accepted) { - await Promise.all( - Object.values(folderRegistry).map((folder: any) => folder?.setFolderItems?.()) - ); - allChatsLoaded = result.allLoaded; - chatListReady = true; - } + await refreshChatRows(); })() ]); }; + const refreshChatRows = async () => { + const result = await refreshChatList(localStorage.token, { refreshPinned: true }); + if (result.accepted) { + await Promise.all(Object.values(folderRegistry).map((folder) => folder?.setFolderItems?.())); + allChatsLoaded = result.allLoaded; + chatListReady = true; + } + }; + const loadMoreChats = async () => { chatListLoading = true; @@ -610,17 +610,6 @@ await initChannels(); } await initChatList(); - - // Check which chats have active tasks - const allChatIds = [...$chats.map((c) => c.id), ...$pinnedChats.map((c) => c.id)]; - if (allChatIds.length > 0) { - try { - const res = await checkActiveChats(localStorage.token, allChatIds); - activeChatIds.set(new Set(res.active_chat_ids || [])); - } catch (e) { - console.debug('Failed to check active chats:', e); - } - } } }), settings.subscribe((value) => { @@ -649,6 +638,7 @@ const socketInstance = $socket; socketInstance?.on('events', chatActiveEventHandler); + socketInstance?.on('connect', refreshChatRows); await tick(); initPinnedMenuSortable(); @@ -672,28 +662,24 @@ } socketInstance?.off('events', chatActiveEventHandler); + socketInstance?.off('connect', refreshChatRows); }; }); // Handler for chat events (defined outside onMount for proper cleanup) - const chatActiveEventHandler = (event: { + const chatActiveEventHandler = async (event: { chat_id: string; message_id: string; - data: { type: string; data: any }; + data: { type: string; data: { active?: boolean } }; }) => { if (event.data?.type === 'chat:active') { - const { active } = event.data.data; - activeChatIds.update((ids) => { - const newSet = new Set(ids); - if (active) { - newSet.add(event.chat_id); - } else { - newSet.delete(event.chat_id); - } - return newSet; - }); + const active = event.data.data.active ?? false; + const found = setChatActive(event.chat_id, active); + if (!found && active) { + await refreshChatRows(); + } } else if (event.data?.type === 'chat:list') { - initChatList(); + refreshChatRows(); } }; @@ -1416,6 +1402,7 @@ createdAt={chat.created_at} updatedAt={chat.updated_at} lastReadAt={chat.last_read_at} + active={chat.active ?? false} {shiftKey} selected={selectedChatId === chat.id} on:select={() => { @@ -1479,6 +1466,7 @@ createdAt={chat.created_at} updatedAt={chat.updated_at} lastReadAt={chat.last_read_at} + active={chat.active ?? false} {shiftKey} selected={selectedChatId === chat.id} on:select={() => { diff --git a/src/lib/components/layout/Sidebar/ChatItem.svelte b/src/lib/components/layout/Sidebar/ChatItem.svelte index a56b2ae159..acb2a5bae7 100644 --- a/src/lib/components/layout/Sidebar/ChatItem.svelte +++ b/src/lib/components/layout/Sidebar/ChatItem.svelte @@ -26,7 +26,6 @@ showSidebar, tags, selectedFolder, - activeChatIds, settings, user } from '$lib/stores'; @@ -58,6 +57,7 @@ export let createdAt: number | null = null; export let updatedAt: number | null = null; export let lastReadAt: number | null = null; + export let active = false; export let selected = false; export let shiftKey = false; @@ -104,7 +104,7 @@ $: unread = id !== $chatId && - !$activeChatIds.has(id) && + !active && (effectiveReadAt === null || (updatedAt !== null && updatedAt > effectiveReadAt)); $: showInlineActions = id === $chatId || confirmEdit || mouseOver || selected; @@ -545,7 +545,7 @@ {/if} - {#if $activeChatIds.has(id)} + {#if active}
diff --git a/src/lib/components/layout/Sidebar/RecursiveFolder.svelte b/src/lib/components/layout/Sidebar/RecursiveFolder.svelte index 0b6196cc00..c13058200c 100644 --- a/src/lib/components/layout/Sidebar/RecursiveFolder.svelte +++ b/src/lib/components/layout/Sidebar/RecursiveFolder.svelte @@ -721,6 +721,7 @@ createdAt={chat.created_at} updatedAt={chat.updated_at} lastReadAt={chat.last_read_at} + active={chat.active ?? false} ownerName={folders[folderId]?.shared ? (chat.owner_name ?? null) : null} ownerUserId={folders[folderId]?.shared && chat.owner_name ? chat.user_id : null} readonly={chat.user_id !== $user?.id} diff --git a/src/lib/components/layout/Sidebar/SharedFolderItem.svelte b/src/lib/components/layout/Sidebar/SharedFolderItem.svelte index d3d9cc611f..ba74ffbba4 100644 --- a/src/lib/components/layout/Sidebar/SharedFolderItem.svelte +++ b/src/lib/components/layout/Sidebar/SharedFolderItem.svelte @@ -104,6 +104,7 @@ title={chat.title} createdAt={chat.created_at} updatedAt={chat.updated_at} + active={chat.active ?? false} ownerName={chat.owner_name} ownerUserId={chat.user_id} readonly={chat.readonly ?? !isWritable} diff --git a/src/lib/stores/chatList.ts b/src/lib/stores/chatList.ts index a3c736d8c5..55b7aa1162 100644 --- a/src/lib/stores/chatList.ts +++ b/src/lib/stores/chatList.ts @@ -3,7 +3,7 @@ import { getChatList, getPinnedChatList } from '$lib/apis/chats'; type ChatListItem = { id: string; - [key: string]: any; + [key: string]: unknown; }; const chatsStore = writable(null); @@ -90,6 +90,21 @@ export const loadNextChatListPage = async (token: string = ''): Promise { + let found = false; + const updateChat = (chat: ChatListItem) => { + if (chat.id !== chatId) { + return chat; + } + found = true; + return { ...chat, active }; + }; + + chatsStore.update((items) => (items ? items.map(updateChat) : items)); + pinnedChatsStore.update((items) => items.map(updateChat)); + return found; +}; + export const resetChatListState = () => { requestGeneration += 1; currentPage = 1; diff --git a/src/lib/stores/index.ts b/src/lib/stores/index.ts index 4545f232d7..37757ef42b 100644 --- a/src/lib/stores/index.ts +++ b/src/lib/stores/index.ts @@ -31,7 +31,6 @@ export const mobile = writable(false); export const socket: Writable = writable(null); export const socketConnected: Writable = writable(true); export const activeUserIds: Writable = writable(null); -export const activeChatIds: Writable> = writable(new Set()); export const USAGE_POOL: Writable = writable(null); export const theme = writable('system');