mirror of
https://github.com/open-webui/open-webui
synced 2024-12-28 06:42:47 +00:00
enh: typing indicator
This commit is contained in:
parent
4f93ecf519
commit
6ff6d57507
@ -4,7 +4,7 @@ import logging
|
||||
import sys
|
||||
import time
|
||||
|
||||
from open_webui.models.users import Users
|
||||
from open_webui.models.users import Users, UserNameResponse
|
||||
from open_webui.models.channels import Channels
|
||||
from open_webui.models.chats import Chats
|
||||
|
||||
@ -152,7 +152,7 @@ async def connect(sid, environ, auth):
|
||||
user = Users.get_user_by_id(data["id"])
|
||||
|
||||
if user:
|
||||
SESSION_POOL[sid] = user.id
|
||||
SESSION_POOL[sid] = user.model_dump()
|
||||
if user.id in USER_POOL:
|
||||
USER_POOL[user.id] = USER_POOL[user.id] + [sid]
|
||||
else:
|
||||
@ -178,7 +178,7 @@ async def user_join(sid, data):
|
||||
if not user:
|
||||
return
|
||||
|
||||
SESSION_POOL[sid] = user.id
|
||||
SESSION_POOL[sid] = user.model_dump()
|
||||
if user.id in USER_POOL:
|
||||
USER_POOL[user.id] = USER_POOL[user.id] + [sid]
|
||||
else:
|
||||
@ -217,22 +217,45 @@ async def join_channel(sid, data):
|
||||
await sio.enter_room(sid, f"channel:{channel.id}")
|
||||
|
||||
|
||||
@sio.on("channel-events")
|
||||
async def channel_events(sid, data):
|
||||
room = f"channel:{data['channel_id']}"
|
||||
participants = sio.manager.get_participants(
|
||||
namespace="/",
|
||||
room=room,
|
||||
)
|
||||
|
||||
sids = [sid for sid, _ in participants]
|
||||
if sid not in sids:
|
||||
return
|
||||
|
||||
event_data = data["data"]
|
||||
event_type = event_data["type"]
|
||||
|
||||
if event_type == "typing":
|
||||
await sio.emit(
|
||||
"channel-events",
|
||||
{
|
||||
"channel_id": data["channel_id"],
|
||||
"data": event_data,
|
||||
"user": UserNameResponse(**SESSION_POOL[sid]).model_dump(),
|
||||
},
|
||||
room=room,
|
||||
)
|
||||
|
||||
|
||||
@sio.on("user-count")
|
||||
async def user_count(sid):
|
||||
await sio.emit("user-count", {"count": len(USER_POOL.items())})
|
||||
|
||||
|
||||
@sio.on("chat")
|
||||
async def chat(sid, data):
|
||||
print("chat", sid, SESSION_POOL[sid], data)
|
||||
|
||||
|
||||
@sio.event
|
||||
async def disconnect(sid):
|
||||
if sid in SESSION_POOL:
|
||||
user_id = SESSION_POOL[sid]
|
||||
user = SESSION_POOL[sid]
|
||||
del SESSION_POOL[sid]
|
||||
|
||||
user_id = user["id"]
|
||||
USER_POOL[user_id] = [_sid for _sid in USER_POOL[user_id] if _sid != sid]
|
||||
|
||||
if len(USER_POOL[user_id]) == 0:
|
||||
@ -289,7 +312,10 @@ def get_event_call(request_info):
|
||||
|
||||
|
||||
def get_user_id_from_session_pool(sid):
|
||||
return SESSION_POOL.get(sid)
|
||||
user = SESSION_POOL.get(sid)
|
||||
if user:
|
||||
return user["id"]
|
||||
return None
|
||||
|
||||
|
||||
def get_user_ids_from_room(room):
|
||||
@ -299,6 +325,8 @@ def get_user_ids_from_room(room):
|
||||
)
|
||||
|
||||
active_user_ids = list(
|
||||
set([SESSION_POOL.get(session_id[0]) for session_id in active_session_ids])
|
||||
set(
|
||||
[SESSION_POOL.get(session_id[0])["id"] for session_id in active_session_ids]
|
||||
)
|
||||
)
|
||||
return active_user_ids
|
||||
|
@ -2,7 +2,7 @@
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { onDestroy, onMount, tick } from 'svelte';
|
||||
|
||||
import { chatId, showSidebar, socket } from '$lib/stores';
|
||||
import { chatId, showSidebar, socket, user } from '$lib/stores';
|
||||
import { getChannelById, getChannelMessages, sendMessage } from '$lib/apis/channels';
|
||||
|
||||
import Messages from './Messages.svelte';
|
||||
@ -20,6 +20,9 @@
|
||||
let channel = null;
|
||||
let messages = null;
|
||||
|
||||
let typingUsers = [];
|
||||
let typingUsersTimeout = {};
|
||||
|
||||
$: if (id) {
|
||||
initHandler();
|
||||
}
|
||||
@ -76,6 +79,32 @@
|
||||
} else if (type === 'message:delete') {
|
||||
console.log('message:delete', data);
|
||||
messages = messages.filter((message) => message.id !== data.id);
|
||||
} else if (type === 'typing') {
|
||||
if (event.user.id === $user.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
typingUsers = data.typing
|
||||
? [
|
||||
...typingUsers,
|
||||
...(typingUsers.find((user) => user.id === event.user.id)
|
||||
? []
|
||||
: [
|
||||
{
|
||||
id: event.user.id,
|
||||
name: event.user.name
|
||||
}
|
||||
])
|
||||
]
|
||||
: typingUsers.filter((user) => user.id !== event.user.id);
|
||||
|
||||
if (typingUsersTimeout[event.user.id]) {
|
||||
clearTimeout(typingUsersTimeout[event.user.id]);
|
||||
}
|
||||
|
||||
typingUsersTimeout[event.user.id] = setTimeout(() => {
|
||||
typingUsers = typingUsers.filter((user) => user.id !== event.user.id);
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -97,6 +126,18 @@
|
||||
}
|
||||
};
|
||||
|
||||
const onChange = async () => {
|
||||
$socket?.emit('channel-events', {
|
||||
channel_id: id,
|
||||
data: {
|
||||
type: 'typing',
|
||||
data: {
|
||||
typing: true
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
if ($chatId) {
|
||||
chatId.set('');
|
||||
@ -150,6 +191,6 @@
|
||||
</div>
|
||||
|
||||
<div class=" pb-[1rem]">
|
||||
<MessageInput onSubmit={submitHandler} {scrollToBottom} {scrollEnd} />
|
||||
<MessageInput {typingUsers} {onChange} onSubmit={submitHandler} {scrollToBottom} {scrollEnd} />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -6,7 +6,7 @@
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
import { config, mobile, settings } from '$lib/stores';
|
||||
import { config, mobile, settings, socket } from '$lib/stores';
|
||||
import { blobToFile, compressImage } from '$lib/utils';
|
||||
|
||||
import Tooltip from '../common/Tooltip.svelte';
|
||||
@ -32,7 +32,10 @@
|
||||
let filesInputElement;
|
||||
let inputFiles;
|
||||
|
||||
export let typingUsers = [];
|
||||
|
||||
export let onSubmit: Function;
|
||||
export let onChange: Function;
|
||||
export let scrollEnd = true;
|
||||
export let scrollToBottom: Function;
|
||||
|
||||
@ -258,6 +261,10 @@
|
||||
chatInputElement?.focus();
|
||||
};
|
||||
|
||||
$: if (content) {
|
||||
onChange();
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
window.setTimeout(() => {
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
@ -290,37 +297,6 @@
|
||||
|
||||
<FilesOverlay show={draggedOver} />
|
||||
|
||||
<div class=" mx-auto inset-x-0 bg-transparent flex justify-center">
|
||||
<div class="flex flex-col px-3 max-w-6xl w-full">
|
||||
<div class="relative">
|
||||
{#if scrollEnd === false}
|
||||
<div class=" absolute -top-12 left-0 right-0 flex justify-center z-30 pointer-events-none">
|
||||
<button
|
||||
class=" bg-white border border-gray-100 dark:border-none dark:bg-white/20 p-1.5 rounded-full pointer-events-auto"
|
||||
on:click={() => {
|
||||
scrollEnd = true;
|
||||
scrollToBottom();
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M10 3a.75.75 0 01.75.75v10.638l3.96-4.158a.75.75 0 111.08 1.04l-5.25 5.5a.75.75 0 01-1.08 0l-5.25-5.5a.75.75 0 111.08-1.04l3.96 4.158V3.75A.75.75 0 0110 3z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input
|
||||
bind:this={filesInputElement}
|
||||
bind:files={inputFiles}
|
||||
@ -341,8 +317,54 @@
|
||||
<div
|
||||
class="{($settings?.widescreenMode ?? null)
|
||||
? 'max-w-full'
|
||||
: 'max-w-6xl'} px-2.5 mx-auto inset-x-0"
|
||||
: 'max-w-6xl'} px-2.5 mx-auto inset-x-0 relative"
|
||||
>
|
||||
<div class="absolute top-0 left-0 right-0 mx-auto inset-x-0 bg-transparent flex justify-center">
|
||||
<div class="flex flex-col px-3 w-full">
|
||||
<div class="relative">
|
||||
{#if scrollEnd === false}
|
||||
<div
|
||||
class=" absolute -top-12 left-0 right-0 flex justify-center z-30 pointer-events-none"
|
||||
>
|
||||
<button
|
||||
class=" bg-white border border-gray-100 dark:border-none dark:bg-white/20 p-1.5 rounded-full pointer-events-auto"
|
||||
on:click={() => {
|
||||
scrollEnd = true;
|
||||
scrollToBottom();
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M10 3a.75.75 0 01.75.75v10.638l3.96-4.158a.75.75 0 111.08 1.04l-5.25 5.5a.75.75 0 01-1.08 0l-5.25-5.5a.75.75 0 111.08-1.04l3.96 4.158V3.75A.75.75 0 0110 3z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="relative">
|
||||
<div class=" -mt-5 bg-gradient-to-t from-white dark:from-gray-900">
|
||||
{#if typingUsers.length > 0}
|
||||
<div class=" text-xs px-4 mb-1">
|
||||
<span class=" font-medium text-black dark:text-white">
|
||||
{typingUsers.map((user) => user.name).join(', ')}
|
||||
</span>
|
||||
{$i18n.t('is typing...')}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="">
|
||||
{#if recording}
|
||||
<VoiceRecording
|
||||
|
Loading…
Reference in New Issue
Block a user