This commit is contained in:
Timothy Jaeryang Baek
2026-07-15 00:30:28 -04:00
parent 7ebf15040b
commit e05f9fa17e
17 changed files with 201 additions and 184 deletions
@@ -45,7 +45,7 @@
</script>
<div id="tab-about" class="flex flex-col h-full justify-between space-y-3 text-sm">
<div class="flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5 -mr-1.5 space-y-3">
<div class="flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5 space-y-3">
<div>
<div class="text-xs text-gray-400 dark:text-gray-600 mb-2.5 flex space-x-2 items-center">
<div>
@@ -125,7 +125,7 @@
</script>
<div id="tab-account" class="flex h-full flex-col text-sm">
<div class="flex-1 min-h-0 w-full overflow-y-auto scrollbar-hover pr-1.5 -mr-1.5 py-1">
<div class="flex-1 min-h-0 w-full overflow-y-auto scrollbar-hover pr-1.5 py-1">
<h2 class="mb-4 text-sm font-medium text-gray-900 dark:text-white">{$i18n.t('Account')}</h2>
<div class="space-y-3">
@@ -174,7 +174,7 @@
/>
<div id="tab-archived-chats" class="flex flex-col h-full text-sm">
<div class="flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5 -mr-1.5">
<div class="flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5">
<div>
<div class="text-xs text-gray-400 dark:text-gray-600 mb-2">
{$i18n.t('Conversations')}
@@ -175,7 +175,7 @@
dispatch('save');
}}
>
<div class="flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5 -mr-1.5 space-y-3">
<div class="flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5 space-y-3">
<div>
<div class="text-xs text-gray-400 dark:text-gray-600 mb-2">
{$i18n.t('STT Settings')}
@@ -76,7 +76,7 @@
updateHandler();
}}
>
<div class="flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5 -mr-1.5">
<div class="flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5">
{#if config !== null}
<div class="">
<div class="pr-1.5">
@@ -133,7 +133,7 @@
/>
<div id="tab-chats" class="flex flex-col h-full text-sm">
<div class="flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5 -mr-1.5 space-y-3">
<div class="flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5 space-y-3">
<input
id="chat-import-input"
bind:this={chatImportInputElement}
@@ -206,7 +206,7 @@
</script>
<div class="flex flex-col h-full justify-between text-sm" id="tab-general">
<div class="flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5 -mr-1.5">
<div class="flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5">
<div class="">
<div class="text-xs text-gray-400 dark:text-gray-600 mb-2">
{$i18n.t('WebUI Settings')}
@@ -80,7 +80,7 @@
updateHandler();
}}
>
<div class="flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5 -mr-1.5">
<div class="flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5">
{#if servers !== null}
<div>
<div class="pr-1.5">
@@ -335,7 +335,7 @@
}}
/>
<div class="flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5 -mr-1.5 space-y-3">
<div class="flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5 space-y-3">
<div>
<h3 class="text-xs text-gray-400 dark:text-gray-600 mb-2">{$i18n.t('UI')}</h3>
@@ -29,7 +29,7 @@
dispatch('save');
}}
>
<div class="flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5 -mr-1.5 py-1">
<div class="flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5 py-1">
<div>
<div class="flex items-center justify-between mb-1">
<Tooltip
@@ -0,0 +1,105 @@
<script lang="ts">
import { getContext, onMount } from 'svelte';
import type { Writable } from 'svelte/store';
import { shortcuts } from '$lib/shortcuts';
import { settings } from '$lib/stores';
import ShortcutItem from '../ShortcutItem.svelte';
type ShortcutDefinition = NonNullable<(typeof shortcuts)[keyof typeof shortcuts]>;
type CategorizedShortcuts = {
[category: string]: ShortcutDefinition[];
};
const i18n: Writable<any> = getContext('i18n');
let categorizedShortcuts: CategorizedShortcuts = {};
let isMac = false;
onMount(() => {
isMac = /Mac/i.test(navigator.userAgent);
});
$: {
const allShortcuts = Object.values(shortcuts).filter((shortcut) => {
if (!shortcut.setting) {
return true;
}
return ($settings as Record<string, any>)[shortcut.setting.id] === shortcut.setting.value;
});
categorizedShortcuts = allShortcuts.reduce<CategorizedShortcuts>((acc, shortcut) => {
const category = shortcut.category;
if (!acc[category]) {
acc[category] = [];
}
acc[category].push(shortcut);
return acc;
}, {});
}
</script>
<div id="tab-shortcuts" class="flex h-full flex-col text-sm">
<div class="flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5">
<div class="flex items-center gap-2 px-1 pb-1">
<span class="flex-1 text-[0.625rem] font-medium text-gray-400 dark:text-gray-600">
{$i18n.t('Command')}
</span>
<span
class="w-[9.5rem] shrink-0 text-right text-[0.625rem] font-medium text-gray-400 dark:text-gray-600"
>
{$i18n.t('Key')}
</span>
</div>
{#each Object.entries(categorizedShortcuts) as [category, items], categoryIndex}
<div class={categoryIndex > 0 ? 'mt-3' : ''}>
<div
class="px-1 pb-0.5 pt-1 text-[0.625rem] font-medium uppercase tracking-wide text-gray-400 dark:text-gray-600"
>
{$i18n.t(category)}
</div>
<div class="divide-y divide-gray-100/70 dark:divide-white/[0.03]">
<!-- {$i18n.t('Chat')} -->
<!-- {$i18n.t('Global')} -->
<!-- {$i18n.t('Input')} -->
<!-- {$i18n.t('Message')} -->
<!-- {$i18n.t('New Chat')} -->
<!-- {$i18n.t('New Temporary Chat')} -->
<!-- {$i18n.t('Delete Chat')} -->
<!-- {$i18n.t('Open Model Selector')} -->
<!-- {$i18n.t('Toggle Dictation')} -->
<!-- {$i18n.t('Search')} -->
<!-- {$i18n.t('Open Settings')} -->
<!-- {$i18n.t('Show Shortcuts')} -->
<!-- {$i18n.t('Toggle Sidebar')} -->
<!-- {$i18n.t('Close Modal')} -->
<!-- {$i18n.t('Focus Chat Input')} -->
<!-- {$i18n.t('Accept Autocomplete Generation\nJump to Prompt Variable')} -->
<!-- {$i18n.t('Prevent File Creation')} -->
<!-- {$i18n.t('Attach File From Knowledge')} -->
<!-- {$i18n.t('Add Custom Prompt')} -->
<!-- {$i18n.t('Talk to Model')} -->
<!-- {$i18n.t('Generate Message Pair')} -->
<!-- {$i18n.t('Regenerate Response')} -->
<!-- {$i18n.t('Stop Generating')} -->
<!-- {$i18n.t('Edit Last Message')} -->
<!-- {$i18n.t('Copy Last Response')} -->
<!-- {$i18n.t('Copy Last Code Block')} -->
<!-- {$i18n.t('Only active when "Paste Large Text as File" setting is toggled on.')} -->
<!-- {$i18n.t('Only active when the chat input is in focus.')} -->
<!-- {$i18n.t('Only active when the chat input is in focus and an LLM is generating a response.')} -->
<!-- {$i18n.t('Only can be triggered when the chat input is in focus.')} -->
<!-- {$i18n.t('Voice')} -->
<!-- {$i18n.t('Toggle Mute')} -->
<!-- {$i18n.t('Only active during Voice Mode.')} -->
{#each items as shortcut}
<ShortcutItem {shortcut} {isMac} />
{/each}
</div>
</div>
{/each}
</div>
</div>
+50 -4
View File
@@ -12,6 +12,7 @@
import About from './Settings/About.svelte';
import General from './Settings/General.svelte';
import Interface from './Settings/Interface.svelte';
import Shortcuts from './Settings/Shortcuts.svelte';
import Audio from './Settings/Audio.svelte';
import DataControls from './Settings/DataControls.svelte';
import ArchivedChats from './Settings/ArchivedChats.svelte';
@@ -31,17 +32,32 @@
import UserBadgeCheck from '../icons/UserBadgeCheck.svelte';
import ArchiveBox from '../icons/ArchiveBox.svelte';
import ChevronLeft from '../icons/ChevronLeft.svelte';
import Keyboard from '../icons/Keyboard.svelte';
const i18n: Writable<any> = getContext('i18n');
export let show: boolean | string = false;
let modalShow = false;
let lastShow: boolean | string = false;
$: if (show) {
$: if (show !== lastShow) {
lastShow = show;
if (typeof show === 'string') {
selectedTab = show;
show = true;
lastShow = true;
modalShow = true;
} else {
modalShow = show;
if (!show) {
selectedTab = 'general';
}
}
} else {
}
$: if (!modalShow && show !== false) {
show = false;
lastShow = false;
selectedTab = 'general';
}
@@ -211,6 +227,21 @@
'web search in chat'
]
},
{
id: 'shortcuts',
title: 'Keyboard Shortcuts',
keywords: [
'commands',
'hotkeys',
'keyboard',
'keyboard shortcuts',
'keybindings',
'keys',
'shortcut',
'shortcuts',
'show shortcuts'
]
},
{
id: 'connections',
title: 'Connections',
@@ -593,8 +624,8 @@
<Modal
size="full"
containerClassName="p-4 sm:p-6 lg:p-8"
className="w-full max-w-[88rem] h-[min(50rem,calc(100dvh-4rem))] max-h-[calc(100dvh-4rem)] flex flex-col md:flex-row bg-white dark:bg-gray-900 rounded-4xl"
bind:show
className="!w-[calc(100vw-2rem)] sm:!w-[calc(100vw-3rem)] lg:!w-[calc(100vw-4rem)] !max-w-[76rem] h-[min(50rem,calc(100dvh-4rem))] max-h-[calc(100dvh-4rem)] flex flex-col md:flex-row bg-white dark:bg-gray-900 rounded-4xl"
bind:show={modalShow}
>
<nav
id="settings-tabs-container"
@@ -668,6 +699,19 @@
<AppNotification className="size-3.5" strokeWidth="2" />
<span>{$i18n.t('Interface')}</span>
</button>
{:else if tabId === 'shortcuts'}
<button
role="tab"
aria-controls="tab-shortcuts"
aria-selected={selectedTab === 'shortcuts'}
class={tabButtonClass(selectedTab === 'shortcuts')}
on:click={() => {
selectedTab = 'shortcuts';
}}
>
<Keyboard className="size-3.5" strokeWidth="2" />
<span>{$i18n.t('Keyboard Shortcuts')}</span>
</button>
{:else if tabId === 'connections'}
{#if $user?.role === 'admin' || ($user?.role === 'user' && $config?.features?.enable_direct_connections)}
<button
@@ -829,6 +873,8 @@
toast.success($i18n.t('Settings saved successfully!'));
}}
/>
{:else if selectedTab === 'shortcuts'}
<Shortcuts />
{:else if selectedTab === 'connections'}
<Connections
saveSettings={async (updated: Record<string, any>) => {
+33 -19
View File
@@ -1,18 +1,25 @@
<script lang="ts">
import { getContext, onMount } from 'svelte';
import type { Writable } from 'svelte/store';
import Tooltip from '../common/Tooltip.svelte';
import type { Shortcut } from '$lib/shortcuts';
import { shortcuts } from '$lib/shortcuts';
export let shortcut: Shortcut;
type ShortcutDefinition = NonNullable<(typeof shortcuts)[keyof typeof shortcuts]>;
export let shortcut: ShortcutDefinition;
export let isMac: boolean;
const i18n = getContext('i18n');
const i18n: Writable<any> = getContext('i18n');
let keyboardLayoutMap: Map<string, string> | undefined;
onMount(async () => {
if (navigator.keyboard && 'getLayoutMap' in navigator.keyboard) {
const nav = navigator as Navigator & {
keyboard?: { getLayoutMap?: () => Promise<Map<string, string>> };
};
if (nav.keyboard?.getLayoutMap) {
try {
keyboardLayoutMap = await navigator.keyboard.getLayoutMap();
keyboardLayoutMap = await nav.keyboard.getLayoutMap();
} catch (error) {
console.error('Failed to get keyboard layout map:', error);
}
@@ -70,27 +77,34 @@
return key.toUpperCase();
}
}
function visibleKeys(keys: string[]): string[] {
return keys.filter((key) => !(key.toLowerCase() === 'delete' && keys.includes('Backspace')));
}
function formatKeys(keys: string[]): string {
return visibleKeys(keys)
.map(formatKey)
.join(isMac ? '' : ' + ');
}
</script>
<div class="w-full flex justify-between">
<div class="text-sm whitespace-pre-line">
<div class="flex min-h-8 w-full items-center gap-3 px-1 py-1.5">
<div class="min-w-0 flex-1 text-[0.71875rem] leading-tight text-gray-700 dark:text-gray-300">
{#if shortcut.tooltip}
<Tooltip content={$i18n.t(shortcut.tooltip)}>
<span class="whitespace-nowrap">
{$i18n.t(shortcut.name)}<span class="text-xs">&nbsp;*</span>
<span class="inline-flex max-w-full items-baseline gap-1">
<span class="truncate whitespace-pre-line">{$i18n.t(shortcut.name)}</span>
<span class="text-[0.625rem] text-gray-400 dark:text-gray-600">*</span>
</span>
</Tooltip>
{:else}
{$i18n.t(shortcut.name)}
<span class="whitespace-pre-line">{$i18n.t(shortcut.name)}</span>
{/if}
</div>
<div class="flex-shrink-0 flex justify-end self-start h-full space-x-1 text-xs">
{#each shortcut.keys.filter((key) => !(key.toLowerCase() === 'delete' && shortcut.keys.includes('Backspace'))) as key}
<div
class="h-fit px-1 py-0.5 flex items-start justify-center rounded-sm border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300"
>
{formatKey(key)}
</div>
{/each}
</div>
<span
class="inline-flex min-h-[1.125rem] max-w-[9.5rem] shrink-0 items-center justify-center rounded-full bg-gray-100 px-[0.4375rem] py-0.5 text-center text-[0.625rem] font-medium leading-none text-gray-500 dark:bg-white/6 dark:text-gray-400"
>
{formatKeys(shortcut.keys)}
</span>
</div>
@@ -1,135 +0,0 @@
<script lang="ts">
import { getContext, onMount } from 'svelte';
import Modal from '../common/Modal.svelte';
import { shortcuts } from '$lib/shortcuts';
import { settings } from '$lib/stores';
import ShortcutItem from './ShortcutItem.svelte';
import XMark from '$lib/components/icons/XMark.svelte';
type CategorizedShortcuts = {
[category: string]: {
left: Shortcut[];
right: Shortcut[];
};
};
const i18n = getContext('i18n');
export let show = false;
let categorizedShortcuts: CategorizedShortcuts = {};
let isMac = false;
onMount(() => {
isMac = /Mac/i.test(navigator.userAgent);
});
$: {
const allShortcuts = Object.values(shortcuts).filter((shortcut) => {
if (!shortcut.setting) {
return true;
}
return $settings[shortcut.setting.id] === shortcut.setting.value;
});
categorizedShortcuts = allShortcuts.reduce((acc, shortcut) => {
const category = shortcut.category;
if (!acc[category]) {
acc[category] = [];
}
acc[category].push(shortcut);
return acc;
}, {});
}
</script>
<Modal bind:show>
<div class="text-gray-700 dark:text-gray-100 px-5 py-4">
<div class="flex justify-between dark:text-gray-300 pb-2">
<div class="text-lg font-normal self-center">{$i18n.t('Keyboard Shortcuts')}</div>
<button class="self-center" aria-label={$i18n.t('Close')} on:click={() => (show = false)}>
<XMark className={'size-5'} />
</button>
</div>
{#each Object.entries(categorizedShortcuts) as [category, items], categoryIndex}
{#if categoryIndex > 0}
<div class="py-3">
<div class="w-full border-t dark:border-gray-850 border-gray-50" />
</div>
{/if}
<div class="flex justify-between dark:text-gray-300 pb-2">
<div class="text-base self-center">{$i18n.t(category)}</div>
</div>
<div class="flex flex-col md:flex-row w-full md:space-x-2 dark:text-gray-200">
<div class="flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
<div class=" grid grid-cols-1 sm:grid-cols-2 gap-2 gap-x-4 w-full">
<!-- {$i18n.t('Chat')} -->
<!-- {$i18n.t('Global')} -->
<!-- {$i18n.t('Input')} -->
<!-- {$i18n.t('Message')} -->
<!-- {$i18n.t('New Chat')} -->
<!-- {$i18n.t('New Temporary Chat')} -->
<!-- {$i18n.t('Delete Chat')} -->
<!-- {$i18n.t('Open Model Selector')} -->
<!-- {$i18n.t('Toggle Dictation')} -->
<!-- {$i18n.t('Search')} -->
<!-- {$i18n.t('Open Settings')} -->
<!-- {$i18n.t('Show Shortcuts')} -->
<!-- {$i18n.t('Toggle Sidebar')} -->
<!-- {$i18n.t('Close Modal')} -->
<!-- {$i18n.t('Focus Chat Input')} -->
<!-- {$i18n.t('Accept Autocomplete Generation\nJump to Prompt Variable')} -->
<!-- {$i18n.t('Prevent File Creation')} -->
<!-- {$i18n.t('Attach File From Knowledge')} -->
<!-- {$i18n.t('Add Custom Prompt')} -->
<!-- {$i18n.t('Talk to Model')} -->
<!-- {$i18n.t('Generate Message Pair')} -->
<!-- {$i18n.t('Regenerate Response')} -->
<!-- {$i18n.t('Stop Generating')} -->
<!-- {$i18n.t('Edit Last Message')} -->
<!-- {$i18n.t('Copy Last Response')} -->
<!-- {$i18n.t('Copy Last Code Block')} -->
<!-- {$i18n.t('Only active when "Paste Large Text as File" setting is toggled on.')} -->
<!-- {$i18n.t('Only active when the chat input is in focus.')} -->
<!-- {$i18n.t('Only active when the chat input is in focus and an LLM is generating a response.')} -->
<!-- {$i18n.t('Only can be triggered when the chat input is in focus.')} -->
<!-- {$i18n.t('Voice')} -->
<!-- {$i18n.t('Toggle Mute')} -->
<!-- {$i18n.t('Only active during Voice Mode.')} -->
{#each items as shortcut}
<div class="col-span-1 flex items-start">
<ShortcutItem {shortcut} {isMac} />
</div>
{/each}
</div>
</div>
</div>
{/each}
</div>
</Modal>
<style>
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
.tabs::-webkit-scrollbar {
display: none;
}
.tabs {
-ms-overflow-style: none;
scrollbar-width: none;
}
input[type='number'] {
-moz-appearance: textfield;
}
</style>
@@ -7,15 +7,7 @@
import { getUsage } from '$lib/apis';
import { getSessionUser, userSignOut } from '$lib/apis/auths';
import {
showSettings,
mobile,
showSidebar,
showShortcuts,
user,
config,
settings
} from '$lib/stores';
import { showSettings, mobile, showSidebar, user, config, settings } from '$lib/stores';
import { WEBUI_API_BASE_URL } from '$lib/constants';
@@ -25,7 +17,6 @@
import QuestionMarkCircle from '$lib/components/icons/QuestionMarkCircle.svelte';
import Map from '$lib/components/icons/Map.svelte';
import Keyboard from '$lib/components/icons/Keyboard.svelte';
import ShortcutsModal from '$lib/components/chat/ShortcutsModal.svelte';
import Settings from '$lib/components/icons/Settings.svelte';
import Code from '$lib/components/icons/Code.svelte';
import UserGroup from '$lib/components/icons/UserGroup.svelte';
@@ -109,7 +100,6 @@
}}
/>
<ShortcutsModal bind:show={$showShortcuts} />
<UserStatusModal
bind:show={showUserStatusModal}
onSave={async () => {
@@ -546,7 +536,7 @@
id="chat-share-button"
on:click={async () => {
show = false;
showShortcuts.set(!$showShortcuts);
showSettings.set('shortcuts');
if ($mobile) {
await tick();
-1
View File
@@ -92,7 +92,6 @@ export const sidebarWidth = writable(245);
export const showSidebar = writable(false);
export const showSearch = writable(false);
export const showSettings: Writable<boolean | string> = writable(false);
export const showShortcuts = writable(false);
export const showChangelog = writable(false);
export const showControls = writable(false);
+1 -3
View File
@@ -30,7 +30,6 @@
tags,
banners,
showSettings,
showShortcuts,
showChangelog,
temporaryChatEnabled,
toolServers,
@@ -281,12 +280,11 @@
} else if (isShortcutMatch(event, shortcuts[Shortcut.SHOW_SHORTCUTS])) {
console.log('Shortcut triggered: SHOW_SHORTCUTS');
event.preventDefault();
showShortcuts.set(!$showShortcuts);
showSettings.set('shortcuts');
} else if (isShortcutMatch(event, shortcuts[Shortcut.CLOSE_MODAL])) {
console.log('Shortcut triggered: CLOSE_MODAL');
event.preventDefault();
showSettings.set(false);
showShortcuts.set(false);
} else if (isShortcutMatch(event, shortcuts[Shortcut.OPEN_MODEL_SELECTOR])) {
console.log('Shortcut triggered: OPEN_MODEL_SELECTOR');
event.preventDefault();