From 48522271586a5bf24b649610f03b4ffd8afb2782 Mon Sep 17 00:00:00 2001 From: Tim Baek Date: Sun, 8 Feb 2026 06:22:56 +0400 Subject: [PATCH] refac --- backend/open_webui/main.py | 16 +++++++ backend/open_webui/routers/tasks.py | 15 ++++++ backend/open_webui/tasks.py | 15 ++++++ src/lib/apis/tasks/index.ts | 14 ++++++ src/lib/components/layout/Sidebar.svelte | 46 +++++++++++++++++-- .../components/layout/Sidebar/ChatItem.svelte | 18 ++++++-- .../layout/Sidebar/RecursiveFolder.svelte | 2 +- src/lib/stores/index.ts | 1 + 8 files changed, 119 insertions(+), 8 deletions(-) create mode 100644 src/lib/apis/tasks/index.ts diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index 2d0aeecf1..5fab6990d 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -1804,6 +1804,16 @@ async def chat_completion( except Exception as e: log.debug(f"Error cleaning up: {e}") pass + # Emit chat:active=false when task completes + try: + if metadata.get("chat_id"): + event_emitter = get_event_emitter(metadata, update_db=False) + if event_emitter: + await event_emitter( + {"type": "chat:active", "data": {"active": False}} + ) + except Exception as e: + log.debug(f"Error emitting chat:active: {e}") if ( metadata.get("session_id") @@ -1816,6 +1826,12 @@ async def chat_completion( process_chat(request, form_data, user, metadata, model), id=metadata["chat_id"], ) + # Emit chat:active=true when task starts + event_emitter = get_event_emitter(metadata, update_db=False) + if event_emitter: + await event_emitter( + {"type": "chat:active", "data": {"active": True}} + ) return {"status": True, "task_id": task_id} else: return await process_chat(request, form_data, user, metadata, model) diff --git a/backend/open_webui/routers/tasks.py b/backend/open_webui/routers/tasks.py index 37a80e2ce..a89e28d3a 100644 --- a/backend/open_webui/routers/tasks.py +++ b/backend/open_webui/routers/tasks.py @@ -49,6 +49,21 @@ router = APIRouter() ################################## +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 { diff --git a/backend/open_webui/tasks.py b/backend/open_webui/tasks.py index d83226ffb..cc91cde1a 100644 --- a/backend/open_webui/tasks.py +++ b/backend/open_webui/tasks.py @@ -183,3 +183,18 @@ async def stop_item_tasks(redis: Redis, item_id: str): return result # Return the first failure return {"status": True, "message": f"All tasks for item {item_id} stopped."} + + +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 new file mode 100644 index 000000000..83299b843 --- /dev/null +++ b/src/lib/apis/tasks/index.ts @@ -0,0 +1,14 @@ +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 5a92d991b..5c78145fe 100644 --- a/src/lib/components/layout/Sidebar.svelte +++ b/src/lib/components/layout/Sidebar.svelte @@ -26,7 +26,8 @@ models, selectedFolder, WEBUI_NAME, - sidebarWidth + sidebarWidth, + activeChatIds } from '$lib/stores'; import { onMount, getContext, tick, onDestroy } from 'svelte'; @@ -42,6 +43,7 @@ importChats } from '$lib/apis/chats'; import { createNewFolder, getFolders, updateFolderParentIdById } from '$lib/apis/folders'; + import { checkActiveChats } from '$lib/apis/tasks'; import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants'; import ArchivedChatsModal from './ArchivedChatsModal.svelte'; @@ -469,6 +471,17 @@ 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) => { @@ -493,8 +506,32 @@ dropZone?.addEventListener('dragover', onDragOver); dropZone?.addEventListener('drop', onDrop); dropZone?.addEventListener('dragleave', onDragLeave); + + // Listen for real-time chat:active events via the events channel + $socket?.off('events', chatActiveEventHandler); + $socket?.on('events', chatActiveEventHandler); }); + // Handler for chat:active events (defined outside onMount for proper cleanup) + const chatActiveEventHandler = (event: { + chat_id: string; + message_id: string; + data: { type: string; data: any }; + }) => { + 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; + }); + } + }; + onDestroy(() => { if (unsubscribers && unsubscribers.length > 0) { unsubscribers.forEach((unsubscriber) => { @@ -518,6 +555,9 @@ dropZone?.removeEventListener('dragover', onDragOver); dropZone?.removeEventListener('drop', onDrop); dropZone?.removeEventListener('dragleave', onDragLeave); + + // Clean up socket listener + $socket?.off('events', chatActiveEventHandler); }); const newChatHandler = async () => { @@ -1244,7 +1284,7 @@ className="" id={chat.id} title={chat.title} - updatedAt={chat.updated_at} + createdAt={chat.created_at} {shiftKey} selected={selectedChatId === chat.id} on:select={() => { @@ -1305,7 +1345,7 @@ className="" id={chat.id} title={chat.title} - updatedAt={chat.updated_at} + createdAt={chat.created_at} {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 f9ec3f902..427adb1f2 100644 --- a/src/lib/components/layout/Sidebar/ChatItem.svelte +++ b/src/lib/components/layout/Sidebar/ChatItem.svelte @@ -27,7 +27,8 @@ showSidebar, currentChatPage, tags, - selectedFolder + selectedFolder, + activeChatIds } from '$lib/stores'; import ChatMenu from './ChatMenu.svelte'; @@ -41,13 +42,14 @@ import XMark from '$lib/components/icons/XMark.svelte'; import Document from '$lib/components/icons/Document.svelte'; import Sparkles from '$lib/components/icons/Sparkles.svelte'; + import Spinner from '$lib/components/common/Spinner.svelte'; import { generateTitle } from '$lib/apis'; export let className = ''; export let id; export let title; - export let updatedAt: number | null = null; + export let createdAt: number | null = null; export let selected = false; export let shiftKey = false; @@ -73,6 +75,7 @@ return '1m'; } + let chat = null; let mouseOver = false; @@ -443,6 +446,13 @@ on:focus={(e) => {}} draggable="false" > + + {#if $activeChatIds.has(id)} +
+ +
+ {/if} +
{title} @@ -450,9 +460,9 @@
- {#if updatedAt && !mouseOver} + {#if createdAt && !mouseOver}
- {formatTimeAgo(updatedAt)} + {formatTimeAgo(createdAt)}
{/if} diff --git a/src/lib/components/layout/Sidebar/RecursiveFolder.svelte b/src/lib/components/layout/Sidebar/RecursiveFolder.svelte index fc5a3f061..f800cfe9b 100644 --- a/src/lib/components/layout/Sidebar/RecursiveFolder.svelte +++ b/src/lib/components/layout/Sidebar/RecursiveFolder.svelte @@ -643,7 +643,7 @@ { dispatch('change', e.detail); diff --git a/src/lib/stores/index.ts b/src/lib/stores/index.ts index 547548019..3fbc001f0 100644 --- a/src/lib/stores/index.ts +++ b/src/lib/stores/index.ts @@ -27,6 +27,7 @@ export const mobile = writable(false); export const socket: Writable = writable(null); 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');