mirror of
https://github.com/open-webui/open-webui
synced 2025-06-26 18:26:48 +00:00
enh: typing indicator
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user