mirror of
https://github.com/open-webui/open-webui.git
synced 2026-10-19 01:51:15 -05:00
refac
This commit is contained in:
1 parent
d637a39ac9
commit
35c927ba99
8 files changed
+217
-42
No files matched your search
@@ -2401,6 +2401,78 @@ class ChatTable:
|
||||
for chat in all_chats
|
||||
]
|
||||
|
||||
async def get_participant_ids_by_chat_ids(
|
||||
self, chat_ids: list[str], db: AsyncSession | None = None
|
||||
) -> dict[str, list[str]]:
|
||||
participants: dict[str, list[str]] = {chat_id: [] for chat_id in chat_ids}
|
||||
if not chat_ids:
|
||||
return participants
|
||||
|
||||
async with get_async_db_context(db) as session:
|
||||
dialect = session.bind.dialect.name
|
||||
# An empty history object is authoritative, including after message deletion.
|
||||
if dialect == 'sqlite':
|
||||
messages = """
|
||||
SELECT c.id AS chat_id, c.user_id AS owner_id,
|
||||
CASE WHEN m.type = 'object' THEN m.value ELSE '{}' END AS message
|
||||
FROM chat AS c, json_each(
|
||||
CASE
|
||||
WHEN json_type(c.chat, '$.history.messages') = 'object'
|
||||
THEN json_extract(c.chat, '$.history.messages')
|
||||
WHEN json_type(c.chat, '$.messages') = 'array'
|
||||
THEN json_extract(c.chat, '$.messages')
|
||||
ELSE '[]'
|
||||
END
|
||||
) AS m
|
||||
WHERE c.id IN :chat_ids
|
||||
"""
|
||||
role = "json_extract(message, '$.role')"
|
||||
|
||||
def author(path):
|
||||
return f"""CASE WHEN json_type(message, '{path}') = 'text'
|
||||
AND trim(json_extract(message, '{path}')) <> ''
|
||||
THEN json_extract(message, '{path}') END"""
|
||||
|
||||
author_id = f"COALESCE({author('$.user_id')}, {author('$.user.id')}, NULLIF(trim(owner_id), ''))"
|
||||
elif dialect == 'postgresql':
|
||||
messages = """
|
||||
SELECT c.id AS chat_id, c.user_id AS owner_id, m.message
|
||||
FROM chat AS c CROSS JOIN LATERAL (
|
||||
SELECT value AS message FROM json_each(
|
||||
CASE WHEN json_typeof(c.chat#>'{history,messages}') = 'object'
|
||||
THEN c.chat#>'{history,messages}' ELSE '{}'::json END
|
||||
)
|
||||
UNION ALL
|
||||
SELECT value AS message FROM json_array_elements(
|
||||
CASE WHEN json_typeof(c.chat#>'{history,messages}') IS DISTINCT FROM 'object'
|
||||
AND json_typeof(c.chat->'messages') = 'array'
|
||||
THEN c.chat->'messages' ELSE '[]'::json END
|
||||
)
|
||||
) AS m
|
||||
WHERE c.id IN :chat_ids
|
||||
"""
|
||||
role = "message->>'role'"
|
||||
|
||||
def author(path):
|
||||
return f"""CASE WHEN json_typeof(message#>'{path}') = 'string'
|
||||
AND trim(message#>>'{path}') <> '' THEN message#>>'{path}' END"""
|
||||
|
||||
author_id = f"COALESCE({author('{user_id}')}, {author('{user,id}')}, NULLIF(trim(owner_id), ''))"
|
||||
else:
|
||||
raise NotImplementedError(f'Unsupported dialect: {dialect}')
|
||||
|
||||
query = text(f"""
|
||||
WITH messages AS ({messages}), authors AS (
|
||||
SELECT chat_id, {author_id} AS author_id
|
||||
FROM messages WHERE {role} = 'user'
|
||||
)
|
||||
SELECT DISTINCT chat_id, author_id FROM authors
|
||||
WHERE author_id IS NOT NULL ORDER BY chat_id, author_id
|
||||
""").bindparams(bindparam('chat_ids', expanding=True))
|
||||
for chat_id, author_id in (await session.execute(query, {'chat_ids': chat_ids})).all():
|
||||
participants[chat_id].append(author_id)
|
||||
return participants
|
||||
|
||||
async def count_all_chats_by_folder_id(
|
||||
self,
|
||||
folder_id: str,
|
||||
|
||||
@@ -629,14 +629,23 @@ async def get_shared_folder_chats(
|
||||
)
|
||||
total = await Chats.count_all_chats_by_folder_id(id, db=db) if page is not None else len(chats)
|
||||
|
||||
# Resolve owner names for display (avatar URLs are constructed client-side)
|
||||
owner_cache: dict[str, str] = {}
|
||||
participant_ids = await Chats.get_participant_ids_by_chat_ids([chat['id'] for chat in chats], db=db)
|
||||
user_ids = {chat['user_id'] for chat in chats}
|
||||
user_ids.update(uid for ids in participant_ids.values() for uid in ids)
|
||||
users = await Users.get_users_by_user_ids(list(user_ids), db=db) if user_ids else []
|
||||
names = {u.id: u.name for u in users}
|
||||
for chat in chats:
|
||||
uid = chat['user_id']
|
||||
if uid not in owner_cache:
|
||||
u = await Users.get_user_by_id(uid, db=db)
|
||||
owner_cache[uid] = u.name if u else 'Unknown'
|
||||
chat['owner_name'] = owner_cache[uid]
|
||||
chat['owner_name'] = names.get(uid, 'Unknown')
|
||||
chat['participants'] = sorted(
|
||||
[{'id': author_id, 'name': names.get(author_id)} for author_id in participant_ids[chat['id']]],
|
||||
key=lambda author: (
|
||||
author['id'] != uid,
|
||||
not bool(author['name']),
|
||||
(author['name'] or '').casefold(),
|
||||
author['id'],
|
||||
),
|
||||
)
|
||||
chat['active'] = False
|
||||
if chat['user_id'] != user.id:
|
||||
chat['last_read_at'] = chat['updated_at']
|
||||
|
||||
@@ -323,6 +323,11 @@ export const getSharedFolders = async (token: string) => {
|
||||
return res;
|
||||
};
|
||||
|
||||
export type ChatParticipant = {
|
||||
id: string;
|
||||
name: string | null;
|
||||
};
|
||||
|
||||
export const getSharedFolderChats = async (
|
||||
token: string,
|
||||
folderId: string,
|
||||
|
||||
@@ -12,12 +12,12 @@
|
||||
import dayjs from 'dayjs';
|
||||
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
||||
import { getTimeRange } from '$lib/utils';
|
||||
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
||||
import type { ChatParticipant } from '$lib/apis/folders';
|
||||
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
|
||||
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
||||
import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte';
|
||||
import ChevronRight from '$lib/components/icons/ChevronRight.svelte';
|
||||
import ProfilePreview from '$lib/components/channel/Messages/Message/ProfilePreview.svelte';
|
||||
import ChatParticipants from '$lib/components/common/ChatParticipants.svelte';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import { chatId, socket } from '$lib/stores';
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
time_range?: string;
|
||||
user_id?: string;
|
||||
owner_name?: string;
|
||||
participants?: ChatParticipant[];
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
@@ -211,20 +212,12 @@
|
||||
<div
|
||||
class=" w-full flex justify-between items-center rounded-lg text-sm py-2 px-3 hover:bg-gray-50 dark:hover:bg-gray-850"
|
||||
>
|
||||
{#if showOwnerInfo && chat.user_id && chat.owner_name}
|
||||
{#if (showOwnerInfo && chat.user_id) || (chat.participants?.length ?? 0) > 1}
|
||||
<div class="flex shrink-0 me-2">
|
||||
<ProfilePreview user={{ id: chat.user_id, name: chat.owner_name }}>
|
||||
<img
|
||||
src="{WEBUI_API_BASE_URL}/users/{chat.user_id}/profile/image"
|
||||
alt=""
|
||||
class="size-4 rounded-full shrink-0 object-cover"
|
||||
on:error={(e) => {
|
||||
if (!e.currentTarget.src.endsWith('/static/favicon.png')) {
|
||||
e.currentTarget.src = `${WEBUI_BASE_URL}/static/favicon.png`;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</ProfilePreview>
|
||||
<ChatParticipants
|
||||
participants={chat.participants ?? []}
|
||||
owner={chat.user_id ? { id: chat.user_id, name: chat.owner_name ?? null } : null}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
import { Popover } from 'bits-ui';
|
||||
import type { ChatParticipant } from '$lib/apis/folders';
|
||||
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
||||
import ProfilePreview from '$lib/components/channel/Messages/Message/ProfilePreview.svelte';
|
||||
|
||||
const i18n = getContext<typeof import('$lib/i18n').default>('i18n');
|
||||
|
||||
export let participants: ChatParticipant[] = [];
|
||||
export let owner: ChatParticipant | null = null;
|
||||
export let avatarClass = 'size-4';
|
||||
export let activeProfileId: string | null = null;
|
||||
export let overflowOpen = false;
|
||||
|
||||
$: people = participants.length ? participants : owner?.id ? [owner] : [];
|
||||
$: if (activeProfileId && !people.some((person) => person.id === activeProfileId)) {
|
||||
activeProfileId = null;
|
||||
}
|
||||
$: if (people.length <= 3) overflowOpen = false;
|
||||
|
||||
const setProfileOpen = (id: string, open: boolean) => {
|
||||
if (open) activeProfileId = id;
|
||||
else if (activeProfileId === id) activeProfileId = null;
|
||||
};
|
||||
</script>
|
||||
|
||||
{#snippet avatar(person: ChatParticipant, className: string)}
|
||||
<img
|
||||
src="{WEBUI_API_BASE_URL}/users/{person.id}/profile/image"
|
||||
alt=""
|
||||
draggable="false"
|
||||
class="{className} rounded-full shrink-0 object-cover"
|
||||
onerror={(event) => {
|
||||
if (!event.currentTarget.src.endsWith('/static/favicon.png')) {
|
||||
event.currentTarget.src = `${WEBUI_BASE_URL}/static/favicon.png`;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/snippet}
|
||||
|
||||
<div class="flex shrink-0 items-center gap-1" data-chat-participants draggable="false">
|
||||
<div class="flex items-center -space-x-1 rtl:space-x-reverse">
|
||||
{#each people.slice(0, 3) as person (person.id)}
|
||||
<div class="relative rounded-full hover:z-10 focus-within:z-10">
|
||||
<ProfilePreview
|
||||
user={{ id: person.id, name: person.name || $i18n.t('Unknown User') }}
|
||||
bind:openPreview={
|
||||
() => !overflowOpen && activeProfileId === person.id,
|
||||
(open) => setProfileOpen(person.id, open)
|
||||
}
|
||||
>
|
||||
{@render avatar(person, `${avatarClass} bg-white ring-1 ring-white dark:bg-gray-900 dark:ring-gray-900`)}
|
||||
</ProfilePreview>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if people.length > 3}
|
||||
<Popover.Root bind:open={overflowOpen} onOpenChange={() => (activeProfileId = null)}>
|
||||
<Popover.Trigger
|
||||
class="shrink-0 rounded px-0.5 text-[0.625rem] text-gray-500 hover:bg-gray-100 focus-visible:outline dark:text-gray-400 dark:hover:bg-gray-800"
|
||||
aria-label={$i18n.t('Show all {{count}} contributors', { count: people.length })}
|
||||
>
|
||||
+{people.length - 3}
|
||||
</Popover.Trigger>
|
||||
<Popover.Portal>
|
||||
<Popover.Content
|
||||
side="bottom"
|
||||
align="start"
|
||||
sideOffset={6}
|
||||
class="z-[9999] w-60 max-w-[calc(100vw-1.5rem)] max-h-80 overflow-y-auto rounded-xl bg-white p-2 text-gray-900 shadow-lg ring-1 ring-black/5 dark:bg-gray-850 dark:text-gray-100 dark:ring-white/10"
|
||||
aria-label={$i18n.t('Contributors')}
|
||||
>
|
||||
<ul class="space-y-1">
|
||||
{#each people as person (person.id)}
|
||||
<li class="rounded-lg p-1 hover:bg-gray-50 dark:hover:bg-gray-800">
|
||||
<ProfilePreview
|
||||
user={{ id: person.id, name: person.name || $i18n.t('Unknown User') }}
|
||||
bind:openPreview={
|
||||
() => overflowOpen && activeProfileId === person.id,
|
||||
(open) => setProfileOpen(person.id, open)
|
||||
}
|
||||
>
|
||||
{@render avatar(person, 'size-6')}
|
||||
<span class="ms-2 min-w-0 truncate text-sm">
|
||||
{person.name || $i18n.t('Unknown User')}
|
||||
</span>
|
||||
</ProfilePreview>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</Popover.Content>
|
||||
</Popover.Portal>
|
||||
</Popover.Root>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
||||
import type { ChatParticipant } from '$lib/apis/folders';
|
||||
import { goto, invalidate, invalidateAll } from '$app/navigation';
|
||||
import { onMount, getContext, createEventDispatcher, tick } from 'svelte';
|
||||
import { LinkPreview } from 'bits-ui';
|
||||
@@ -50,7 +50,7 @@
|
||||
import DragGhost from '$lib/components/common/DragGhost.svelte';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import ChatHoverPreview from './ChatHoverPreview.svelte';
|
||||
import ProfilePreview from '$lib/components/channel/Messages/Message/ProfilePreview.svelte';
|
||||
import ChatParticipants from '$lib/components/common/ChatParticipants.svelte';
|
||||
import ChatIcon from './icons/Chat.svelte';
|
||||
import MoreHorizontalIcon from './icons/MoreHorizontal.svelte';
|
||||
import SparklesIcon from './icons/Sparkles.svelte';
|
||||
@@ -79,6 +79,7 @@
|
||||
|
||||
export let ownerName: string | null = null;
|
||||
export let ownerUserId: string | null = null;
|
||||
export let participants: ChatParticipant[] = [];
|
||||
export let onReadStateChange: (data: Record<string, unknown>) => void = () => {};
|
||||
|
||||
export let onDragEnd = () => {};
|
||||
@@ -108,9 +109,10 @@
|
||||
let focusWithin = false;
|
||||
let menuOpen = false;
|
||||
let openPreview = false;
|
||||
let openProfilePreview = false;
|
||||
let activeProfileId: string | null = null;
|
||||
let overflowOpen = false;
|
||||
|
||||
$: if (openProfilePreview) openPreview = false;
|
||||
$: if (activeProfileId || overflowOpen) openPreview = false;
|
||||
|
||||
const closeHoverPreview = () => {
|
||||
if (openPreview) {
|
||||
@@ -636,23 +638,15 @@
|
||||
</div>
|
||||
{:else}
|
||||
<div id="sidebar-chat-item" class={chatItemClass}>
|
||||
{#if ownerUserId}
|
||||
{#if ownerUserId || participants.length > 1}
|
||||
<div class="flex items-center self-center shrink-0 mr-2">
|
||||
<ProfilePreview
|
||||
user={{ id: ownerUserId, name: ownerName }}
|
||||
bind:openPreview={openProfilePreview}
|
||||
>
|
||||
<img
|
||||
src="{WEBUI_API_BASE_URL}/users/{ownerUserId}/profile/image"
|
||||
alt=""
|
||||
class="size-3.5 rounded-full shrink-0 object-cover"
|
||||
on:error={(e) => {
|
||||
if (!e.currentTarget.src.endsWith('/static/favicon.png')) {
|
||||
e.currentTarget.src = `${WEBUI_BASE_URL}/static/favicon.png`;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</ProfilePreview>
|
||||
<ChatParticipants
|
||||
{participants}
|
||||
owner={ownerUserId ? { id: ownerUserId, name: ownerName } : null}
|
||||
avatarClass="size-3.5"
|
||||
bind:activeProfileId
|
||||
bind:overflowOpen
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{#if $mobile}
|
||||
@@ -672,7 +666,10 @@
|
||||
disabled={confirmEdit || dragged || !($settings?.chatHoverPreview ?? true)}
|
||||
bind:open={openPreview}
|
||||
onOpenChange={(open) => {
|
||||
if (open) openProfilePreview = false;
|
||||
if (open) {
|
||||
activeProfileId = null;
|
||||
overflowOpen = false;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<LinkPreview.Trigger
|
||||
|
||||
@@ -996,6 +996,7 @@
|
||||
lastReadAt={chat.last_read_at}
|
||||
active={chat.active ?? false}
|
||||
ownerName={chat.owner_name ?? null}
|
||||
participants={chat.participants ?? []}
|
||||
ownerUserId={folders[folderId]?.showOwnerInfo ||
|
||||
folders[folderId]?.shared ||
|
||||
chat.user_id !== $user?.id
|
||||
|
||||
@@ -130,6 +130,7 @@
|
||||
lastReadAt={chat.last_read_at}
|
||||
active={chat.active ?? false}
|
||||
ownerName={chat.owner_name}
|
||||
participants={chat.participants ?? []}
|
||||
ownerUserId={chat.user_id}
|
||||
readonly={chat.readonly ?? !isWritable}
|
||||
/>
|
||||
|
||||
Reference in new issue
Block a user