mirror of
https://github.com/open-webui/open-webui
synced 2024-12-28 14:52:23 +00:00
enh: user status indicator
This commit is contained in:
parent
c53ace3c98
commit
50534a0dcf
@ -10,6 +10,9 @@ from open_webui.models.users import (
|
|||||||
UserSettings,
|
UserSettings,
|
||||||
UserUpdateForm,
|
UserUpdateForm,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
from open_webui.socket.main import get_active_status_by_user_id
|
||||||
from open_webui.constants import ERROR_MESSAGES
|
from open_webui.constants import ERROR_MESSAGES
|
||||||
from open_webui.env import SRC_LOG_LEVELS
|
from open_webui.env import SRC_LOG_LEVELS
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||||
@ -196,6 +199,7 @@ async def update_user_info_by_session_user(
|
|||||||
class UserResponse(BaseModel):
|
class UserResponse(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
profile_image_url: str
|
profile_image_url: str
|
||||||
|
active: Optional[bool] = None
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{user_id}", response_model=UserResponse)
|
@router.get("/{user_id}", response_model=UserResponse)
|
||||||
@ -216,7 +220,13 @@ async def get_user_by_id(user_id: str, user=Depends(get_verified_user)):
|
|||||||
user = Users.get_user_by_id(user_id)
|
user = Users.get_user_by_id(user_id)
|
||||||
|
|
||||||
if user:
|
if user:
|
||||||
return UserResponse(name=user.name, profile_image_url=user.profile_image_url)
|
return UserResponse(
|
||||||
|
**{
|
||||||
|
"name": user.name,
|
||||||
|
"profile_image_url": user.profile_image_url,
|
||||||
|
"active": get_active_status_by_user_id(user_id),
|
||||||
|
}
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
@ -159,7 +159,7 @@ async def connect(sid, environ, auth):
|
|||||||
USER_POOL[user.id] = [sid]
|
USER_POOL[user.id] = [sid]
|
||||||
|
|
||||||
# print(f"user {user.name}({user.id}) connected with session ID {sid}")
|
# print(f"user {user.name}({user.id}) connected with session ID {sid}")
|
||||||
await sio.emit("user-count", {"count": len(USER_POOL.items())})
|
await sio.emit("user-list", {"user_ids": list(USER_POOL.keys())})
|
||||||
await sio.emit("usage", {"models": get_models_in_use()})
|
await sio.emit("usage", {"models": get_models_in_use()})
|
||||||
|
|
||||||
|
|
||||||
@ -192,7 +192,7 @@ async def user_join(sid, data):
|
|||||||
|
|
||||||
# print(f"user {user.name}({user.id}) connected with session ID {sid}")
|
# print(f"user {user.name}({user.id}) connected with session ID {sid}")
|
||||||
|
|
||||||
await sio.emit("user-count", {"count": len(USER_POOL.items())})
|
await sio.emit("user-list", {"user_ids": list(USER_POOL.keys())})
|
||||||
return {"id": user.id, "name": user.name}
|
return {"id": user.id, "name": user.name}
|
||||||
|
|
||||||
|
|
||||||
@ -244,9 +244,9 @@ async def channel_events(sid, data):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@sio.on("user-count")
|
@sio.on("user-list")
|
||||||
async def user_count(sid):
|
async def user_list(sid):
|
||||||
await sio.emit("user-count", {"count": len(USER_POOL.items())})
|
await sio.emit("user-list", {"user_ids": list(USER_POOL.keys())})
|
||||||
|
|
||||||
|
|
||||||
@sio.event
|
@sio.event
|
||||||
@ -261,7 +261,7 @@ async def disconnect(sid):
|
|||||||
if len(USER_POOL[user_id]) == 0:
|
if len(USER_POOL[user_id]) == 0:
|
||||||
del USER_POOL[user_id]
|
del USER_POOL[user_id]
|
||||||
|
|
||||||
await sio.emit("user-count", {"count": len(USER_POOL)})
|
await sio.emit("user-list", {"user_ids": list(USER_POOL.keys())})
|
||||||
else:
|
else:
|
||||||
pass
|
pass
|
||||||
# print(f"Unknown session ID {sid} disconnected")
|
# print(f"Unknown session ID {sid} disconnected")
|
||||||
@ -330,3 +330,9 @@ def get_user_ids_from_room(room):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
return active_user_ids
|
return active_user_ids
|
||||||
|
|
||||||
|
|
||||||
|
def get_active_status_by_user_id(user_id):
|
||||||
|
if user_id in USER_POOL:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
@ -25,6 +25,7 @@
|
|||||||
import Textarea from '$lib/components/common/Textarea.svelte';
|
import Textarea from '$lib/components/common/Textarea.svelte';
|
||||||
import Image from '$lib/components/common/Image.svelte';
|
import Image from '$lib/components/common/Image.svelte';
|
||||||
import FileItem from '$lib/components/common/FileItem.svelte';
|
import FileItem from '$lib/components/common/FileItem.svelte';
|
||||||
|
import ProfilePreview from './Message/ProfilePreview.svelte';
|
||||||
|
|
||||||
export let message;
|
export let message;
|
||||||
export let showUserProfile = true;
|
export let showUserProfile = true;
|
||||||
@ -101,11 +102,13 @@
|
|||||||
class={`flex-shrink-0 ${($settings?.chatDirection ?? 'LTR') === 'LTR' ? 'mr-3' : 'ml-3'} w-9`}
|
class={`flex-shrink-0 ${($settings?.chatDirection ?? 'LTR') === 'LTR' ? 'mr-3' : 'ml-3'} w-9`}
|
||||||
>
|
>
|
||||||
{#if showUserProfile}
|
{#if showUserProfile}
|
||||||
<ProfileImage
|
<ProfilePreview user={message.user}>
|
||||||
src={message.user?.profile_image_url ??
|
<ProfileImage
|
||||||
($i18n.language === 'dg-DG' ? `/doge.png` : `${WEBUI_BASE_URL}/static/favicon.png`)}
|
src={message.user?.profile_image_url ??
|
||||||
className={'size-8 translate-y-1 ml-0.5'}
|
($i18n.language === 'dg-DG' ? `/doge.png` : `${WEBUI_BASE_URL}/static/favicon.png`)}
|
||||||
/>
|
className={'size-8 translate-y-1 ml-0.5'}
|
||||||
|
/>
|
||||||
|
</ProfilePreview>
|
||||||
{:else}
|
{:else}
|
||||||
<!-- <div class="w-7 h-7 rounded-full bg-transparent" /> -->
|
<!-- <div class="w-7 h-7 rounded-full bg-transparent" /> -->
|
||||||
|
|
||||||
|
@ -0,0 +1,85 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { DropdownMenu } from 'bits-ui';
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
|
import { flyAndScale } from '$lib/utils/transitions';
|
||||||
|
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||||
|
import { activeUserIds } from '$lib/stores';
|
||||||
|
|
||||||
|
export let side = 'right';
|
||||||
|
export let align = 'top';
|
||||||
|
|
||||||
|
export let user = null;
|
||||||
|
let show = false;
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenu.Root
|
||||||
|
bind:open={show}
|
||||||
|
closeFocus={false}
|
||||||
|
onOpenChange={(state) => {
|
||||||
|
dispatch('change', state);
|
||||||
|
}}
|
||||||
|
typeahead={false}
|
||||||
|
>
|
||||||
|
<DropdownMenu.Trigger>
|
||||||
|
<slot />
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
|
||||||
|
<slot name="content">
|
||||||
|
<DropdownMenu.Content
|
||||||
|
class="w-full max-w-[200px] rounded-lg z-50 bg-white dark:bg-black dark:text-white shadow-lg"
|
||||||
|
sideOffset={8}
|
||||||
|
{side}
|
||||||
|
{align}
|
||||||
|
transition={flyAndScale}
|
||||||
|
>
|
||||||
|
{#if user}
|
||||||
|
<div class=" flex flex-col gap-2 w-full rounded-lg">
|
||||||
|
<div class="py-8 relative bg-gray-900 rounded-t-lg">
|
||||||
|
<img
|
||||||
|
crossorigin="anonymous"
|
||||||
|
src={user?.profile_image_url ?? `${WEBUI_BASE_URL}/static/favicon.png`}
|
||||||
|
class=" absolute -bottom-5 left-3 size-12 ml-0.5 object-cover rounded-full -translate-y-[1px]"
|
||||||
|
alt="profile"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class=" flex flex-col pt-4 pb-2.5 px-4">
|
||||||
|
<div class=" -mb-1">
|
||||||
|
<span class="font-medium text-sm line-clamp-1"> {user.name} </span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class=" flex items-center gap-2">
|
||||||
|
{#if $activeUserIds.includes(user.id)}
|
||||||
|
<div>
|
||||||
|
<span class="relative flex size-2">
|
||||||
|
<span
|
||||||
|
class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"
|
||||||
|
/>
|
||||||
|
<span class="relative inline-flex rounded-full size-2 bg-green-500" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class=" -translate-y-[1px]">
|
||||||
|
<span class="text-xs"> Active </span>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div>
|
||||||
|
<span class="relative flex size-2">
|
||||||
|
<span class="relative inline-flex rounded-full size-2 bg-gray-500" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class=" -translate-y-[1px]">
|
||||||
|
<span class="text-xs"> Away </span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</slot>
|
||||||
|
</DropdownMenu.Root>
|
@ -5,6 +5,8 @@
|
|||||||
import { flyAndScale } from '$lib/utils/transitions';
|
import { flyAndScale } from '$lib/utils/transitions';
|
||||||
|
|
||||||
export let show = false;
|
export let show = false;
|
||||||
|
export let side = 'bottom';
|
||||||
|
export let align = 'start';
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -24,8 +26,8 @@
|
|||||||
<DropdownMenu.Content
|
<DropdownMenu.Content
|
||||||
class="w-full max-w-[130px] rounded-lg px-1 py-1.5 border border-gray-900 z-50 bg-gray-850 text-white"
|
class="w-full max-w-[130px] rounded-lg px-1 py-1.5 border border-gray-900 z-50 bg-gray-850 text-white"
|
||||||
sideOffset={8}
|
sideOffset={8}
|
||||||
side="bottom"
|
{side}
|
||||||
align="start"
|
{align}
|
||||||
transition={flyAndScale}
|
transition={flyAndScale}
|
||||||
>
|
>
|
||||||
<DropdownMenu.Item class="flex items-center px-3 py-2 text-sm font-medium">
|
<DropdownMenu.Item class="flex items-center px-3 py-2 text-sm font-medium">
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
import { flyAndScale } from '$lib/utils/transitions';
|
import { flyAndScale } from '$lib/utils/transitions';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import ArchiveBox from '$lib/components/icons/ArchiveBox.svelte';
|
import ArchiveBox from '$lib/components/icons/ArchiveBox.svelte';
|
||||||
import { showSettings, activeUserCount, USAGE_POOL, mobile, showSidebar } from '$lib/stores';
|
import { showSettings, activeUserIds, USAGE_POOL, mobile, showSidebar } from '$lib/stores';
|
||||||
import { fade, slide } from 'svelte/transition';
|
import { fade, slide } from 'svelte/transition';
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
import { userSignOut } from '$lib/apis/auths';
|
import { userSignOut } from '$lib/apis/auths';
|
||||||
@ -184,7 +184,7 @@
|
|||||||
<div class=" self-center truncate">{$i18n.t('Sign Out')}</div>
|
<div class=" self-center truncate">{$i18n.t('Sign Out')}</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{#if $activeUserCount}
|
{#if $activeUserIds?.length > 0}
|
||||||
<hr class=" border-gray-50 dark:border-gray-850 my-1 p-0" />
|
<hr class=" border-gray-50 dark:border-gray-850 my-1 p-0" />
|
||||||
|
|
||||||
<Tooltip
|
<Tooltip
|
||||||
@ -207,7 +207,7 @@
|
|||||||
{$i18n.t('Active Users')}:
|
{$i18n.t('Active Users')}:
|
||||||
</span>
|
</span>
|
||||||
<span class=" font-semibold">
|
<span class=" font-semibold">
|
||||||
{$activeUserCount}
|
{$activeUserIds?.length}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -15,7 +15,7 @@ export const MODEL_DOWNLOAD_POOL = writable({});
|
|||||||
export const mobile = writable(false);
|
export const mobile = writable(false);
|
||||||
|
|
||||||
export const socket: Writable<null | Socket> = writable(null);
|
export const socket: Writable<null | Socket> = writable(null);
|
||||||
export const activeUserCount: Writable<null | number> = writable(null);
|
export const activeUserIds: Writable<null | string[]> = writable(null);
|
||||||
export const USAGE_POOL: Writable<null | string[]> = writable(null);
|
export const USAGE_POOL: Writable<null | string[]> = writable(null);
|
||||||
|
|
||||||
export const theme = writable('system');
|
export const theme = writable('system');
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
WEBUI_NAME,
|
WEBUI_NAME,
|
||||||
mobile,
|
mobile,
|
||||||
socket,
|
socket,
|
||||||
activeUserCount,
|
activeUserIds,
|
||||||
USAGE_POOL,
|
USAGE_POOL,
|
||||||
chatId,
|
chatId,
|
||||||
chats,
|
chats,
|
||||||
@ -81,9 +81,9 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
_socket.on('user-count', (data) => {
|
_socket.on('user-list', (data) => {
|
||||||
console.log('user-count', data);
|
console.log('user-list', data);
|
||||||
activeUserCount.set(data.count);
|
activeUserIds.set(data.user_ids);
|
||||||
});
|
});
|
||||||
|
|
||||||
_socket.on('usage', (data) => {
|
_socket.on('usage', (data) => {
|
||||||
|
Loading…
Reference in New Issue
Block a user