mirror of
https://github.com/open-webui/open-webui.git
synced 2026-10-10 03:27:57 +00:00
refac
This commit is contained in:
parent
b5a20423e4
commit
1e9dd6e330
6 changed files with 120 additions and 4 deletions
|
|
@ -700,6 +700,32 @@ async def chat_events(sid, data):
|
|||
event_data = data.get('data', {})
|
||||
event_type = event_data.get('type')
|
||||
|
||||
if event_type == 'typing':
|
||||
chat_id = data.get('chat_id')
|
||||
typing_data = event_data.get('data')
|
||||
typing = typing_data.get('typing') if isinstance(typing_data, dict) else None
|
||||
if not isinstance(chat_id, str) or not is_saved_chat_id(chat_id) or not isinstance(typing, bool):
|
||||
return False
|
||||
room = f'chat:{chat_id}'
|
||||
if sid not in (get_room_sid_map(sio.manager, '/', room) or {}):
|
||||
return False
|
||||
sender = await Users.get_user_by_id(user['id'])
|
||||
if not sender or not await Chats.get_accessible_chat_by_id(chat_id, sender, permission='write'):
|
||||
return False
|
||||
await sio.emit(
|
||||
'events',
|
||||
{
|
||||
'chat_id': chat_id,
|
||||
'user_id': sender.id,
|
||||
'user': {'id': sender.id, 'name': sender.name},
|
||||
'shared': True,
|
||||
'data': {'type': 'typing', 'data': {'typing': typing}},
|
||||
},
|
||||
room=room,
|
||||
skip_sid=sid,
|
||||
)
|
||||
return True
|
||||
|
||||
if event_type in {'join', 'leave'}:
|
||||
chat_id = data.get('chat_id')
|
||||
if not isinstance(chat_id, str) or not is_saved_chat_id(chat_id):
|
||||
|
|
|
|||
|
|
@ -1264,6 +1264,36 @@
|
|||
|
||||
let joinedChatId = '';
|
||||
let chatRefresh: { id: string; promise: Promise<void> } | null = null;
|
||||
let typingUsers: { id: string; name: string }[] = [];
|
||||
let typingUsersTimeout: Record<string, ReturnType<typeof setTimeout>> = {};
|
||||
let lastTypingEmit = 0;
|
||||
|
||||
const emitTyping = (typing: boolean) => {
|
||||
if (!$socket?.connected || !joinedChatId) return;
|
||||
if (typing && (joinedChatId !== $chatId || (readOnly && chat?.chat?.share_mode !== 'continue')))
|
||||
return;
|
||||
if (typing ? Date.now() - lastTypingEmit < 2000 : !lastTypingEmit) return;
|
||||
lastTypingEmit = typing ? Date.now() : 0;
|
||||
$socket.emit('events:chat', {
|
||||
chat_id: joinedChatId,
|
||||
data: { type: 'typing', data: { typing } }
|
||||
});
|
||||
};
|
||||
|
||||
const removeTypingUser = (id: string) => {
|
||||
clearTimeout(typingUsersTimeout[id]);
|
||||
delete typingUsersTimeout[id];
|
||||
typingUsers = typingUsers.filter((user) => user.id !== id);
|
||||
};
|
||||
|
||||
const clearTyping = () => {
|
||||
emitTyping(false);
|
||||
Object.values(typingUsersTimeout).forEach(clearTimeout);
|
||||
typingUsersTimeout = {};
|
||||
typingUsers = [];
|
||||
lastTypingEmit = 0;
|
||||
};
|
||||
$: if (!prompt.trim()) emitTyping(false);
|
||||
|
||||
const mergeChatMessages = (
|
||||
incoming: typeof history,
|
||||
|
|
@ -1333,6 +1363,7 @@
|
|||
const syncChatRoom = (socket: typeof $socket, id: string, temporary: boolean) => {
|
||||
const next = socket && id && !temporary && !isTemporaryChatId(id) ? id : '';
|
||||
if (next === joinedChatId) return;
|
||||
clearTyping();
|
||||
if (joinedChatId)
|
||||
socket?.emit('events:chat', { chat_id: joinedChatId, data: { type: 'leave' } });
|
||||
joinedChatId = next;
|
||||
|
|
@ -1354,12 +1385,32 @@
|
|||
) {
|
||||
await tick();
|
||||
const type = event?.data?.type ?? null;
|
||||
if (type === 'typing') {
|
||||
const sender = event.user;
|
||||
if (
|
||||
!sender?.id ||
|
||||
sender.id === $user?.id ||
|
||||
(readOnly && chat?.chat?.share_mode !== 'continue')
|
||||
)
|
||||
return;
|
||||
if (event.data.data?.typing) {
|
||||
if (!typingUsers.some((entry) => entry.id === sender.id))
|
||||
typingUsers = [...typingUsers, { id: sender.id, name: sender.name }];
|
||||
clearTimeout(typingUsersTimeout[sender.id]);
|
||||
typingUsersTimeout[sender.id] = setTimeout(() => removeTypingUser(sender.id), 5000);
|
||||
} else {
|
||||
removeTypingUser(sender.id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (event.shared) {
|
||||
if (type === 'chat:messages') {
|
||||
removeTypingUser(event.user_id);
|
||||
mergeChatMessages(event.data.data);
|
||||
return;
|
||||
}
|
||||
if (type === 'chat:access' || type === 'chat:active') {
|
||||
if (type === 'chat:access') clearTyping();
|
||||
if (type === 'chat:access' || event.data.data?.active === false) await refreshChat();
|
||||
return;
|
||||
}
|
||||
|
|
@ -1684,6 +1735,7 @@
|
|||
);
|
||||
|
||||
const handleSocketConnect = async () => {
|
||||
clearTyping();
|
||||
if (!$chatId || $temporaryChatEnabled) return;
|
||||
joinedChatId = '';
|
||||
syncChatRoom($socket, $chatId, $temporaryChatEnabled);
|
||||
|
|
@ -1698,6 +1750,7 @@
|
|||
window.addEventListener('message', onMessageHandler);
|
||||
$socket?.on('events', chatEventHandler);
|
||||
$socket?.on('connect', handleSocketConnect);
|
||||
$socket?.on('disconnect', clearTyping);
|
||||
|
||||
$audioQueue?.destroy();
|
||||
|
||||
|
|
@ -1792,6 +1845,7 @@
|
|||
// Clear the selected chat when leaving the chat surface (e.g. navigating
|
||||
// to the admin panel), otherwise the previously-viewed chat stays selected
|
||||
// in the sidebar and deleting/archiving it wrongly navigates away.
|
||||
clearTyping();
|
||||
if (joinedChatId)
|
||||
$socket?.emit('events:chat', { chat_id: joinedChatId, data: { type: 'leave' } });
|
||||
joinedChatId = '';
|
||||
|
|
@ -1801,6 +1855,7 @@
|
|||
window.removeEventListener('message', onMessageHandler);
|
||||
$socket?.off('events', chatEventHandler);
|
||||
$socket?.off('connect', handleSocketConnect);
|
||||
$socket?.off('disconnect', clearTyping);
|
||||
dismissContextCompactionToast();
|
||||
audioQueueInstance?.destroy();
|
||||
audioQueue.set(null);
|
||||
|
|
@ -3458,6 +3513,7 @@
|
|||
bridge: bridgeRequest = null
|
||||
}: { _raw?: boolean; bridge?: { userMessageId: string; modelId: string } | null } = {}
|
||||
): Promise<BridgeSubmission> => {
|
||||
emitTyping(false);
|
||||
if (bridgeRequest && (selectedModelIds.length !== 1 || !bridgeRequest.modelId))
|
||||
return { status: 'rejected' };
|
||||
|
||||
|
|
@ -4831,6 +4887,8 @@
|
|||
class=" pb-2 {dragged ? 'z-0' : 'z-10'}"
|
||||
>
|
||||
<MessageInput
|
||||
{typingUsers}
|
||||
on:typing={(event) => emitTyping(event.detail)}
|
||||
callActive={!!(bridge?.connected || bridge?.connecting)}
|
||||
bind:this={messageInput}
|
||||
{history}
|
||||
|
|
@ -4924,6 +4982,8 @@
|
|||
{/if}
|
||||
<div id={embedded ? messageInputDropzoneId : undefined} class="pb-2 z-10">
|
||||
<MessageInput
|
||||
{typingUsers}
|
||||
on:typing={(event) => emitTyping(event.detail)}
|
||||
callActive={!!(bridge?.connected || bridge?.connecting)}
|
||||
bind:this={messageInput}
|
||||
{history}
|
||||
|
|
|
|||
|
|
@ -117,6 +117,7 @@
|
|||
import AskUserCard from './AskUserCard.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import InputModal from '../common/InputModal.svelte';
|
||||
import Skeleton from './Messages/Skeleton.svelte';
|
||||
import Expand from '../icons/Expand.svelte';
|
||||
import QueuedMessageItem from './MessageInput/QueuedMessageItem.svelte';
|
||||
import TaskList from './Messages/ResponseMessage/TaskList.svelte';
|
||||
|
|
@ -134,6 +135,12 @@
|
|||
|
||||
export let onUpload: Function = (e) => {};
|
||||
export let onChange: Function = () => {};
|
||||
export let typingUsers: { id: string; name: string }[] = [];
|
||||
|
||||
const handleTypingInput = (event: Event) => {
|
||||
const input = event.target as HTMLElement;
|
||||
if (input.isContentEditable) dispatch('typing', Boolean(input.textContent?.trim()));
|
||||
};
|
||||
export let onWebSearchToggle: Function = () => {};
|
||||
|
||||
export let createMessagePair: Function;
|
||||
|
|
@ -1672,6 +1679,7 @@
|
|||
|
||||
<InputModal
|
||||
bind:show={showInputModal}
|
||||
oninput={handleTypingInput}
|
||||
bind:value={prompt}
|
||||
bind:inputContent
|
||||
onChange={(content) => {
|
||||
|
|
@ -1693,6 +1701,21 @@
|
|||
: 'max-w-[58rem]'} w-full"
|
||||
>
|
||||
<div class="relative">
|
||||
{#if typingUsers.length > 0}
|
||||
<div
|
||||
class="absolute -top-7 inset-x-0 pointer-events-none select-none bg-linear-to-t from-white dark:from-gray-900 to-transparent pb-2.5"
|
||||
>
|
||||
<div class="text-xs px-1 mt-1.5 flex items-center gap-1.5">
|
||||
<Skeleton size="xs" />
|
||||
<div class="min-w-0 truncate">
|
||||
<span class="font-normal text-black dark:text-white">
|
||||
{typingUsers.map((user) => user.name).join(', ')}
|
||||
</span>
|
||||
{$i18n.t(typingUsers.length === 1 ? 'is typing...' : 'are typing...')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if autoScroll === false && history?.currentId}
|
||||
<div
|
||||
class=" absolute -top-12 left-0 right-0 flex justify-center z-30 pointer-events-none"
|
||||
|
|
@ -2062,6 +2085,7 @@
|
|||
: 'pt-2'
|
||||
: ''}"
|
||||
id="chat-input-container"
|
||||
on:input={handleTypingInput}
|
||||
>
|
||||
{#if suggestions}
|
||||
{#key $settings?.richTextInput ?? true}
|
||||
|
|
|
|||
|
|
@ -142,12 +142,16 @@
|
|||
/>
|
||||
|
||||
<div
|
||||
class="flex w-full user-message group {!isOwn && !message?.meta?.internal ? 'mb-2' : ''}"
|
||||
class="flex w-full user-message group {!isOwn && !message?.meta?.internal
|
||||
? compactPreview
|
||||
? 'my-2'
|
||||
: 'mb-2'
|
||||
: ''}"
|
||||
dir={$settings.chatDirection}
|
||||
id="message-{message.id}"
|
||||
style="scroll-margin-top: 3rem;"
|
||||
>
|
||||
{#if (!($settings?.chatBubble ?? true) || !isOwn) && !(message?.meta?.internal === true && message?.meta?.type === 'subagent') && !(message?.meta?.internal === true && message?.meta?.type === 'timer')}
|
||||
{#if !compactPreview && (!($settings?.chatBubble ?? true) || !isOwn) && !(message?.meta?.internal === true && message?.meta?.type === 'subagent') && !(message?.meta?.internal === true && message?.meta?.type === 'timer')}
|
||||
<div
|
||||
class="shrink-0 me-2 {($settings?.chatBubble ?? true) ? 'mt-6' : 'hidden @lg:flex mt-0.5'}"
|
||||
>
|
||||
|
|
@ -169,7 +173,7 @@
|
|||
? ''
|
||||
: 'pl-1'}"
|
||||
>
|
||||
{#if (!($settings?.chatBubble ?? true) || !isOwn) && !(message?.meta?.internal === true && message?.meta?.type === 'subagent') && !(message?.meta?.internal === true && message?.meta?.type === 'timer')}
|
||||
{#if !compactPreview && (!($settings?.chatBubble ?? true) || !isOwn) && !(message?.meta?.internal === true && message?.meta?.type === 'subagent') && !(message?.meta?.internal === true && message?.meta?.type === 'timer')}
|
||||
<div
|
||||
class={($settings?.chatBubble ?? true)
|
||||
? 'mb-1 ps-2 text-xs text-gray-400 dark:text-gray-500'
|
||||
|
|
|
|||
|
|
@ -17,13 +17,14 @@
|
|||
export let generateAutoCompletion = null;
|
||||
|
||||
export let onChange = () => {};
|
||||
export let oninput: ((event: Event) => void) | undefined = undefined;
|
||||
export let onClose = () => {};
|
||||
|
||||
let inputElement;
|
||||
</script>
|
||||
|
||||
<Drawer bind:show>
|
||||
<div class="flex h-full min-h-screen flex-col">
|
||||
<div class="flex h-full min-h-screen flex-col" {oninput}>
|
||||
<div
|
||||
class=" sticky top-0 z-30 flex justify-between bg-white px-4.5 pt-3 pb-3 dark:bg-gray-900 dark:text-gray-100"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1256,6 +1256,7 @@
|
|||
"Invalid JSON format in {{NAME}}": "",
|
||||
"Invalid JSON format in Additional Config": "",
|
||||
"Invalid JSON format in MinerU Parameters": "",
|
||||
"are typing...": "",
|
||||
"is typing...": "",
|
||||
"Italic": "",
|
||||
"January": "",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue