This commit is contained in:
Tim Baek
2026-02-08 06:22:56 +04:00
parent 59b98ab730
commit 4852227158
8 changed files with 119 additions and 8 deletions
+16
View File
@@ -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)
+15
View File
@@ -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 {
+15
View File
@@ -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
+14
View File
@@ -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();
};
+43 -3
View File
@@ -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={() => {
@@ -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"
>
<!-- Loading spinner for active chat (left side) -->
{#if $activeChatIds.has(id)}
<div class="shrink-0 self-center pr-2">
<Spinner className="size-3" />
</div>
{/if}
<div class="flex self-center flex-1 w-full min-w-0">
<div dir="auto" class="text-left self-center overflow-hidden w-full h-[20px] truncate">
{title}
@@ -450,9 +460,9 @@
</div>
<!-- Time ago indicator -->
{#if updatedAt && !mouseOver}
{#if createdAt && !mouseOver}
<div class="shrink-0 self-center text-[10px] text-gray-400 dark:text-gray-500 pl-2">
{formatTimeAgo(updatedAt)}
{formatTimeAgo(createdAt)}
</div>
{/if}
</a>
@@ -643,7 +643,7 @@
<ChatItem
id={chat.id}
title={chat.title}
updatedAt={chat.updated_at}
createdAt={chat.created_at}
{shiftKey}
on:change={(e) => {
dispatch('change', e.detail);
+1
View File
@@ -27,6 +27,7 @@ export const mobile = writable(false);
export const socket: Writable<null | Socket> = writable(null);
export const activeUserIds: Writable<null | string[]> = writable(null);
export const activeChatIds: Writable<Set<string>> = writable(new Set());
export const USAGE_POOL: Writable<null | string[]> = writable(null);
export const theme = writable('system');