From 6ff6d575071194ea85b51e6df6f832249f8e74c0 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 26 Dec 2024 21:51:09 -0800 Subject: [PATCH] enh: typing indicator --- backend/open_webui/socket/main.py | 50 ++++++++--- src/lib/components/channel/Channel.svelte | 45 +++++++++- .../components/channel/MessageInput.svelte | 88 ++++++++++++------- 3 files changed, 137 insertions(+), 46 deletions(-) diff --git a/backend/open_webui/socket/main.py b/backend/open_webui/socket/main.py index c3a7bfaeb..bed6ada27 100644 --- a/backend/open_webui/socket/main.py +++ b/backend/open_webui/socket/main.py @@ -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 diff --git a/src/lib/components/channel/Channel.svelte b/src/lib/components/channel/Channel.svelte index 34f081158..ba0808958 100644 --- a/src/lib/components/channel/Channel.svelte +++ b/src/lib/components/channel/Channel.svelte @@ -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 @@
- +
diff --git a/src/lib/components/channel/MessageInput.svelte b/src/lib/components/channel/MessageInput.svelte index 3f1bd3d3d..c4290b24e 100644 --- a/src/lib/components/channel/MessageInput.svelte +++ b/src/lib/components/channel/MessageInput.svelte @@ -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 @@ -
-
-
- {#if scrollEnd === false} -
- -
- {/if} -
-
-
- +
+
+
+ {#if scrollEnd === false} +
+ +
+ {/if} +
+ +
+
+ {#if typingUsers.length > 0} +
+ + {typingUsers.map((user) => user.name).join(', ')} + + {$i18n.t('is typing...')} +
+ {/if} +
+
+
+
+
{#if recording}