+ {#each Object.entries(categorizedShortcuts) as [category, columns], i}
+ {#if i > 0}
+
-
-
{$i18n.t('Generate prompt pair')}
-
-
-
- Ctrl/⌘
-
-
-
- Shift
-
-
-
- Enter
-
-
+
+
+
+
+ {#each columns.left as shortcut}
+
+ {/each}
-
-
-
{$i18n.t('Toggle search')}
-
-
-
-
-
-
{$i18n.t('Toggle settings')}
-
-
-
-
-
-
{$i18n.t('Toggle sidebar')}
-
-
-
- Ctrl/⌘
-
-
-
- Shift
-
-
-
- S
-
-
-
-
-
-
{$i18n.t('Delete chat')}
-
-
-
- Ctrl/⌘
-
-
- Shift
-
-
-
- ⌫/Delete
-
-
-
-
-
-
{$i18n.t('Show shortcuts')}
-
-
+
+ {#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')}
-
-
-
-
-
-
-
+
\ 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}
+
+ {keys.map(formatKey).join(isMac ? '' : '+')}
+
+{/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 @@
-
-
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