diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index 0f8bff5c34..48d4546b0a 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -14,7 +14,7 @@ from urllib.parse import urlparse import redis import requests from authlib.integrations.starlette_client import OAuth -from pydantic import BaseModel +from pydantic import BaseModel, StrictBool, TypeAdapter from open_webui.env import ( USE_SLIM, @@ -1745,6 +1745,12 @@ except Exception as e: DEFAULT_INTERFACE_SETTINGS = default_interface_settings if isinstance(default_interface_settings, dict) else {} +try: + UI_VISIBILITY = TypeAdapter(dict[str, StrictBool]).validate_json(os.getenv('UI_VISIBILITY', '{}')) +except Exception as e: + log.exception(f'Error loading UI_VISIBILITY: {e}') + UI_VISIBILITY = {} + DEFAULT_USER_ROLE = os.getenv('DEFAULT_USER_ROLE', 'pending') DEFAULT_GROUP_ID = os.getenv('DEFAULT_GROUP_ID', '') @@ -3191,6 +3197,7 @@ DEFAULT_CONFIG = { 'ui.default_models': DEFAULT_MODELS, 'ui.default_pinned_models': DEFAULT_PINNED_MODELS, 'ui.default_interface_settings': DEFAULT_INTERFACE_SETTINGS, + 'ui.visibility': UI_VISIBILITY, 'ui.i18n': {}, 'ui.prompt_suggestions': DEFAULT_PROMPT_SUGGESTIONS, 'ui.prompt_suggestions_i18n': DEFAULT_PROMPT_SUGGESTIONS_I18N, diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index 2c35a44503..864d8d189c 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -2366,6 +2366,7 @@ async def get_app_config(request: Request): 'ui.default_models', 'ui.default_pinned_models', 'ui.default_interface_settings', + 'ui.visibility', 'ui.i18n', 'ui.prompt_suggestions', 'ui.prompt_suggestions_i18n', @@ -2532,6 +2533,7 @@ async def get_app_config(request: Request): }, 'ui': { 'default_interface_settings': config.get('ui.default_interface_settings'), + 'visibility': config.get('ui.visibility'), 'pending_user_overlay_title': config.get('ui.pending_user_overlay_title'), 'pending_user_overlay_content': config.get('ui.pending_user_overlay_content'), 'response_watermark': config.get('ui.watermark'), diff --git a/backend/open_webui/routers/configs.py b/backend/open_webui/routers/configs.py index 9aca44f967..1a8585dd6e 100644 --- a/backend/open_webui/routers/configs.py +++ b/backend/open_webui/routers/configs.py @@ -31,7 +31,7 @@ from open_webui.utils.tools import ( set_terminal_servers, set_tool_servers, ) -from pydantic import BaseModel, ConfigDict +from pydantic import BaseModel, ConfigDict, StrictBool router = APIRouter() @@ -127,6 +127,24 @@ async def get_config_namespace(namespace: str, user=Depends(get_admin_user)): return await Config.get_namespace(namespace) +class InterfaceVisibilityForm(BaseModel): + visibility: dict[str, StrictBool] + + +@router.post('/interface/visibility', response_model=InterfaceVisibilityForm) +async def set_interface_visibility(request: Request, form_data: InterfaceVisibilityForm, user=Depends(get_admin_user)): + await Config.upsert({'ui.visibility': form_data.visibility}) + await publish_event( + request, + EVENTS.CONFIG_UPDATED, + actor=user, + subject_id='interface', + subject_type='config', + data={'keys': ['ui.visibility']}, + ) + return form_data + + ############################ # Connections Config ############################ diff --git a/src/lib/apis/configs/index.ts b/src/lib/apis/configs/index.ts index 236664f115..b50d547fde 100644 --- a/src/lib/apis/configs/index.ts +++ b/src/lib/apis/configs/index.ts @@ -1,5 +1,22 @@ import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants'; import type { Banner } from '$lib/types'; +import type { Visibility } from '$lib/utils/ui-visibility'; + +export const setInterfaceVisibility = async ( + token: string, + visibility: Visibility +): Promise => { + const res = await fetch(`${WEBUI_API_BASE_URL}/configs/interface/visibility`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ visibility }) + }); + if (!res.ok) throw (await res.json()).detail; + return (await res.json()).visibility; +}; export const importConfig = async (token: string, config: object) => { let error = null; diff --git a/src/lib/components/admin/Analytics/UsageChart.svelte b/src/lib/components/admin/Analytics/UsageChart.svelte index 903e607b84..858782de5a 100644 --- a/src/lib/components/admin/Analytics/UsageChart.svelte +++ b/src/lib/components/admin/Analytics/UsageChart.svelte @@ -89,6 +89,10 @@ let keyboardIndex = 0; let plot: HTMLDivElement; let chartWidth = 0; + let legendExpanded = false; + $: legendColumns = Math.max(1, Math.floor((chartWidth + 16) / 256)); + $: legendLimit = legendColumns * 2 - 1; + $: legendIds = legendExpanded ? ids : ids.slice(0, legendLimit); let tooltip: Instance | undefined; let tooltipContent: HTMLDivElement; let tooltipId = ''; @@ -145,22 +149,27 @@ : [0, series.length - 1] ) ].filter((i) => i >= 0 && i < series.length); - $: lanes = visibleIds.map((id) => { + $: lanes = (view === 'activity' ? visibleIds : []).map((id) => { const values = series.map((point) => point.models[id] ?? 0); return { id, peak: Math.max(0, ...values), values }; }); - $: stacks = series.map((point) => visibleIds.filter((id) => (point.models[id] ?? 0) > 0)); + $: stacks = + view === 'volume' + ? series.map((point) => visibleIds.filter((id) => (point.models[id] ?? 0) > 0)) + : []; + $: activityPositions = series.map((point) => (point.time - start) / span); const name = (id: string) => modelNames[id] || id; - const number = (value: number) => - new Intl.NumberFormat($i18n.language, { notation: 'compact', maximumFractionDigits: 1 }).format( - value - ); + $: numberFormat = new Intl.NumberFormat($i18n.language, { + notation: 'compact', + maximumFractionDigits: 1 + }); + const number = (value: number) => numberFormat.format(value); const dateLabel = (date: string) => dayjs(date) .locale($i18n.language) .format(granularity === 'daily' ? 'MMM D' : 'MMM D, HH:mm'); const position = (time: number) => ((time - start + step / 2) / span) * 100; - function navigate(event: KeyboardEvent, index: number) { + async function navigate(event: KeyboardEvent, index: number) { const buttons = plot.querySelectorAll('button[data-point]'); const destinations: Record = { ArrowRight: index + 1, @@ -171,12 +180,74 @@ if (view === 'activity') { destinations.ArrowDown = index + series.length; destinations.ArrowUp = index - series.length; + destinations.End = lanes.length * series.length - 1; } const next = destinations[event.key]; if (next === undefined) return; event.preventDefault(); - keyboardIndex = Math.max(0, Math.min(buttons.length - 1, next)); - buttons[keyboardIndex]?.focus(); + keyboardIndex = Math.max( + 0, + Math.min((view === 'activity' ? lanes.length * series.length : buttons.length) - 1, next) + ); + if (view === 'activity') { + const row = Math.floor(keyboardIndex / series.length); + await tick(); + buttons[row]?.focus(); + showTooltip({ currentTarget: buttons[row] }, keyboardIndex % series.length, lanes[row].id); + } else buttons[keyboardIndex]?.focus(); + } + + // One canvas and focus target per lane avoids a DOM element for every model/date pair. + function drawActivity( + canvas: HTMLCanvasElement, + options: { + lane: { id: string; peak: number; values: number[] }; + positions: number[]; + cellWidth: number; + width: number; + } + ) { + function draw() { + const { lane, positions, cellWidth } = options; + const width = canvas.clientWidth; + const height = canvas.clientHeight; + const scale = window.devicePixelRatio || 1; + canvas.width = Math.round(width * scale); + canvas.height = Math.round(height * scale); + const context = canvas.getContext('2d'); + if (!context) return; + context.scale(scale, scale); + context.fillStyle = modelColor(lane.id); + lane.values.forEach((value, index) => { + if (value <= 0) return; + context.globalAlpha = 0.25 + (0.75 * value) / lane.peak; + context.fillRect(positions[index] * width, 0, cellWidth * width, height); + }); + } + draw(); + return { + update(next: typeof options) { + options = next; + draw(); + } + }; + } + + function hoverActivity(event: MouseEvent, id: string) { + const rect = (event.currentTarget as HTMLElement).getBoundingClientRect(); + const time = start + Math.max(0, Math.min(1, (event.clientX - rect.left) / rect.width)) * span; + let low = 0; + let high = series.length - 1; + while (low < high) { + const mid = Math.ceil((low + high) / 2); + if (series[mid].time <= time) low = mid; + else high = mid - 1; + } + if (time >= series[low].time + step && time < start + span) { + hideTooltip(); + return; + } + if (active !== low || activeModel !== id) showTooltip(event, low, id); } onMount(() => { @@ -188,7 +259,7 @@ placement: 'right-start', offset: [0, 12], popperOptions: { - modifiers: [{ name: 'flip', options: { fallbackPlacements: ['left-start'] } }] + modifiers: [{ name: 'flip', options: { fallbackPlacements: ['left-start', 'top'] } }] }, duration: 0, interactive: true, @@ -203,7 +274,9 @@ const rect = (anchor ?? plot).getBoundingClientRect(); const height = tooltip?.popper.offsetHeight || tooltipContent.offsetHeight; return new DOMRect( - rect.x + rect.width / 2, + rect.x + + rect.width * + (view === 'activity' && active !== null ? position(series[active].time) / 100 : 0.5), plot.getBoundingClientRect().top - height / 2, 0, 0 @@ -214,7 +287,7 @@ return () => tooltip?.destroy(); }); async function showTooltip( - event: MouseEvent | FocusEvent, + event: { currentTarget: EventTarget | null }, index: number, modelId: string | null = null ) { @@ -410,6 +483,8 @@ {:else}
{#each lanes as lane, row (lane.id)} + {@const index = + Math.floor(keyboardIndex / series.length) === row ? keyboardIndex % series.length : 0}
-
- {#each series as point, index (point.date)} - {@const cellIndex = row * series.length + index} - - {/each} -
+
{/each}
@@ -489,16 +557,19 @@ > {/each} -
+
- {#each ids as id (id)} + {#each legendIds as id (id)}
+ {#if ids.length > legendLimit} + + {/if}

{view === 'activity' ? $i18n.t('Intensity relative to each model’s peak. Blank means no recorded usage.') diff --git a/src/lib/components/admin/Settings/Interface.svelte b/src/lib/components/admin/Settings/Interface.svelte index 58f5db4516..179f3480b6 100644 --- a/src/lib/components/admin/Settings/Interface.svelte +++ b/src/lib/components/admin/Settings/Interface.svelte @@ -16,6 +16,9 @@ import AdminSettingRow from './AdminSettingRow.svelte'; import AdminSettingSection from './AdminSettingSection.svelte'; import { config as appConfig } from '$lib/stores'; + import { setInterfaceVisibility } from '$lib/apis/configs'; + import type { Visibility } from '$lib/utils/ui-visibility'; + import VisibilitySettings from './Interface/Visibility.svelte'; export let saveHandler: () => void; @@ -58,6 +61,7 @@ }; let toolSearchAlwaysLoaded = ''; let showTaskParameters = false; + let visibility: Visibility = { ...($appConfig?.ui?.visibility ?? {}) }; const configuredParams = (params: Record = {}) => Object.fromEntries( @@ -73,7 +77,7 @@ }; try { - [taskConfig, chatConfig] = await Promise.all([ + [taskConfig, chatConfig, visibility] = await Promise.all([ updateTaskConfig(localStorage.token, taskConfigPayload), updateChatConfig(localStorage.token, { ...chatConfig, @@ -81,7 +85,8 @@ .split(',') .map((item) => item.trim()) .filter((item) => item !== '') - }) + }), + setInterfaceVisibility(localStorage.token, visibility) ]); } catch (error) { toast.error( @@ -94,6 +99,7 @@ current ? { ...current, + ui: { ...current.ui, visibility }, features: { ...current.features, enable_context_compaction: chatConfig.ENABLE_CONTEXT_COMPACTION, @@ -193,7 +199,8 @@

- + +
diff --git a/src/lib/components/admin/Settings/Interface/Visibility.svelte b/src/lib/components/admin/Settings/Interface/Visibility.svelte new file mode 100644 index 0000000000..125a042e2f --- /dev/null +++ b/src/lib/components/admin/Settings/Interface/Visibility.svelte @@ -0,0 +1,62 @@ + + + + + + + + diff --git a/src/lib/components/chat/MessageInput/Commands/SlashCommands.svelte b/src/lib/components/chat/MessageInput/Commands/SlashCommands.svelte index 93c044500a..92b6d92aef 100644 --- a/src/lib/components/chat/MessageInput/Commands/SlashCommands.svelte +++ b/src/lib/components/chat/MessageInput/Commands/SlashCommands.svelte @@ -14,7 +14,8 @@ selectedTerminalId, settings, terminalServers, - terminalSkills + terminalSkills, + visibleControls } from '$lib/stores'; import Tooltip from '$lib/components/common/Tooltip.svelte'; import ChatBubbleDotted from '$lib/components/icons/ChatBubbleDotted.svelte'; @@ -60,19 +61,19 @@ ...(canCompact && 'compact'.startsWith(query.toLowerCase()) ? [{ type: 'command', data: { id: 'compact' } }] : []), - ...(canFork && 'fork'.startsWith(query.toLowerCase()) + ...(canFork && $visibleControls['chat.fork'] && 'fork'.startsWith(query.toLowerCase()) ? [{ type: 'command', data: { id: 'fork' } }] : []), - ...(canStatus && 'status'.startsWith(query.toLowerCase()) + ...(canStatus && $visibleControls['chat.status'] && 'status'.startsWith(query.toLowerCase()) ? [{ type: 'command', data: { id: 'status' } }] : []), - ...('model'.startsWith(query.toLowerCase()) + ...($visibleControls['chat.model_command'] && 'model'.startsWith(query.toLowerCase()) ? [{ type: 'command', data: { id: 'model' } }] : []), - ...('settings'.startsWith(query.toLowerCase()) + ...($visibleControls['chat.settings_command'] && 'settings'.startsWith(query.toLowerCase()) ? [{ type: 'command', data: { id: 'settings' } }] : []), - ...(canCompare && 'compare'.startsWith(query.toLowerCase()) + ...(canCompare && $visibleControls['chat.compare'] && 'compare'.startsWith(query.toLowerCase()) ? [{ type: 'command', data: { id: 'compare' } }] : []), ...(hasChatContent && diff --git a/src/lib/components/chat/MessageInput/InputMenu.svelte b/src/lib/components/chat/MessageInput/InputMenu.svelte index 795c8b3249..5e255be17c 100644 --- a/src/lib/components/chat/MessageInput/InputMenu.svelte +++ b/src/lib/components/chat/MessageInput/InputMenu.svelte @@ -2,7 +2,7 @@ import { getContext, onMount, tick } from 'svelte'; import { fly } from 'svelte/transition'; - import { config, user, tools as _tools, mobile, knowledge } from '$lib/stores'; + import { config, user, tools as _tools, mobile, knowledge, visibleControls } from '$lib/stores'; import { getKnowledgeBases } from '$lib/apis/knowledge'; import { createPicker } from '$lib/utils/google-drive-picker'; @@ -214,7 +214,7 @@ - {#if visionCapableModels.length > 0} + {#if visionCapableModels.length > 0 && $visibleControls['chat.capture']} {/if} - - - + {#if $visibleControls['chat.compare']} - + {/if} - - + + {/if} -
-
- {$i18n.t('Attach Files')} + {#if $visibleControls['chat.attach_files']} + + + + {/if} -
- -
-
- - - - {#if ($config?.features?.enable_notes ?? false) && ($user?.role === 'admin' || ($user?.permissions?.features?.notes ?? true))} + {#if ($config?.features?.enable_notes ?? false) && ($user?.role === 'admin' || ($user?.permissions?.features?.notes ?? true)) && $visibleControls['chat.attach_notes']} {/if} - - + + {/if} -
- -
-
- -
- - - - + + + {/if} {#if fileUploadEnabled} {#if $config?.features?.enable_google_drive_integration} diff --git a/src/lib/components/chat/Messages/CodeBlock.svelte b/src/lib/components/chat/Messages/CodeBlock.svelte index 752b58b901..da35821b7f 100644 --- a/src/lib/components/chat/Messages/CodeBlock.svelte +++ b/src/lib/components/chat/Messages/CodeBlock.svelte @@ -4,7 +4,7 @@ import { getContext, onMount, tick, onDestroy } from 'svelte'; import type { Writable } from 'svelte/store'; import type { i18n as i18nType } from 'i18next'; - import { config, pyodideWorker as pyodideWorkerStore } from '$lib/stores'; + import { config, pyodideWorker as pyodideWorkerStore, visibleControls } from '$lib/stores'; import { createPyodideWorker } from '$lib/pyodide/createPyodideWorker'; import { executeCode } from '$lib/apis/utils'; @@ -544,7 +544,7 @@ on:click={copyCode}>{copied ? $i18n.t('Copied') : $i18n.t('Copy')} - {#if preview && ['html', 'svg'].includes(lang)} + {#if preview && ['html', 'svg'].includes(lang) && $visibleControls['chat.code_preview']} + +
+ {#if $pinnedModels.includes(model?.id)} + {$i18n.t('Hide from Sidebar')} + {:else} + {$i18n.t('Keep in Sidebar')} + {/if} +
+ + {/if} + {#if $visibleControls['chat.archive']} + + {/if} {#if $user?.role === 'admin' || ($user?.permissions?.chat?.export ?? true)} - + {#if $visibleControls['chat.archive']} + + + + {/if} {#if $user?.role === 'admin' || ($user?.permissions?.chat?.delete ?? true)} - + + + {/if} {#if canManageChats({ user: $user, config: null }, 'delete')}
-
-
- + {#if $visibleControls['settings.user_location']} +
+
+ -
- { - toggleUserLocation(); - }} - /> +
+ { + toggleUserLocation(); + }} + /> +
+

+ {$i18n.t('settings.personal.interface.allowUserLocation.description')} +

-

- {$i18n.t('settings.personal.interface.allowUserLocation.description')} -

-
+ {/if}
@@ -916,28 +918,34 @@

-
-
- + {#if $visibleControls['settings.chat_bubble']} +
+
+ -
- { - saveSettings({ chatBubble }); - }} - /> +
+ { + saveSettings({ chatBubble }); + }} + /> +
+

+ {$i18n.t('settings.personal.interface.chatBubbleUi.description')} +

-

- {$i18n.t('settings.personal.interface.chatBubbleUi.description')} -

-
+ {/if} {#if !chatBubble}
@@ -968,32 +976,34 @@
{/if} -
-
- + {#if $visibleControls['settings.widescreen']} +
+
+ -
- { - saveSettings({ widescreenMode }); - }} - /> +
+ { + saveSettings({ widescreenMode }); + }} + /> +
+

+ {$i18n.t('settings.personal.interface.widescreenMode.description')} +

-

- {$i18n.t('settings.personal.interface.widescreenMode.description')} -

-
+ {/if} {#if canUseTemporaryChats({ user: $user, config: $config })}
@@ -1615,47 +1625,49 @@

-
-
- + {#if $visibleControls['settings.floating_action_buttons']} +
+
+ -
- {#if showFloatingActionButtons} - + {/if} + + { + saveSettings({ showFloatingActionButtons }); }} - > - {$i18n.t('Manage')} - - {/if} - - { - saveSettings({ showFloatingActionButtons }); - }} - /> + /> +
+

+ {$i18n.t('Show the floating quick-action toolbar in chat.')} +

-

- {$i18n.t('Show the floating quick-action toolbar in chat.')} -

-
+ {/if} {#if canUseWebSearch({ user: $user, config: $config })}
@@ -1723,28 +1735,30 @@

-
-
- + {#if $visibleControls['settings.rich_text_input']} +
+
+ -
- { - saveSettings({ richTextInput }); - }} - /> +
+ { + saveSettings({ richTextInput }); + }} + /> +
+

+ {$i18n.t('settings.personal.interface.richTextInputForChat.description')} +

-

- {$i18n.t('settings.personal.interface.richTextInputForChat.description')} -

-
+ {/if} {#if $config?.features?.enable_autocomplete_generation}
@@ -1831,167 +1845,179 @@
{/if} -
-
- + {#if $visibleControls['settings.large_text_as_file']} +
+
+ -
- { - saveSettings({ largeTextAsFile }); - }} - /> +
+ { + saveSettings({ largeTextAsFile }); + }} + /> +
+

+ {$i18n.t('settings.personal.interface.pasteLargeTextAsFile.description')} +

-

- {$i18n.t('settings.personal.interface.pasteLargeTextAsFile.description')} -

-
+ {/if}
{$i18n.t('settings.personal.interface.sections.artifacts.title')}
-
-
- + {#if $visibleControls['settings.detect_artifacts']} +
+
+ -
- { - saveSettings({ detectArtifacts }); - }} - /> +
+ { + saveSettings({ detectArtifacts }); + }} + /> +
+

+ {$i18n.t('settings.personal.interface.detectArtifactsAutomatically.description')} +

-

- {$i18n.t('settings.personal.interface.detectArtifactsAutomatically.description')} -

-
+ {/if} -
-
- + {#if $visibleControls['settings.iframe_sandbox_allow_scripts']} +
+
+ -
- { - saveSettings({ iframeSandboxAllowScripts }); - }} - /> +
+ { + saveSettings({ iframeSandboxAllowScripts }); + }} + /> +
+

+ {$i18n.t('settings.personal.interface.iframeSandboxAllowScripts.description')} +

-

- {$i18n.t('settings.personal.interface.iframeSandboxAllowScripts.description')} -

-
+ {/if} -
-
- + {#if $visibleControls['settings.iframe_sandbox_allow_same_origin']} +
+
+ -
- { - saveSettings({ iframeSandboxAllowSameOrigin }); - }} - /> +
+ { + saveSettings({ iframeSandboxAllowSameOrigin }); + }} + /> +
+

+ {$i18n.t('settings.personal.interface.iframeSandboxAllowSameOrigin.description')} +

-

- {$i18n.t('settings.personal.interface.iframeSandboxAllowSameOrigin.description')} -

-
+ {/if} -
-
- + {#if $visibleControls['settings.iframe_sandbox_allow_forms']} +
+
+ -
- { - saveSettings({ iframeSandboxAllowForms }); - }} - /> +
+ { + saveSettings({ iframeSandboxAllowForms }); + }} + /> +
+

+ {$i18n.t('settings.personal.interface.iframeSandboxAllowForms.description')} +

-

- {$i18n.t('settings.personal.interface.iframeSandboxAllowForms.description')} -

-
+ {/if} -
-
- + {#if $visibleControls['settings.iframe_sandbox_allow_downloads']} +
+
+ -
- { - saveSettings({ iframeSandboxAllowDownloads }); - }} - /> +
+ { + saveSettings({ iframeSandboxAllowDownloads }); + }} + /> +
+

+ {$i18n.t('settings.personal.interface.iframeSandboxAllowDownloads.description')} +

-

- {$i18n.t('settings.personal.interface.iframeSandboxAllowDownloads.description')} -

-
+ {/if} {#if canUseCalls({ user: $user, config: $config })}
diff --git a/src/lib/components/layout/Navbar/Menu.svelte b/src/lib/components/layout/Navbar/Menu.svelte index 295307a7aa..76cb6bac11 100644 --- a/src/lib/components/layout/Navbar/Menu.svelte +++ b/src/lib/components/layout/Navbar/Menu.svelte @@ -18,7 +18,8 @@ settings, folders, showEmbeds, - artifactContents + artifactContents, + visibleControls } from '$lib/stores'; import { getChatById } from '$lib/apis/chats'; @@ -389,7 +390,7 @@
{/if} - {#if ($artifactContents ?? []).length > 0} + {#if ($artifactContents ?? []).length > 0 && $visibleControls['chat.artifacts']} + {#if $visibleControls['chat.archive']} + + {/if} {#if $user?.role === 'admin' || ($user?.permissions?.chat?.delete ?? true)}
diff --git a/src/lib/components/layout/Sidebar.svelte b/src/lib/components/layout/Sidebar.svelte index 38ff0215be..3ddd1ae054 100644 --- a/src/lib/components/layout/Sidebar.svelte +++ b/src/lib/components/layout/Sidebar.svelte @@ -24,6 +24,7 @@ config, isApp, visiblePinnedModels, + visibleControls, selectedFolder, WEBUI_NAME, sidebarWidth @@ -1867,17 +1868,19 @@ >{$i18n.t('{{count}} selected', { count: $selectedChatIds.size })}
- - - + {#if $visibleControls['chat.archive']} + + + + {/if} {#if canDeleteChats}