diff --git a/src/lib/components/chat/MessageInput.svelte b/src/lib/components/chat/MessageInput.svelte index 562603cd47..232a26a3a3 100644 --- a/src/lib/components/chat/MessageInput.svelte +++ b/src/lib/components/chat/MessageInput.svelte @@ -1050,6 +1050,12 @@ dispatch('submit', prompt); }} > + -
-
-
-
-
{$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..4dd8bdaa0b --- /dev/null +++ b/src/lib/components/common/HotkeyHint.svelte @@ -0,0 +1,37 @@ + + +{#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..520e00eecf --- /dev/null +++ b/src/lib/shortcuts.ts @@ -0,0 +1,178 @@ +type ShortcutRegistry = { + [key in Shortcut]?: { + name: string; + keys: string[]; + category: string; + tooltip?: string; + setting?: { + id: string; + value: any; + }; + }; +}; + +export enum Shortcut { + //Chat + NEW_CHAT = 'newChat', + NEW_TEMPORARY_CHAT = 'newTemporaryChat', + DELETE_CHAT = 'deleteChat', + + //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', + 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' +} + +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', 'Delete'], + category: 'Chat' + }, + + //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 Chat Input', + 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.SEND_MESSAGE_NORMAL]: { + name: 'Send Message', + keys: ['Enter'], + category: 'Input', + 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', + 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.GENERATE_MESSAGE_PAIR]: { + name: 'Generate Message Pair', + keys: ['mod', 'shift', 'Enter'], + category: 'Message', + tooltip: 'Only active when the chat input is in focus.' + }, + [Shortcut.REGENERATE_RESPONSE]: { + name: 'Regenerate Response', + keys: ['mod', 'KeyR'], + 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.' + }, + [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/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte index f9a8470465..b17f972860 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,120 @@ }) ]); - 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_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 + 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 +461,4 @@ cursor: pointer; background-color: #bcbabb; } - + \ No newline at end of file