This commit is contained in:
Timothy Jaeryang Baek 2026-10-08 14:58:06 +04:00
parent f55e09e50d
commit 6cfd6987e5
9 changed files with 101 additions and 36 deletions

View file

@ -1404,6 +1404,7 @@ async def chat_completion(
if user_message_id and user_message:
user_message['childrenIds'] = all_assistant_ids
user_message['user_id'] = user.id
user_message['user'] = {'id': user.id, 'name': user.name}
history_messages[user_message_id] = user_message
for entry in message_ids:

View file

@ -603,7 +603,9 @@ class ChatTable:
await ChatMessages.upsert_message(
message_id=message_id,
chat_id=id,
user_id=user_id,
user_id=message.get('user_id')
or (message['user'].get('id') if isinstance(message.get('user'), dict) else None)
or user_id,
data=message,
)
except Exception as e:
@ -677,7 +679,11 @@ class ChatTable:
messages = list((chat.chat.get('history', {}).get('messages') or {}).values())
messages.extend(chat.chat.get('messages') or [])
for message in messages:
message['user_id'] = user_id
message['user_id'] = (
message.get('user_id')
or (message['user'].get('id') if isinstance(message.get('user'), dict) else None)
or user_id
)
return chat
async def import_chats(
@ -729,7 +735,9 @@ class ChatTable:
await ChatMessages.upsert_message(
message_id=message_id,
chat_id=imported_chat.id,
user_id=user_id,
user_id=message.get('user_id')
or (message['user'].get('id') if isinstance(message.get('user'), dict) else None)
or user_id,
data=message,
)
except Exception as e:
@ -1839,7 +1847,8 @@ class ChatTable:
if (
chat.user_id == user.id
or user.role == 'admin'
and (ENABLE_ADMIN_CHAT_ACCESS or permission == 'read' and is_internal_chat(chat.meta))
and permission == 'read'
and (ENABLE_ADMIN_CHAT_ACCESS or is_internal_chat(chat.meta))
):
return ChatModel.model_validate(chat)
from open_webui.models.shared_chats import SharedChats

View file

@ -127,7 +127,9 @@ async def can_read_shared_chat(user, shared, db: AsyncSession) -> bool:
)
def shared_chat_response(chat, user=None):
async def shared_chat_response(chat, user=None, db=None):
from open_webui.models.users import Users
data = ChatResponse.model_validate(chat, from_attributes=True).model_dump()
if user is None or chat.user_id != user.id:
data['variables'] = {}
@ -135,9 +137,24 @@ def shared_chat_response(chat, user=None):
data['chat'].pop(key, None)
messages = list((data['chat'].get('history', {}).get('messages') or {}).values())
messages.extend(data['chat'].get('messages') or [])
authors = {}
for message in messages:
if message.get('role') == 'user':
author = message.get('user')
author = author if isinstance(author, dict) else {}
author_id = message.get('user_id') or author.get('id') or chat.user_id
message['user_id'] = author_id
if author.get('id') == author_id and author.get('name'):
authors[author_id] = {'id': author_id, 'name': author['name']}
if user is None or (message.get('user_id') or chat.user_id) != user.id:
message.pop('meta', None)
missing_ids = {message['user_id'] for message in messages if message.get('role') == 'user'} - authors.keys()
if missing_ids:
for author in await Users.get_users_by_user_ids(list(missing_ids), db=db):
authors[author.id] = {'id': author.id, 'name': author.name}
for message in messages:
if message.get('role') == 'user':
message['user'] = authors.get(message['user_id'], {'id': message['user_id'], 'name': ''})
return data
@ -1240,7 +1257,7 @@ async def get_shared_chat_by_id(
live = (
shared.chat.get('share_mode') == 'continue'
and user is not None
and await can_read_shared_chat(user, shared, db=db)
and await Chats.get_accessible_chat_by_id(shared.chat_id, user, db=db, permission='write') is not None
)
chat = (
await Chats.get_chat_by_id(shared.chat_id, db=db)
@ -1248,7 +1265,7 @@ async def get_shared_chat_by_id(
else await Chats.get_chat_by_share_id(share_id, db=db)
)
if chat:
data = shared_chat_response(chat, user)
data = await shared_chat_response(chat, user, db=db)
data['chat']['share_mode'] = 'continue' if live else None
return data
@ -1262,7 +1279,13 @@ async def get_shared_chat_by_id(
if user is not None and user.role == 'admin' and ENABLE_ADMIN_CHAT_ACCESS:
chat = await Chats.get_chat_by_id(share_id, db=db)
if chat:
return ChatResponse.model_validate(chat, from_attributes=True)
data = await shared_chat_response(chat, user, db=db)
data['chat']['share_mode'] = (
'continue'
if await Chats.get_accessible_chat_by_id(chat.id, user, db=db, permission='write', chat=chat)
else None
)
return data
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND)
@ -1370,9 +1393,11 @@ async def get_chat_by_id(
)
if chat:
data = shared_chat_response(chat, user)
data = await shared_chat_response(chat, user, db=db)
data['chat']['share_mode'] = (
'continue' if await Chats.get_accessible_chat_by_id(id, user, db=db, permission='write', chat=chat) else None
'continue'
if await Chats.get_accessible_chat_by_id(id, user, db=db, permission='write', chat=chat)
else None
)
data = overlay_response_streams(
data,
@ -1746,11 +1771,14 @@ async def fork_chat_by_id(
detail=detail,
) from exc
# An unfinished message is stale unless it is awaiting tool approval
for message in fork_history['messages'].values():
author = (history.get('messages', {}).get(message['id']) or {}).get('user')
if isinstance(author, dict) and author.get('id') == message.get('user_id'):
message['user'] = author
if message.get('role') != 'assistant' or message.get('done') is not False:
continue
# An unfinished message is stale unless it is awaiting tool approval
output = message.get('output')
if isinstance(output, list) and any(
isinstance(item, dict)
@ -1910,7 +1938,7 @@ async def clone_shared_chat_by_id(
)
updated_chat = {
**shared_chat_response(chat, user)['chat'],
**(await shared_chat_response(chat, user, db=db))['chat'],
'originalChatId': chat.id,
'branchPointMessageId': chat.chat['history']['currentId'],
'title': f'Clone of {chat.title}',

View file

@ -2997,6 +2997,8 @@
parentId: parentMessage ? parentMessage.id : null,
childrenIds: [responseMessageId],
role: 'user',
user_id: $user?.id,
user: { id: $user?.id, name: $user?.name },
content: userPrompt ? userPrompt : `[PROMPT] ${userMessageId}`,
timestamp: Math.floor(Date.now() / 1000)
};
@ -3279,6 +3281,7 @@
parentId: history.currentId ?? null,
childrenIds: [],
role: 'user',
user: { id: $user?.id, name: $user?.name },
user_id: $user?.id,
content: inputContent,
files: _files.length > 0 ? _files : undefined,
@ -4188,6 +4191,8 @@
parentId: parentId,
childrenIds: [],
role: 'user',
user_id: $user?.id,
user: { id: $user?.id, name: $user?.name },
content: userPrompt,
models: selectedModels,
timestamp: Math.floor(Date.now() / 1000) // Unix epoch
@ -4775,7 +4780,7 @@
<Messages
bind:this={messagesRef}
chatId={$chatId}
user={chatOwner ?? $user}
user={chatOwner ?? (chat ? { id: chat.user_id } : $user)}
{readOnly}
shareMode={chat?.chat?.share_mode ?? null}
bind:history

View file

@ -333,6 +333,8 @@
parentId: history.messages[messageId].parentId,
childrenIds: [],
role: 'user',
user_id: $_user?.id,
user: { id: $_user?.id, name: $_user?.name },
content: userPrompt,
...(files && { files: files }),
models: selectedModels,

View file

@ -68,7 +68,7 @@
{#if history.messages[messageId]}
{#if history.messages[messageId].role === 'user'}
<UserMessage
user={history.messages[messageId].user ?? user}
{user}
{chatId}
{history}
{messageId}

View file

@ -682,7 +682,7 @@
>
<div class={`shrink-0 ltr:mr-2 rtl:ml-2 hidden @lg:flex mt-0.5 `}>
<ProfileImage
src={`${WEBUI_API_BASE_URL}/models/model/profile/image?id=${model?.id}&lang=${$i18n.language}`}
src={`${WEBUI_API_BASE_URL}/models/model/profile/image?id=${model?.id ?? message.model}&lang=${$i18n.language}`}
className={'size-7 assistant-message-profile-image'}
/>
</div>

View file

@ -57,6 +57,17 @@
let editScrollContainer: HTMLDivElement;
let message = structuredClone(history.messages[messageId]);
$: authorId = message.user_id || message.user?.id || user?.id;
$: author =
message.user?.id === authorId
? message.user
: $_user?.id === authorId
? $_user
: user?.id === authorId
? user
: null;
$: isOwn = Boolean($_user?.id && authorId === $_user.id);
$: authorName = author?.name || $i18n.t('Unknown User');
let timerExpanded = false;
$: if (history.messages) {
const source = history.messages[messageId];
@ -136,14 +147,16 @@
id="message-{message.id}"
style="scroll-margin-top: 3rem;"
>
{#if !($settings?.chatBubble ?? true) && !(message?.meta?.internal === true && message?.meta?.type === 'subagent') && !(message?.meta?.internal === true && message?.meta?.type === 'timer')}
<div class={`shrink-0 ltr:mr-2 rtl:ml-2 hidden @lg:flex mt-0.5`}>
{#if (!($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'}"
>
<!-- LICENSE covers this Open WebUI fallback logo.
Do not alter, remove, obscure, or replace it except as LICENSE permits:
https://docs.openwebui.com/license. -->
<ProfileImage
src={user?.id
? `${WEBUI_API_BASE_URL}/users/${user.id}/profile/image`
src={authorId
? `${WEBUI_API_BASE_URL}/users/${authorId}/profile/image`
: `${WEBUI_BASE_URL}/static/favicon.png`}
className={'size-7 user-message-profile-image'}
/>
@ -156,18 +169,17 @@
? ''
: 'pl-1'}"
>
{#if !($settings?.chatBubble ?? true) && !(message?.meta?.internal === true && message?.meta?.type === 'subagent') && !(message?.meta?.internal === true && message?.meta?.type === 'timer')}
<div>
<Name>
{#if message.user}
{$i18n.t('You')}
<span class=" text-gray-500 text-[0.9375rem] font-normal">{message?.user ?? ''}</span>
{:else if $settings.showUsername || $_user?.name !== user?.name}
{user?.name ?? $i18n.t('You')}
{:else}
{$i18n.t('You')}
{/if}
</Name>
{#if (!($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 text-xs text-gray-400 dark:text-gray-500'
: ''}
>
{#if $settings?.chatBubble ?? true}
{authorName}
{:else}
<Name>{isOwn && !$settings.showUsername ? $i18n.t('You') : authorName}</Name>
{/if}
</div>
{/if}
@ -183,7 +195,9 @@
file.url?.startsWith('data') || file.url?.startsWith('http')
? file.url
: `${WEBUI_API_BASE_URL}/files/${file.url}${file?.content_type ? '/content' : ''}`}
<div class={($settings?.chatBubble ?? true) ? 'self-end' : ''}>
<div
class={($settings?.chatBubble ?? true) ? (isOwn ? 'self-end' : 'self-start') : ''}
>
{#if file.type === 'image' || isRasterImageContentType(file?.content_type)}
<Image src={fileUrl} imageClassName=" max-h-96 rounded-lg" />
{:else}
@ -381,11 +395,15 @@
<SubagentResultRow content={message.content} result={message.meta} />
{:else if message.content !== ''}
<div class="w-full">
<div class="flex {($settings?.chatBubble ?? true) ? 'justify-end pb-1' : 'w-full'}">
<div
class="flex {($settings?.chatBubble ?? true)
? `${isOwn ? 'justify-end' : 'justify-start'} pb-1`
: 'w-full'}"
>
<div
class="rounded-3xl {($settings?.chatBubble ?? true)
? `max-w-[90%] px-4 py-1.5 bg-gray-50 dark:bg-gray-850 ${
message.files ? 'rounded-tr-lg' : ''
message.files ? (isOwn ? 'rounded-se-lg' : 'rounded-ss-lg') : ''
}`
: ' w-full'}"
>
@ -416,7 +434,9 @@
{#if edit !== true && !(message?.meta?.internal === true && message?.meta?.type === 'subagent') && !(message?.meta?.internal === true && message?.meta?.type === 'timer')}
<div
class=" flex {($settings?.chatBubble ?? true)
? 'justify-end'
? isOwn
? 'justify-end'
: 'justify-start'
: 'items-center'} text-gray-600 dark:text-gray-500"
>
{#if message.timestamp}

View file

@ -185,7 +185,7 @@
class="h-screen max-h-[100dvh] w-full flex flex-col text-gray-700 dark:text-gray-100 bg-white dark:bg-gray-900"
>
<div class="flex flex-col flex-auto justify-center relative">
<div class=" flex flex-col w-full flex-auto overflow-auto h-0" id="messages-container">
<div class="@container flex flex-col w-full flex-auto overflow-auto h-0" id="messages-container">
<div
class="pt-5 px-2 w-full {($settings?.widescreenMode ?? null)
? 'max-w-full'