From e361606c61d8fbcc9a1f17d3674e3a0421aa28ee Mon Sep 17 00:00:00 2001 From: silentoplayz Date: Sat, 11 Oct 2025 20:59:08 -0400 Subject: [PATCH 1/2] feat: add toggleable hotkey hints to sidebar buttons and refac ShortcutsModal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add src/lib/shortcuts.ts as the single source of truth for every shortcut - Create HotkeyHint.svelte to show OS-aware key combos (⌘ on Mac, Ctrl elsewhere) - Make sidebar “New Chat” and “Search” buttons display their shortcuts on hover - Add user setting to toggle these sidebar hints - Refactor ShortcutsModal into categorized (Global, Chat, Message, Input), data-driven sections - Introduce ShortcutItem.svelte to render each row, dividers, and multi-line text - Fix “Focus text area” action and include “Close modal” shortcut - Wire everything through +layout.svelte and the shortcuts registry --- .../components/chat/Settings/Interface.svelte | 21 + src/lib/components/chat/ShortcutItem.svelte | 58 +++ src/lib/components/chat/ShortcutsModal.svelte | 417 +++--------------- src/lib/components/common/HotkeyHint.svelte | 41 ++ src/lib/components/layout/Sidebar.svelte | 12 +- src/lib/i18n/locales/en-US/translation.json | 1 + src/lib/shortcuts.ts | 146 ++++++ src/lib/stores/index.ts | 1 + src/routes/(app)/+layout.svelte | 206 +++++---- 9 files changed, 450 insertions(+), 453 deletions(-) create mode 100644 src/lib/components/chat/ShortcutItem.svelte create mode 100644 src/lib/components/common/HotkeyHint.svelte create mode 100644 src/lib/shortcuts.ts diff --git a/src/lib/components/chat/Settings/Interface.svelte b/src/lib/components/chat/Settings/Interface.svelte index 4ae24d0846..adbf8f05b1 100644 --- a/src/lib/components/chat/Settings/Interface.svelte +++ b/src/lib/components/chat/Settings/Interface.svelte @@ -67,6 +67,7 @@ let collapseCodeBlocks = false; let expandDetails = false; let showChatTitleInTab = true; + let showSidebarHotkeyHints = true; let showFloatingActionButtons = true; let floatingActionButtons = null; @@ -226,6 +227,7 @@ chatDirection = $settings?.chatDirection ?? 'auto'; userLocation = $settings?.userLocation ?? false; showChatTitleInTab = $settings?.showChatTitleInTab ?? true; + showSidebarHotkeyHints = $settings?.showSidebarHotkeyHints ?? true; notificationSound = $settings?.notificationSound ?? true; notificationSoundAlways = $settings?.notificationSoundAlways ?? false; @@ -350,6 +352,25 @@ +
+
+
+ {$i18n.t('Show Sidebar Hotkey Hints')} +
+ +
+ { + saveSettings({ showSidebarHotkeyHints }); + }} + /> +
+
+
+
diff --git a/src/lib/components/chat/ShortcutItem.svelte b/src/lib/components/chat/ShortcutItem.svelte new file mode 100644 index 0000000000..6eca81d3bd --- /dev/null +++ b/src/lib/components/chat/ShortcutItem.svelte @@ -0,0 +1,58 @@ + + +
+
+ {#if shortcut.tooltip} + + + {$i18n.t(shortcut.name)} * + + + {:else} + {$i18n.t(shortcut.name)} + {/if} +
+
+ {#each shortcut.keys as key} +
+ {formatKey(key)} +
+ {/each} +
+
\ No newline at end of file diff --git a/src/lib/components/chat/ShortcutsModal.svelte b/src/lib/components/chat/ShortcutsModal.svelte index d5b083caa5..12161e75f5 100644 --- a/src/lib/components/chat/ShortcutsModal.svelte +++ b/src/lib/components/chat/ShortcutsModal.svelte @@ -1,392 +1,107 @@
-
-
{$i18n.t('Keyboard shortcuts')}
-
-
-
-
-
-
{$i18n.t('Open new chat')}
- -
-
- Ctrl/⌘ -
- -
- Shift -
- -
- O -
-
-
- -
-
{$i18n.t('Focus chat input')}
- -
-
- Shift -
- -
- Esc -
-
-
- -
-
- - {$i18n.t('Stop Generating')} * - -
- -
-
- Esc -
-
-
- -
-
{$i18n.t('Copy last code block')}
- -
-
- Ctrl/⌘ -
- -
- Shift -
- -
- ; -
-
-
- -
-
{$i18n.t('Copy last response')}
- -
-
- Ctrl/⌘ -
- -
- Shift -
- -
- C -
-
-
- -
-
- - {$i18n.t('Prevent file creation')} * - -
- -
-
- Ctrl/⌘ -
- -
- Shift -
- -
- V -
-
-
+ {#each Object.entries(categorizedShortcuts) as [category, columns], i} + {#if i > 0} +
+
+ {/if} -
-
-
{$i18n.t('Generate prompt pair')}
- -
-
- Ctrl/⌘ -
- -
- Shift -
- -
- Enter -
-
+
+
{$i18n.t(category)}
+
+
+
+
+ {#each columns.left as shortcut} + + {/each}
- -
-
{$i18n.t('Toggle search')}
- -
-
- Ctrl/⌘ -
-
- K -
-
-
- -
-
{$i18n.t('Toggle settings')}
- -
-
- Ctrl/⌘ -
-
- . -
-
-
- -
-
{$i18n.t('Toggle sidebar')}
- -
-
- Ctrl/⌘ -
- -
- Shift -
- -
- S -
-
-
- -
-
{$i18n.t('Delete chat')}
- -
-
- Ctrl/⌘ -
-
- Shift -
- -
- ⌫/Delete -
-
-
- -
-
{$i18n.t('Show shortcuts')}
- -
-
- Ctrl/⌘ -
- -
- / -
-
+
+ {#each columns.right as shortcut} + + {/each}
-
+ {/each}
- {$i18n.t( + {@html $i18n.t( 'Shortcuts with an asterisk (*) are situational and only active under specific conditions.' )}
-
-
{$i18n.t('Input commands')}
-
- -
-
-
-
-
- {$i18n.t('Attach file from knowledge')} -
- -
-
- # -
-
-
- -
-
- {$i18n.t('Add custom prompt')} -
- -
-
- / -
-
-
- -
-
- {$i18n.t('Talk to model')} -
- -
-
- @ -
-
-
- -
-
- {$i18n.t('Accept autocomplete generation / Jump to prompt variable')} -
- -
-
- TAB -
-
-
-
-
-
+ \ No newline at end of file diff --git a/src/lib/components/common/HotkeyHint.svelte b/src/lib/components/common/HotkeyHint.svelte new file mode 100644 index 0000000000..1246630cbd --- /dev/null +++ b/src/lib/components/common/HotkeyHint.svelte @@ -0,0 +1,41 @@ + + +{#if mounted && isVisible} + +{/if} \ No newline at end of file diff --git a/src/lib/components/layout/Sidebar.svelte b/src/lib/components/layout/Sidebar.svelte index 282909465e..dd0eb0ec81 100644 --- a/src/lib/components/layout/Sidebar.svelte +++ b/src/lib/components/layout/Sidebar.svelte @@ -62,6 +62,7 @@ import PinnedModelList from './Sidebar/PinnedModelList.svelte'; import Note from '../icons/Note.svelte'; import { slide } from 'svelte/transition'; + import HotkeyHint from '../common/HotkeyHint.svelte'; const BREAKPOINT = 768; @@ -787,7 +788,7 @@ -
-
+
{$i18n.t('Search')}
+
diff --git a/src/lib/i18n/locales/en-US/translation.json b/src/lib/i18n/locales/en-US/translation.json index 0ed731795f..5977953945 100644 --- a/src/lib/i18n/locales/en-US/translation.json +++ b/src/lib/i18n/locales/en-US/translation.json @@ -1446,6 +1446,7 @@ "Show image preview": "", "Show Model": "", "Show shortcuts": "", + "Show Sidebar Hotkey Hints": "", "Show your support!": "", "Showcased creativity": "", "Sign in": "", diff --git a/src/lib/shortcuts.ts b/src/lib/shortcuts.ts new file mode 100644 index 0000000000..44eee95d6c --- /dev/null +++ b/src/lib/shortcuts.ts @@ -0,0 +1,146 @@ +type ShortcutRegistry = { + [key in Shortcut]: { + name: string; + keys: string[]; + category: string; + tooltip?: string; + }; +}; + +export enum Shortcut { + //Chat + NEW_CHAT = 'newChat', + NEW_TEMPORARY_CHAT = 'newTemporaryChat', + DELETE_CHAT = 'deleteChat', + GENERATE_PROMPT_PAIR = 'generatePromptPair', + + //Global + SEARCH = 'search', + OPEN_SETTINGS = 'openSettings', + SHOW_SHORTCUTS = 'showShortcuts', + TOGGLE_SIDEBAR = 'toggleSidebar', + CLOSE_MODAL = 'closeModal', + + //Input + FOCUS_INPUT = 'focusInput', + ACCEPT_AUTOCOMPLETE = 'acceptAutocomplete', + PREVENT_FILE_CREATION = 'preventFileCreation', + NAVIGATE_PROMPT_HISTORY_UP = 'navigatePromptHistoryUp', + ATTACH_FILE = 'attachFile', + ADD_PROMPT = 'addPrompt', + TALK_TO_MODEL = 'talkToModel', + + //Message + COPY_LAST_CODE_BLOCK = 'copyLastCodeBlock', + COPY_LAST_RESPONSE = 'copyLastResponse', + STOP_GENERATING = 'stopGenerating' +} + +export const shortcuts: ShortcutRegistry = { + //Chat + [Shortcut.NEW_CHAT]: { + name: 'New Chat', + keys: ['mod', 'shift', 'KeyO'], + category: 'Chat' + }, + [Shortcut.NEW_TEMPORARY_CHAT]: { + name: 'New Temporary Chat', + keys: ['mod', 'shift', 'Quote'], + category: 'Chat' + }, + [Shortcut.DELETE_CHAT]: { + name: 'Delete Chat', + keys: ['mod', 'shift', 'Backspace'], + category: 'Chat' + }, + [Shortcut.GENERATE_PROMPT_PAIR]: { + name: 'Generate Prompt Pair', + keys: ['mod', 'shift', 'Enter'], + category: 'Chat', + tooltip: 'Only active when the chat input is in focus and an LLM is generating a response.' + }, + + //Global + [Shortcut.SEARCH]: { + name: 'Search', + keys: ['mod', 'KeyK'], + category: 'Global' + }, + [Shortcut.OPEN_SETTINGS]: { + name: 'Open Settings', + keys: ['mod', 'Period'], + category: 'Global' + }, + [Shortcut.SHOW_SHORTCUTS]: { + name: 'Show Shortcuts', + keys: ['mod', 'Slash'], + category: 'Global' + }, + [Shortcut.TOGGLE_SIDEBAR]: { + name: 'Toggle Sidebar', + keys: ['mod', 'shift', 'KeyS'], + category: 'Global' + }, + [Shortcut.CLOSE_MODAL]: { + name: 'Close Modal', + keys: ['Escape'], + category: 'Global' + }, + + //Input + [Shortcut.FOCUS_INPUT]: { + name: 'Focus Text Area', + keys: ['shift', 'Escape'], + category: 'Input' + }, + [Shortcut.ACCEPT_AUTOCOMPLETE]: { + name: 'Accept Autocomplete Generation\nJump to Prompt Variable', + keys: ['Tab'], + category: 'Input' + }, + [Shortcut.PREVENT_FILE_CREATION]: { + name: 'Prevent File Creation', + keys: ['mod', 'shift', 'KeyV'], + category: 'Input', + tooltip: 'Only active when "Paste Large Text as File" setting is toggled on.' + }, + [Shortcut.NAVIGATE_PROMPT_HISTORY_UP]: { + name: 'Edit Last Message', + keys: ['ArrowUp'], + category: 'Input', + tooltip: 'Only can be triggered when the chat input is in focus.' + }, + [Shortcut.ATTACH_FILE]: { + name: 'Attach File From Knowledge', + keys: ['#'], + category: 'Input' + }, + [Shortcut.ADD_PROMPT]: { + name: 'Add Custom Prompt', + keys: ['/'], + category: 'Input' + }, + [Shortcut.TALK_TO_MODEL]: { + name: 'Talk to Model', + keys: ['@'], + category: 'Input' + }, + + //Message + [Shortcut.COPY_LAST_CODE_BLOCK]: { + name: 'Copy Last Code Block', + keys: ['mod', 'shift', 'Semicolon'], + category: 'Message' + }, + [Shortcut.COPY_LAST_RESPONSE]: { + name: 'Copy Last Response', + keys: ['mod', 'shift', 'KeyC'], + category: 'Message' + }, + [Shortcut.STOP_GENERATING]: { + name: 'Stop Generating', + keys: ['Escape'], + category: 'Message', + tooltip: 'Only active when the chat input is in focus and an LLM is generating a response.' + } +}; \ No newline at end of file diff --git a/src/lib/stores/index.ts b/src/lib/stores/index.ts index de37963adb..9fab67df4b 100644 --- a/src/lib/stores/index.ts +++ b/src/lib/stores/index.ts @@ -187,6 +187,7 @@ type Settings = { highContrastMode?: boolean; title?: TitleSettings; showChatTitleInTab?: boolean; + showSidebarHotkeyHints?: boolean; splitLargeDeltas?: boolean; chatDirection?: 'LTR' | 'RTL' | 'auto'; ctrlEnterToSend?: boolean; diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte index f9a8470465..979a9ca3ca 100644 --- a/src/routes/(app)/+layout.svelte +++ b/src/routes/(app)/+layout.svelte @@ -47,6 +47,7 @@ import AccountPending from '$lib/components/layout/Overlay/AccountPending.svelte'; import UpdateInfoToast from '$lib/components/layout/UpdateInfoToast.svelte'; import Spinner from '$lib/components/common/Spinner.svelte'; + import { Shortcut, shortcuts } from '$lib/shortcuts'; const i18n = getContext('i18n'); @@ -163,105 +164,114 @@ }) ]); - const setupKeyboardShortcuts = () => { - document.addEventListener('keydown', async function (event) { - const isCtrlPressed = event.ctrlKey || event.metaKey; // metaKey is for Cmd key on Mac - // Check if the Shift key is pressed - const isShiftPressed = event.shiftKey; +// Helper function to check if the pressed keys match the shortcut definition + const checkShortcut = (event: KeyboardEvent, keys: string[]): boolean => { + const lowerCaseKeys = keys.map((key) => key.toLowerCase()); - // Check if Ctrl + K is pressed - if (isCtrlPressed && event.key.toLowerCase() === 'k') { - event.preventDefault(); - console.log('search'); - showSearch.set(!$showSearch); + const isModPressed = lowerCaseKeys.includes('mod') ? event.ctrlKey || event.metaKey : true; + const isShiftPressed = lowerCaseKeys.includes('shift') ? event.shiftKey : true; + const isAltPressed = lowerCaseKeys.includes('alt') ? event.altKey : true; + const isCtrlPressed = lowerCaseKeys.includes('ctrl') ? event.ctrlKey : true; + + const mainKeys = lowerCaseKeys.filter((key) => !['mod', 'shift', 'alt', 'ctrl'].includes(key)); + + if (mainKeys.length > 0 && !mainKeys.includes(event.code.toLowerCase())) { + return false; + } + + let modConflict = false; + if (keys.includes('mod')) { + if (!lowerCaseKeys.includes('shift') && event.shiftKey) modConflict = true; + if (!lowerCaseKeys.includes('alt') && event.altKey) modConflict = true; + } + + return ( + !modConflict && + isModPressed && + isShiftPressed && + isAltPressed && + isCtrlPressed && + (event.ctrlKey || event.metaKey) === (lowerCaseKeys.includes('mod') || lowerCaseKeys.includes('ctrl')) && + event.shiftKey === lowerCaseKeys.includes('shift') && + event.altKey === lowerCaseKeys.includes('alt') + ); + }; + +const setupKeyboardShortcuts = () => { + document.addEventListener('keydown', async (event) => { + for (const shortcutName in shortcuts) { + const shortcut = shortcuts[shortcutName]; + if (checkShortcut(event, shortcut.keys)) { + // Here you can map the shortcut name to an action + // This is a simple example, you might want a more robust solution + switch (shortcutName) { + case Shortcut.SEARCH: + event.preventDefault(); + showSearch.set(!$showSearch); + break; + case Shortcut.NEW_CHAT: + event.preventDefault(); + document.getElementById('sidebar-new-chat-button')?.click(); + break; + case Shortcut.FOCUS_INPUT: + event.preventDefault(); + document.getElementById('chat-input')?.focus(); + break; + case Shortcut.COPY_LAST_CODE_BLOCK: + event.preventDefault(); + [...document.getElementsByClassName('copy-code-button')]?.at(-1)?.click(); + break; + case Shortcut.COPY_LAST_RESPONSE: + event.preventDefault(); + [...document.getElementsByClassName('copy-response-button')]?.at(-1)?.click(); + break; + case Shortcut.TOGGLE_SIDEBAR: + event.preventDefault(); + showSidebar.set(!$showSidebar); + break; + case Shortcut.DELETE_CHAT: + event.preventDefault(); + document.getElementById('delete-chat-button')?.click(); + break; + case Shortcut.OPEN_SETTINGS: + event.preventDefault(); + showSettings.set(!$showSettings); + break; + case Shortcut.SHOW_SHORTCUTS: + event.preventDefault(); + showShortcuts.set(!$showShortcuts); + break; + case Shortcut.CLOSE_MODAL: + event.preventDefault(); + showSettings.set(false); + showShortcuts.set(false); + break; + case Shortcut.NEW_TEMPORARY_CHAT: + event.preventDefault(); + if ($user?.role !== 'admin' && $user?.permissions?.chat?.temporary_enforced) { + temporaryChatEnabled.set(true); + } else { + temporaryChatEnabled.set(!$temporaryChatEnabled); + } + await goto('/'); + setTimeout(() => { + document.getElementById('new-chat-button')?.click(); + }, 0); + break; + case Shortcut.GENERATE_PROMPT_PAIR: + // Placeholder for future implementation + break; + case Shortcut.STOP_GENERATING: + // Placeholder for future implementation + break; + case Shortcut.PREVENT_FILE_CREATION: + // This shortcut is handled by the paste event in MessageInput.svelte + break; } - - // Check if Ctrl + Shift + O is pressed - if (isCtrlPressed && isShiftPressed && event.key.toLowerCase() === 'o') { - event.preventDefault(); - console.log('newChat'); - document.getElementById('sidebar-new-chat-button')?.click(); - } - - // Check if Shift + Esc is pressed - if (isShiftPressed && event.key === 'Escape') { - event.preventDefault(); - console.log('focusInput'); - document.getElementById('chat-input')?.focus(); - } - - // Check if Ctrl + Shift + ; is pressed - if (isCtrlPressed && isShiftPressed && event.key === ';') { - event.preventDefault(); - console.log('copyLastCodeBlock'); - const button = [...document.getElementsByClassName('copy-code-button')]?.at(-1); - button?.click(); - } - - // Check if Ctrl + Shift + C is pressed - if (isCtrlPressed && isShiftPressed && event.key.toLowerCase() === 'c') { - event.preventDefault(); - console.log('copyLastResponse'); - const button = [...document.getElementsByClassName('copy-response-button')]?.at(-1); - console.log(button); - button?.click(); - } - - // Check if Ctrl + Shift + S is pressed - if (isCtrlPressed && isShiftPressed && event.key.toLowerCase() === 's') { - event.preventDefault(); - console.log('toggleSidebar'); - document.getElementById('sidebar-toggle-button')?.click(); - } - - // Check if Ctrl + Shift + Backspace is pressed - if ( - isCtrlPressed && - isShiftPressed && - (event.key === 'Backspace' || event.key === 'Delete') - ) { - event.preventDefault(); - console.log('deleteChat'); - document.getElementById('delete-chat-button')?.click(); - } - - // Check if Ctrl + . is pressed - if (isCtrlPressed && event.key === '.') { - event.preventDefault(); - console.log('openSettings'); - showSettings.set(!$showSettings); - } - - // Check if Ctrl + / is pressed - if (isCtrlPressed && event.key === '/') { - event.preventDefault(); - - showShortcuts.set(!$showShortcuts); - } - - // Check if Ctrl + Shift + ' is pressed - if ( - isCtrlPressed && - isShiftPressed && - (event.key.toLowerCase() === `'` || event.key.toLowerCase() === `"`) - ) { - event.preventDefault(); - console.log('temporaryChat'); - - if ($user?.role !== 'admin' && $user?.permissions?.chat?.temporary_enforced) { - temporaryChatEnabled.set(true); - } else { - temporaryChatEnabled.set(!$temporaryChatEnabled); - } - - await goto('/'); - const newChatButton = document.getElementById('new-chat-button'); - setTimeout(() => { - newChatButton?.click(); - }, 0); - } - }); - }; + } + } + }); +}; setupKeyboardShortcuts(); if ($user?.role === 'admin' && ($settings?.showChangelog ?? true)) { @@ -445,4 +455,4 @@ cursor: pointer; background-color: #bcbabb; } - + \ No newline at end of file From 3c7e739b3cc99b351a06d563fef20d34ecfc3458 Mon Sep 17 00:00:00 2001 From: silentoplayz Date: Sat, 18 Oct 2025 00:32:03 -0400 Subject: [PATCH 2/2] refac --- src/lib/components/chat/MessageInput.svelte | 24 ++---- .../components/chat/Settings/Interface.svelte | 21 ------ src/lib/components/chat/ShortcutItem.svelte | 57 ++++++++------ src/lib/components/chat/ShortcutsModal.svelte | 19 ++++- src/lib/components/common/HotkeyHint.svelte | 4 - src/lib/shortcuts.ts | 74 +++++++++++++------ src/lib/stores/index.ts | 1 - src/routes/(app)/+layout.svelte | 14 +++- 8 files changed, 119 insertions(+), 95 deletions(-) diff --git a/src/lib/components/chat/MessageInput.svelte b/src/lib/components/chat/MessageInput.svelte index c525dcf34c..9175d6ccdf 100644 --- a/src/lib/components/chat/MessageInput.svelte +++ b/src/lib/components/chat/MessageInput.svelte @@ -1051,6 +1051,12 @@ dispatch('submit', prompt); }} > +
-
diff --git a/src/lib/components/chat/ShortcutItem.svelte b/src/lib/components/chat/ShortcutItem.svelte index 6eca81d3bd..3b6c2e3cd6 100644 --- a/src/lib/components/chat/ShortcutItem.svelte +++ b/src/lib/components/chat/ShortcutItem.svelte @@ -11,26 +11,39 @@ function formatKey(key: string): string { const lowerKey = key.toLowerCase(); - if (lowerKey === 'mod') return isMac ? '⌘' : 'Ctrl/⌘'; - if (lowerKey === 'shift') return isMac ? '⇧' : 'Shift'; - if (lowerKey === 'backspace') return '⌫/Delete'; - if (lowerKey === 'escape') return 'Esc'; - if (lowerKey === 'enter') return 'Enter'; - if (lowerKey === 'tab') return 'Tab'; - if (lowerKey === 'arrowup') return '↑'; - if (lowerKey === 'arrowdown') return '↓'; - - // For keys like 'KeyK', 'KeyO', etc., we just want the last character. - if (lowerKey.startsWith('key')) return key.slice(-1); - // For keys like 'Digit2', 'Digit3', etc. - if (lowerKey.startsWith('digit')) return key.slice(-1); - // For other special keys - if (lowerKey === 'quote') return "'"; - if (lowerKey === 'period') return '.'; - if (lowerKey === 'slash') return '/'; - if (lowerKey === 'semicolon') return ';'; - - return key.toUpperCase(); + switch (lowerKey) { + case 'mod': + return isMac ? '⌘' : 'Ctrl'; + case 'shift': + return isMac ? '⇧' : 'Shift'; + case 'alt': + return isMac ? '⌥' : 'Alt'; + case 'backspace': + case 'delete': + return isMac ? '⌫' : 'Delete'; + case 'escape': + return 'Esc'; + case 'enter': + return isMac ? '↩' : 'Enter'; + case 'tab': + return isMac ? '⇥' : 'Tab'; + case 'arrowup': + return '↑'; + case 'arrowdown': + return '↓'; + case 'quote': + return "'"; + case 'period': + return '.'; + case 'slash': + return '/'; + case 'semicolon': + return ';'; + default: + if (lowerKey.startsWith('key')) return key.slice(-1); + if (lowerKey.startsWith('digit')) return key.slice(-1); + return key.toUpperCase(); + } } @@ -46,8 +59,8 @@ {$i18n.t(shortcut.name)} {/if}
-
- {#each shortcut.keys as key} +
+ {#each shortcut.keys.filter(key => !(key.toLowerCase() === 'delete' && shortcut.keys.includes('Backspace'))) as key}
diff --git a/src/lib/components/chat/ShortcutsModal.svelte b/src/lib/components/chat/ShortcutsModal.svelte index 12161e75f5..d5a28b46f4 100644 --- a/src/lib/components/chat/ShortcutsModal.svelte +++ b/src/lib/components/chat/ShortcutsModal.svelte @@ -2,6 +2,7 @@ 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'; @@ -21,7 +22,15 @@ onMount(() => { isMac = /Mac/i.test(navigator.userAgent); - const allShortcuts = Object.values(shortcuts); + }); + + $: { + const allShortcuts = Object.values(shortcuts).filter((shortcut) => { + if (!shortcut.setting) { + return true; + } + return $settings[shortcut.setting.id] === shortcut.setting.value; + }); const result = allShortcuts.reduce((acc, shortcut) => { const category = shortcut.category; @@ -32,20 +41,22 @@ return acc; }, {}); + const newCategorizedShortcuts = {}; for (const category in result) { const half = Math.ceil(result[category].length / 2); - categorizedShortcuts[category] = { + newCategorizedShortcuts[category] = { left: result[category].slice(0, half), right: result[category].slice(half) }; } - }); + categorizedShortcuts = newCategorizedShortcuts; + }
-
{$i18n.t('Shortcuts')}
+
{$i18n.t('Keyboard Shortcuts')}
diff --git a/src/lib/components/common/HotkeyHint.svelte b/src/lib/components/common/HotkeyHint.svelte index 1246630cbd..4dd8bdaa0b 100644 --- a/src/lib/components/common/HotkeyHint.svelte +++ b/src/lib/components/common/HotkeyHint.svelte @@ -17,10 +17,6 @@ mounted = true; }); - $: if (name === 'newChat' || name === 'search') { - isVisible = $settings?.showSidebarHotkeyHints !== false; - } - function formatKey(key: string): string { const lowerKey = key.toLowerCase(); diff --git a/src/lib/shortcuts.ts b/src/lib/shortcuts.ts index 44eee95d6c..520e00eecf 100644 --- a/src/lib/shortcuts.ts +++ b/src/lib/shortcuts.ts @@ -1,9 +1,13 @@ type ShortcutRegistry = { - [key in Shortcut]: { + [key in Shortcut]?: { name: string; keys: string[]; category: string; tooltip?: string; + setting?: { + id: string; + value: any; + }; }; }; @@ -12,7 +16,6 @@ export enum Shortcut { NEW_CHAT = 'newChat', NEW_TEMPORARY_CHAT = 'newTemporaryChat', DELETE_CHAT = 'deleteChat', - GENERATE_PROMPT_PAIR = 'generatePromptPair', //Global SEARCH = 'search', @@ -26,11 +29,15 @@ export enum Shortcut { ACCEPT_AUTOCOMPLETE = 'acceptAutocomplete', PREVENT_FILE_CREATION = 'preventFileCreation', NAVIGATE_PROMPT_HISTORY_UP = 'navigatePromptHistoryUp', + SEND_MESSAGE_NORMAL = 'sendMessageNormal', + SEND_MESSAGE_MOD = 'sendMessageMod', ATTACH_FILE = 'attachFile', ADD_PROMPT = 'addPrompt', TALK_TO_MODEL = 'talkToModel', //Message + GENERATE_MESSAGE_PAIR = 'generateMessagePair', + REGENERATE_RESPONSE = 'regenerateResponse', COPY_LAST_CODE_BLOCK = 'copyLastCodeBlock', COPY_LAST_RESPONSE = 'copyLastResponse', STOP_GENERATING = 'stopGenerating' @@ -50,15 +57,9 @@ export const shortcuts: ShortcutRegistry = { }, [Shortcut.DELETE_CHAT]: { name: 'Delete Chat', - keys: ['mod', 'shift', 'Backspace'], + keys: ['mod', 'shift', 'Backspace', 'Delete'], category: 'Chat' }, - [Shortcut.GENERATE_PROMPT_PAIR]: { - name: 'Generate Prompt Pair', - keys: ['mod', 'shift', 'Enter'], - category: 'Chat', - tooltip: 'Only active when the chat input is in focus and an LLM is generating a response.' - }, //Global [Shortcut.SEARCH]: { @@ -89,7 +90,7 @@ export const shortcuts: ShortcutRegistry = { //Input [Shortcut.FOCUS_INPUT]: { - name: 'Focus Text Area', + name: 'Focus Chat Input', keys: ['shift', 'Escape'], category: 'Input' }, @@ -104,11 +105,25 @@ export const shortcuts: ShortcutRegistry = { category: 'Input', tooltip: 'Only active when "Paste Large Text as File" setting is toggled on.' }, - [Shortcut.NAVIGATE_PROMPT_HISTORY_UP]: { - name: 'Edit Last Message', - keys: ['ArrowUp'], + [Shortcut.SEND_MESSAGE_NORMAL]: { + name: 'Send Message', + keys: ['Enter'], category: 'Input', - tooltip: 'Only can be triggered when the chat input is in focus.' + tooltip: 'The behavior of this shortcut is determined by the "Enter Key Behavior" setting.', + setting: { + id: 'ctrlEnterToSend', + value: false + } + }, + [Shortcut.SEND_MESSAGE_MOD]: { + name: 'Send Message', + keys: ['mod', 'Enter'], + category: 'Input', + tooltip: 'The behavior of this shortcut is determined by the "Enter Key Behavior" setting.', + setting: { + id: 'ctrlEnterToSend', + value: true + } }, [Shortcut.ATTACH_FILE]: { name: 'Attach File From Knowledge', @@ -127,14 +142,15 @@ export const shortcuts: ShortcutRegistry = { }, //Message - [Shortcut.COPY_LAST_CODE_BLOCK]: { - name: 'Copy Last Code Block', - keys: ['mod', 'shift', 'Semicolon'], - category: 'Message' + [Shortcut.GENERATE_MESSAGE_PAIR]: { + name: 'Generate Message Pair', + keys: ['mod', 'shift', 'Enter'], + category: 'Message', + tooltip: 'Only active when the chat input is in focus.' }, - [Shortcut.COPY_LAST_RESPONSE]: { - name: 'Copy Last Response', - keys: ['mod', 'shift', 'KeyC'], + [Shortcut.REGENERATE_RESPONSE]: { + name: 'Regenerate Response', + keys: ['mod', 'KeyR'], category: 'Message' }, [Shortcut.STOP_GENERATING]: { @@ -142,5 +158,21 @@ export const shortcuts: ShortcutRegistry = { keys: ['Escape'], category: 'Message', tooltip: 'Only active when the chat input is in focus and an LLM is generating a response.' + }, + [Shortcut.NAVIGATE_PROMPT_HISTORY_UP]: { + name: 'Edit Last Message', + keys: ['ArrowUp'], + category: 'Message', + tooltip: 'Only can be triggered when the chat input is in focus.' + }, + [Shortcut.COPY_LAST_RESPONSE]: { + name: 'Copy Last Response', + keys: ['mod', 'shift', 'KeyC'], + category: 'Message' + }, + [Shortcut.COPY_LAST_CODE_BLOCK]: { + name: 'Copy Last Code Block', + keys: ['mod', 'shift', 'Semicolon'], + category: 'Message' } }; \ No newline at end of file diff --git a/src/lib/stores/index.ts b/src/lib/stores/index.ts index 9fab67df4b..de37963adb 100644 --- a/src/lib/stores/index.ts +++ b/src/lib/stores/index.ts @@ -187,7 +187,6 @@ type Settings = { highContrastMode?: boolean; title?: TitleSettings; showChatTitleInTab?: boolean; - showSidebarHotkeyHints?: boolean; splitLargeDeltas?: boolean; chatDirection?: 'LTR' | 'RTL' | 'auto'; ctrlEnterToSend?: boolean; diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte index 979a9ca3ca..b17f972860 100644 --- a/src/routes/(app)/+layout.svelte +++ b/src/routes/(app)/+layout.svelte @@ -247,7 +247,7 @@ const setupKeyboardShortcuts = () => { showShortcuts.set(false); break; case Shortcut.NEW_TEMPORARY_CHAT: - event.preventDefault(); + event.preventDefault(); if ($user?.role !== 'admin' && $user?.permissions?.chat?.temporary_enforced) { temporaryChatEnabled.set(true); } else { @@ -258,14 +258,20 @@ const setupKeyboardShortcuts = () => { document.getElementById('new-chat-button')?.click(); }, 0); break; - case Shortcut.GENERATE_PROMPT_PAIR: - // Placeholder for future implementation + case Shortcut.GENERATE_MESSAGE_PAIR: + event.preventDefault(); + document.getElementById('generate-message-pair-button')?.click(); + break; + + case Shortcut.REGENERATE_RESPONSE: + event.preventDefault(); + [...document.getElementsByClassName('regenerate-response-button')]?.at(-1)?.click(); break; case Shortcut.STOP_GENERATING: // Placeholder for future implementation break; case Shortcut.PREVENT_FILE_CREATION: - // This shortcut is handled by the paste event in MessageInput.svelte + // This shortcut is handled by the paste event in MessageInput.svelte break; } }