This commit is contained in:
Timothy Jaeryang Baek
2026-07-14 00:19:41 -04:00
parent 7088d245bb
commit 797293c749
8 changed files with 99 additions and 110 deletions
+9 -3
View File
@@ -75,6 +75,7 @@ from open_webui.socket.main import (
)
from open_webui.utils.access_control import has_connection_access, has_permission
from open_webui.utils.access_control.files import get_accessible_folder_files
from open_webui.utils.access_control.folders import has_folder_access
from open_webui.utils.chat import generate_chat_completion
from open_webui.utils.code_interpreter import execute_code_jupyter
from open_webui.utils.context_compaction import compact_messages_for_request
@@ -2353,13 +2354,14 @@ async def process_chat_payload(request, form_data, user, metadata, model):
folder_id = metadata.get('folder_id', None)
if folder_id and user:
folder = await Folders.get_folder_by_id_and_user_id(folder_id, user.id)
folder = await Folders.get_folder_by_id(folder_id)
if folder and user.role != 'admin' and not await has_folder_access(user.id, folder, 'read', db=None):
folder = None
if folder and folder.data:
if 'system_prompt' in folder.data:
form_data = await apply_system_prompt_to_body(folder.data['system_prompt'], form_data, metadata, user)
if 'files' in folder.data:
# Defensive: filter to entries the caller can still read.
allowed_files = await get_accessible_folder_files(folder.data['files'], user)
if metadata.get('params', {}).get('function_calling') == 'legacy':
form_data['files'] = [
@@ -2583,7 +2585,11 @@ async def process_chat_payload(request, form_data, user, metadata, model):
# Get folder files
folder_id = file_item.get('id', None)
if folder_id:
folder = await Folders.get_folder_by_id_and_user_id(folder_id, user.id)
folder = await Folders.get_folder_by_id(folder_id)
if folder and user.role != 'admin' and not await has_folder_access(
user.id, folder, 'read', db=None
):
folder = None
if folder and folder.data and 'files' in folder.data:
files = [f for f in files if f.get('id', None) != folder_id]
files = [*files, *await get_accessible_folder_files(folder.data['files'], user)]
@@ -1,5 +1,4 @@
<script lang="ts">
import dayjs from 'dayjs';
import { onMount, tick, getContext } from 'svelte';
import { createEventDispatcher } from 'svelte';
@@ -16,12 +15,11 @@
import Markdown from './Markdown.svelte';
import Name from './Name.svelte';
import Skeleton from './Skeleton.svelte';
import localizedFormat from 'dayjs/plugin/localizedFormat';
import ProfileImage from './ProfileImage.svelte';
import { WEBUI_BASE_URL } from '$lib/constants';
import equal from 'fast-deep-equal';
import { formatMessageTimestamp, formatMessageTimestampFull } from '$lib/utils';
const i18n = getContext('i18n');
dayjs.extend(localizedFormat);
export let chatId;
export let history;
@@ -402,14 +400,6 @@
<div class="w-full rounded-xl pl-5 pr-2 py-2 mt-2">
<Name>
{$i18n.t('Merged Response')}
{#if message.timestamp}
<span
class=" self-center invisible group-hover:visible text-gray-400 text-xs font-medium uppercase ml-0.5 -mt-0.5"
>
{dayjs(message.timestamp * 1000).format('LT')}
</span>
{/if}
</Name>
<div class="mt-1 markdown-prose w-full min-w-full">
@@ -419,6 +409,23 @@
<Markdown id={`merged`} content={message.content ?? ''} />
{/if}
</div>
{#if message.timestamp}
<div class="mt-0.5 flex justify-start text-gray-600 dark:text-gray-500">
<Tooltip
className="flex self-center"
content={formatMessageTimestampFull(message.timestamp * 1000)}
placement="bottom"
>
<time
datetime={new Date(message.timestamp * 1000).toISOString()}
class="invisible group-hover:visible ml-1 text-[0.6875rem] tabular-nums text-gray-400 dark:text-gray-600 select-none"
>
{formatMessageTimestamp(message.timestamp * 1000)}
</time>
</Tooltip>
</div>
{/if}
</div>
{/if}
</div>
@@ -1,6 +1,5 @@
<script lang="ts">
import { toast } from 'svelte-sonner';
import dayjs from 'dayjs';
import { createEventDispatcher, onDestroy } from 'svelte';
import { onMount, tick, getContext } from 'svelte';
@@ -32,7 +31,8 @@
getMessageContentParts,
sanitizeResponseContent,
createMessagesList,
formatDate,
formatMessageTimestamp,
formatMessageTimestampFull,
removeDetails,
removeAllDetails
} from '$lib/utils';
@@ -668,24 +668,6 @@
{model?.name ?? message.model}
</span>
</Tooltip>
{#if message.timestamp}
<div
class="self-center text-xs font-medium first-letter:capitalize ml-0.5 translate-y-[1px] {($settings?.highContrastMode ??
false)
? 'dark:text-gray-100 text-gray-900'
: 'invisible group-hover:visible transition text-gray-400'}"
>
<Tooltip content={dayjs(message.timestamp * 1000).format('LLLL')}>
<span class="line-clamp-1"
>{$i18n.t(formatDate(message.timestamp * 1000), {
LOCALIZED_TIME: dayjs(message.timestamp * 1000).format('LT'),
LOCALIZED_DATE: dayjs(message.timestamp * 1000).format('L')
})}</span
>
</Tooltip>
</div>
{/if}
</Name>
<div>
@@ -1496,6 +1478,21 @@
</button>
</Tooltip>
{/each}
{#if message.timestamp}
<Tooltip
className="flex self-center"
content={formatMessageTimestampFull(message.timestamp * 1000)}
placement="bottom"
>
<time
datetime={new Date(message.timestamp * 1000).toISOString()}
class="invisible group-hover:visible ml-1 text-[0.6875rem] tabular-nums text-gray-400 dark:text-gray-600 select-none"
>
{formatMessageTimestamp(message.timestamp * 1000)}
</time>
</Tooltip>
{/if}
{/if}
{/if}
{/if}
@@ -1,11 +1,14 @@
<script lang="ts">
import dayjs from 'dayjs';
import { toast } from 'svelte-sonner';
import { tick, getContext, onMount } from 'svelte';
import { models, settings } from '$lib/stores';
import { user as _user } from '$lib/stores';
import { copyToClipboard as _copyToClipboard, formatDate } from '$lib/utils';
import {
copyToClipboard as _copyToClipboard,
formatMessageTimestamp,
formatMessageTimestampFull
} from '$lib/utils';
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
import equal from 'fast-deep-equal';
@@ -18,10 +21,7 @@
import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
import SubagentResultRow from './SubagentResultRow.svelte';
import localizedFormat from 'dayjs/plugin/localizedFormat';
const i18n = getContext('i18n');
dayjs.extend(localizedFormat);
type SubagentResult = {
async_subagent_result: true;
delegation_id?: string;
@@ -165,48 +165,8 @@
{:else}
{$i18n.t('You')}
{/if}
{#if message.timestamp}
<div
class="self-center text-xs font-medium first-letter:capitalize ml-0.5 translate-y-[1px] {($settings?.highContrastMode ??
false)
? 'dark:text-gray-100 text-gray-900'
: 'invisible group-hover:visible transition'}"
>
<Tooltip content={dayjs(message.timestamp * 1000).format('LLLL')}>
<!-- $i18n.t('Today at {{LOCALIZED_TIME}}') -->
<!-- $i18n.t('Yesterday at {{LOCALIZED_TIME}}') -->
<!-- $i18n.t('{{LOCALIZED_DATE}} at {{LOCALIZED_TIME}}') -->
<span class="line-clamp-1"
>{$i18n.t(formatDate(message.timestamp * 1000), {
LOCALIZED_TIME: dayjs(message.timestamp * 1000).format('LT'),
LOCALIZED_DATE: dayjs(message.timestamp * 1000).format('L')
})}</span
>
</Tooltip>
</div>
{/if}
</Name>
</div>
{:else if message.timestamp && !subagentResult}
<div class="flex justify-end pr-2 text-xs">
<div
class="text-[0.65rem] font-medium first-letter:capitalize mb-0.5 {($settings?.highContrastMode ??
false)
? 'dark:text-gray-100 text-gray-900'
: 'invisible group-hover:visible transition text-gray-400'}"
>
<Tooltip content={dayjs(message.timestamp * 1000).format('LLLL')}>
<span class="line-clamp-1"
>{$i18n.t(formatDate(message.timestamp * 1000), {
LOCALIZED_TIME: dayjs(message.timestamp * 1000).format('LT'),
LOCALIZED_DATE: dayjs(message.timestamp * 1000).format('L')
})}</span
>
</Tooltip>
</div>
</div>
{/if}
<div class="chat-{message.role} w-full min-w-full markdown-prose">
@@ -412,6 +372,21 @@
? 'justify-end'
: ''} text-gray-600 dark:text-gray-500"
>
{#if message.timestamp}
<Tooltip
className="flex self-center"
content={formatMessageTimestampFull(message.timestamp * 1000)}
placement="bottom"
>
<time
datetime={new Date(message.timestamp * 1000).toISOString()}
class="invisible group-hover:visible mr-1 text-[0.6875rem] tabular-nums text-gray-400 dark:text-gray-600 select-none"
>
{formatMessageTimestamp(message.timestamp * 1000)}
</time>
</Tooltip>
{/if}
{#if !($settings?.chatBubble ?? true)}
{#if siblings.length > 1}
<div class="flex self-center" dir="ltr">
+27 -27
View File
@@ -526,7 +526,7 @@
isResizing = true;
startClientX = e.clientX;
startWidth = $sidebarWidth ?? 250;
startWidth = $sidebarWidth ?? 245;
document.body.style.userSelect = 'none';
};
@@ -847,11 +847,11 @@
<div class=" self-center flex items-center justify-center size-9">
<img
src="{WEBUI_BASE_URL}/static/favicon.png"
class="sidebar-new-chat-icon size-6 rounded-full group-hover:hidden"
class="sidebar-new-chat-icon size-5 rounded-full group-hover:hidden"
alt=""
/>
<Sidebar className="size-5 hidden group-hover:flex" />
<Sidebar className="size-4.5 hidden group-hover:flex" />
</div>
</button>
</Tooltip>
@@ -874,7 +874,7 @@
aria-label={$i18n.t('New Chat')}
>
<div class=" self-center flex items-center justify-center size-9">
<PencilSquare className="size-4.5" />
<PencilSquare className="size-4" />
</div>
</a>
</Tooltip>
@@ -894,7 +894,7 @@
aria-label={$i18n.t('Search')}
>
<div class=" self-center flex items-center justify-center size-9">
<Search className="size-4.5" />
<Search className="size-4" />
</div>
</button>
</Tooltip>
@@ -919,7 +919,7 @@
>
<div class=" self-center flex items-center justify-center size-9">
{#if itemId === 'notes'}
<Note className="size-4.5" />
<Note className="size-4" />
{:else if itemId === 'workspace'}
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -927,7 +927,7 @@
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-4.5"
class="size-4"
>
<path
stroke-linecap="round"
@@ -942,7 +942,7 @@
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-4.5"
class="size-4"
>
<path
stroke-linecap="round"
@@ -957,7 +957,7 @@
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-4.5"
class="size-4"
>
<path
stroke-linecap="round"
@@ -966,7 +966,7 @@
/>
</svg>
{:else if itemId === 'playground'}
<Code className="size-4.5" />
<Code className="size-4" />
{/if}
</div>
</a>
@@ -1047,7 +1047,7 @@
: 'invisible'}"
>
<div
class="sidebar px-1.5 pt-2 pb-1.5 flex justify-between space-x-1 text-gray-600 dark:text-gray-400 sticky top-0 z-10 -mb-3"
class="sidebar px-1.5 pt-1.5 pb-1 flex justify-between space-x-1 text-gray-600 dark:text-gray-400 sticky top-0 z-10 -mb-2"
>
<a
class="flex items-center rounded-xl size-8.5 h-full justify-center hover:bg-gray-100/50 dark:hover:bg-gray-850/50 transition no-drag-region"
@@ -1058,7 +1058,7 @@
<img
crossorigin="anonymous"
src="{WEBUI_BASE_URL}/static/favicon.png"
class="sidebar-new-chat-icon size-6 rounded-full"
class="sidebar-new-chat-icon size-5 rounded-full"
alt=""
/>
</a>
@@ -1085,7 +1085,7 @@
aria-label={$showSidebar ? $i18n.t('Close Sidebar') : $i18n.t('Open Sidebar')}
>
<div class=" self-center p-1.5">
<Sidebar />
<Sidebar className="size-4.5" />
</div>
</button>
</Tooltip>
@@ -1098,7 +1098,7 @@
</div>
<div
class="relative flex flex-col flex-1 overflow-y-auto scrollbar-hidden pt-3 pb-3"
class="relative flex flex-col flex-1 overflow-y-auto scrollbar-hidden pt-2.5 pb-2.5"
on:scroll={(e) => {
if (e.target.scrollTop === 0) {
scrollTop = 0;
@@ -1107,18 +1107,18 @@
}
}}
>
<div class="pb-1.5">
<div class="pb-1">
<div class="px-1.5 flex justify-center text-gray-800 dark:text-gray-200">
<a
id="sidebar-new-chat-button"
class="group grow flex items-center space-x-2.5 rounded-xl px-2.5 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition outline-none"
class="group grow flex items-center space-x-2.5 rounded-xl px-2.5 py-1.5 hover:bg-gray-100 dark:hover:bg-gray-900 transition outline-none"
href="/"
draggable="false"
on:click={newChatHandler}
aria-label={$i18n.t('New Chat')}
>
<div class="self-center">
<PencilSquare className=" size-4.5" strokeWidth="2" />
<PencilSquare className=" size-4" strokeWidth="2" />
</div>
<div class="flex flex-1 self-center translate-y-[0.5px]">
@@ -1132,7 +1132,7 @@
<div class="px-1.5 flex justify-center text-gray-800 dark:text-gray-200">
<button
id="sidebar-search-button"
class="group grow flex items-center space-x-2.5 rounded-xl px-2.5 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition outline-none"
class="group grow flex items-center space-x-2.5 rounded-xl px-2.5 py-1.5 hover:bg-gray-100 dark:hover:bg-gray-900 transition outline-none"
on:click={() => {
showSearch.set(true);
}}
@@ -1140,7 +1140,7 @@
aria-label={$i18n.t('Search')}
>
<div class="self-center">
<Search strokeWidth="2" className="size-4.5" />
<Search strokeWidth="2" className="size-4" />
</div>
<div class="flex flex-1 self-center translate-y-[0.5px]">
@@ -1160,7 +1160,7 @@
>
<a
id="sidebar-{itemId}-button"
class="grow flex items-center space-x-2.5 rounded-xl px-2.5 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition"
class="grow flex items-center space-x-2.5 rounded-xl px-2.5 py-1.5 hover:bg-gray-100 dark:hover:bg-gray-900 transition"
href={meta.href}
on:click={itemClickHandler}
draggable="false"
@@ -1168,7 +1168,7 @@
>
<div class="self-center">
{#if itemId === 'notes'}
<Note className="size-4.5" strokeWidth="2" />
<Note className="size-4" strokeWidth="2" />
{:else if itemId === 'workspace'}
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -1176,7 +1176,7 @@
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="size-4.5"
class="size-4"
>
<path
stroke-linecap="round"
@@ -1191,7 +1191,7 @@
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="size-4.5"
class="size-4"
>
<path
stroke-linecap="round"
@@ -1206,7 +1206,7 @@
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="size-4.5"
class="size-4"
>
<path
stroke-linecap="round"
@@ -1215,7 +1215,7 @@
/>
</svg>
{:else if itemId === 'playground'}
<Code className="size-4.5" strokeWidth="2" />
<Code className="size-4" strokeWidth="2" />
{/if}
</div>
@@ -1524,7 +1524,7 @@
class="w-full pl-2.5 text-xs text-gray-500 dark:text-gray-500 font-medium {idx ===
0
? ''
: 'pt-5'} pb-1.5"
: 'pt-4'} pb-1"
>
{$i18n.t(chat.time_range)}
<!-- localisation keys for time_range to be recognized from the i18next parser (so they don't get automatically removed):
@@ -1602,7 +1602,7 @@
</Folder>
</div>
<div class="px-1.5 pt-1.5 pb-2 sticky bottom-0 z-10 -mt-3 sidebar">
<div class="px-1.5 pt-1 pb-1.5 sticky bottom-0 z-10 -mt-2 sidebar">
<div
class=" sidebar-bg-gradient-to-t bg-linear-to-t from-gray-50 dark:from-gray-950 to-transparent from-50% pointer-events-none absolute inset-0 -z-10 -mt-6"
></div>
@@ -122,13 +122,13 @@
<div slot="content">
<div
class="{className} user-menu rounded-xl p-0.5 border border-gray-100 dark:border-gray-800 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg text-xs"
class="{className} user-menu rounded-xl p-0.5 border border-gray-100 dark:border-gray-800 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg text-[13px]"
>
{#if profile}
{#if $user?.status_emoji || $user?.status_message}
<div class="user-menu-status">
<button
class="w-full gap-2 hover:bg-gray-50/60 dark:hover:bg-gray-800/60 transition cursor-pointer select-none text-xs flex items-center"
class="w-full gap-2 hover:bg-gray-50/60 dark:hover:bg-gray-800/60 transition cursor-pointer select-none text-[13px] flex items-center"
type="button"
on:click={() => {
show = false;
@@ -180,7 +180,7 @@
{:else}
<div class="user-menu-status">
<button
class="w-full gap-1 hover:bg-gray-50/60 dark:hover:bg-gray-800/60 transition cursor-pointer select-none text-xs flex items-center justify-center"
class="w-full gap-1 hover:bg-gray-50/60 dark:hover:bg-gray-800/60 transition cursor-pointer select-none text-[13px] flex items-center justify-center"
type="button"
on:click={() => {
show = false;
+1 -1
View File
@@ -88,7 +88,7 @@ export const chatRequestQueues: Writable<
Record<string, { id: string; prompt: string; files: any[] }[]>
> = writable({});
export const sidebarWidth = writable(250);
export const sidebarWidth = writable(245);
export const showSidebar = writable(false);
export const showSearch = writable(false);
+4
View File
@@ -493,6 +493,10 @@ export const formatDate = (inputDate) => {
}
};
export const formatMessageTimestamp = (inputDate) => dayjs(inputDate).format('MMM D, LT');
export const formatMessageTimestampFull = (inputDate) => dayjs(inputDate).format('LLLL');
export const copyToClipboard = async (text, html = null, formatted = false) => {
if (formatted) {
let styledHtml = '';