This commit is contained in:
silentoplayz 2025-10-18 00:32:03 -04:00
parent e361606c61
commit 3c7e739b3c
8 changed files with 119 additions and 95 deletions

View file

@ -1051,6 +1051,12 @@
dispatch('submit', prompt); dispatch('submit', prompt);
}} }}
> >
<button
id="generate-message-pair-button"
class="hidden"
on:click={() => createMessagePair(prompt)}
/>
<div <div
id="message-input-container" id="message-input-container"
class="flex-1 flex flex-col relative w-full shadow-lg rounded-3xl border {$temporaryChatEnabled class="flex-1 flex flex-col relative w-full shadow-lg rounded-3xl border {$temporaryChatEnabled
@ -1255,24 +1261,6 @@
stopResponse(); stopResponse();
} }
// Command/Ctrl + Shift + Enter to submit a message pair
if (isCtrlPressed && e.key === 'Enter' && e.shiftKey) {
e.preventDefault();
createMessagePair(prompt);
}
// Check if Ctrl + R is pressed
if (prompt === '' && isCtrlPressed && e.key.toLowerCase() === 'r') {
e.preventDefault();
console.log('regenerate');
const regenerateButton = [
...document.getElementsByClassName('regenerate-response-button')
]?.at(-1);
regenerateButton?.click();
}
if (prompt === '' && e.key == 'ArrowUp') { if (prompt === '' && e.key == 'ArrowUp') {
e.preventDefault(); e.preventDefault();

View file

@ -67,7 +67,6 @@
let collapseCodeBlocks = false; let collapseCodeBlocks = false;
let expandDetails = false; let expandDetails = false;
let showChatTitleInTab = true; let showChatTitleInTab = true;
let showSidebarHotkeyHints = true;
let showFloatingActionButtons = true; let showFloatingActionButtons = true;
let floatingActionButtons = null; let floatingActionButtons = null;
@ -227,7 +226,6 @@
chatDirection = $settings?.chatDirection ?? 'auto'; chatDirection = $settings?.chatDirection ?? 'auto';
userLocation = $settings?.userLocation ?? false; userLocation = $settings?.userLocation ?? false;
showChatTitleInTab = $settings?.showChatTitleInTab ?? true; showChatTitleInTab = $settings?.showChatTitleInTab ?? true;
showSidebarHotkeyHints = $settings?.showSidebarHotkeyHints ?? true;
notificationSound = $settings?.notificationSound ?? true; notificationSound = $settings?.notificationSound ?? true;
notificationSoundAlways = $settings?.notificationSoundAlways ?? false; notificationSoundAlways = $settings?.notificationSoundAlways ?? false;
@ -352,25 +350,6 @@
</div> </div>
</div> </div>
<div>
<div class=" py-0.5 flex w-full justify-between">
<div id="show-sidebar-hotkey-hints-label" class=" self-center text-xs">
{$i18n.t('Show Sidebar Hotkey Hints')}
</div>
<div class="flex items-center gap-2 p-1">
<Switch
ariaLabelledbyId="show-sidebar-hotkey-hints-label"
tooltip={true}
bind:state={showSidebarHotkeyHints}
on:change={() => {
saveSettings({ showSidebarHotkeyHints });
}}
/>
</div>
</div>
</div>
<div> <div>
<div class="py-0.5 flex w-full justify-between"> <div class="py-0.5 flex w-full justify-between">
<div id="notification-sound-label" class=" self-center text-xs"> <div id="notification-sound-label" class=" self-center text-xs">

View file

@ -11,26 +11,39 @@
function formatKey(key: string): string { function formatKey(key: string): string {
const lowerKey = key.toLowerCase(); const lowerKey = key.toLowerCase();
if (lowerKey === 'mod') return isMac ? '⌘' : 'Ctrl/⌘'; switch (lowerKey) {
if (lowerKey === 'shift') return isMac ? '⇧' : 'Shift'; case 'mod':
if (lowerKey === 'backspace') return '⌫/Delete'; return isMac ? '⌘' : 'Ctrl';
if (lowerKey === 'escape') return 'Esc'; case 'shift':
if (lowerKey === 'enter') return 'Enter'; return isMac ? '⇧' : 'Shift';
if (lowerKey === 'tab') return 'Tab'; case 'alt':
if (lowerKey === 'arrowup') return '↑'; return isMac ? '⌥' : 'Alt';
if (lowerKey === 'arrowdown') return '↓'; case 'backspace':
case 'delete':
// For keys like 'KeyK', 'KeyO', etc., we just want the last character. return isMac ? '⌫' : 'Delete';
if (lowerKey.startsWith('key')) return key.slice(-1); case 'escape':
// For keys like 'Digit2', 'Digit3', etc. return 'Esc';
if (lowerKey.startsWith('digit')) return key.slice(-1); case 'enter':
// For other special keys return isMac ? '↩' : 'Enter';
if (lowerKey === 'quote') return "'"; case 'tab':
if (lowerKey === 'period') return '.'; return isMac ? '⇥' : 'Tab';
if (lowerKey === 'slash') return '/'; case 'arrowup':
if (lowerKey === 'semicolon') return ';'; return '↑';
case 'arrowdown':
return key.toUpperCase(); 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();
}
} }
</script> </script>
@ -46,8 +59,8 @@
{$i18n.t(shortcut.name)} {$i18n.t(shortcut.name)}
{/if} {/if}
</div> </div>
<div class="flex-shrink-0 flex items-center self-center space-x-1 text-xs"> <div class="flex-shrink-0 flex flex-wrap justify-end items-center self-center space-x-1 text-xs">
{#each shortcut.keys as key} {#each shortcut.keys.filter(key => !(key.toLowerCase() === 'delete' && shortcut.keys.includes('Backspace'))) as key}
<div <div
class="h-fit py-1 px-2 flex items-center justify-center rounded-sm border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300" class="h-fit py-1 px-2 flex items-center justify-center rounded-sm border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300"
> >

View file

@ -2,6 +2,7 @@
import { getContext, onMount } from 'svelte'; import { getContext, onMount } from 'svelte';
import Modal from '../common/Modal.svelte'; import Modal from '../common/Modal.svelte';
import { shortcuts } from '$lib/shortcuts'; import { shortcuts } from '$lib/shortcuts';
import { settings } from '$lib/stores';
import ShortcutItem from './ShortcutItem.svelte'; import ShortcutItem from './ShortcutItem.svelte';
import XMark from '$lib/components/icons/XMark.svelte'; import XMark from '$lib/components/icons/XMark.svelte';
@ -21,7 +22,15 @@
onMount(() => { onMount(() => {
isMac = /Mac/i.test(navigator.userAgent); 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 result = allShortcuts.reduce((acc, shortcut) => {
const category = shortcut.category; const category = shortcut.category;
@ -32,20 +41,22 @@
return acc; return acc;
}, {}); }, {});
const newCategorizedShortcuts = {};
for (const category in result) { for (const category in result) {
const half = Math.ceil(result[category].length / 2); const half = Math.ceil(result[category].length / 2);
categorizedShortcuts[category] = { newCategorizedShortcuts[category] = {
left: result[category].slice(0, half), left: result[category].slice(0, half),
right: result[category].slice(half) right: result[category].slice(half)
}; };
} }
}); categorizedShortcuts = newCategorizedShortcuts;
}
</script> </script>
<Modal bind:show> <Modal bind:show>
<div class="text-gray-700 dark:text-gray-100"> <div class="text-gray-700 dark:text-gray-100">
<div class="flex justify-between dark:text-gray-300 px-5 pt-4"> <div class="flex justify-between dark:text-gray-300 px-5 pt-4">
<div class="text-lg font-medium self-center">{$i18n.t('Shortcuts')}</div> <div class="text-lg font-medium self-center">{$i18n.t('Keyboard Shortcuts')}</div>
<button class="self-center" on:click={() => (show = false)}> <button class="self-center" on:click={() => (show = false)}>
<XMark className={'size-5'} /> <XMark className={'size-5'} />
</button> </button>

View file

@ -17,10 +17,6 @@
mounted = true; mounted = true;
}); });
$: if (name === 'newChat' || name === 'search') {
isVisible = $settings?.showSidebarHotkeyHints !== false;
}
function formatKey(key: string): string { function formatKey(key: string): string {
const lowerKey = key.toLowerCase(); const lowerKey = key.toLowerCase();

View file

@ -1,9 +1,13 @@
type ShortcutRegistry = { type ShortcutRegistry = {
[key in Shortcut]: { [key in Shortcut]?: {
name: string; name: string;
keys: string[]; keys: string[];
category: string; category: string;
tooltip?: string; tooltip?: string;
setting?: {
id: string;
value: any;
};
}; };
}; };
@ -12,7 +16,6 @@ export enum Shortcut {
NEW_CHAT = 'newChat', NEW_CHAT = 'newChat',
NEW_TEMPORARY_CHAT = 'newTemporaryChat', NEW_TEMPORARY_CHAT = 'newTemporaryChat',
DELETE_CHAT = 'deleteChat', DELETE_CHAT = 'deleteChat',
GENERATE_PROMPT_PAIR = 'generatePromptPair',
//Global //Global
SEARCH = 'search', SEARCH = 'search',
@ -26,11 +29,15 @@ export enum Shortcut {
ACCEPT_AUTOCOMPLETE = 'acceptAutocomplete', ACCEPT_AUTOCOMPLETE = 'acceptAutocomplete',
PREVENT_FILE_CREATION = 'preventFileCreation', PREVENT_FILE_CREATION = 'preventFileCreation',
NAVIGATE_PROMPT_HISTORY_UP = 'navigatePromptHistoryUp', NAVIGATE_PROMPT_HISTORY_UP = 'navigatePromptHistoryUp',
SEND_MESSAGE_NORMAL = 'sendMessageNormal',
SEND_MESSAGE_MOD = 'sendMessageMod',
ATTACH_FILE = 'attachFile', ATTACH_FILE = 'attachFile',
ADD_PROMPT = 'addPrompt', ADD_PROMPT = 'addPrompt',
TALK_TO_MODEL = 'talkToModel', TALK_TO_MODEL = 'talkToModel',
//Message //Message
GENERATE_MESSAGE_PAIR = 'generateMessagePair',
REGENERATE_RESPONSE = 'regenerateResponse',
COPY_LAST_CODE_BLOCK = 'copyLastCodeBlock', COPY_LAST_CODE_BLOCK = 'copyLastCodeBlock',
COPY_LAST_RESPONSE = 'copyLastResponse', COPY_LAST_RESPONSE = 'copyLastResponse',
STOP_GENERATING = 'stopGenerating' STOP_GENERATING = 'stopGenerating'
@ -50,15 +57,9 @@ export const shortcuts: ShortcutRegistry = {
}, },
[Shortcut.DELETE_CHAT]: { [Shortcut.DELETE_CHAT]: {
name: 'Delete Chat', name: 'Delete Chat',
keys: ['mod', 'shift', 'Backspace'], keys: ['mod', 'shift', 'Backspace', 'Delete'],
category: 'Chat' 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 //Global
[Shortcut.SEARCH]: { [Shortcut.SEARCH]: {
@ -89,7 +90,7 @@ export const shortcuts: ShortcutRegistry = {
//Input //Input
[Shortcut.FOCUS_INPUT]: { [Shortcut.FOCUS_INPUT]: {
name: 'Focus Text Area', name: 'Focus Chat Input',
keys: ['shift', 'Escape'], keys: ['shift', 'Escape'],
category: 'Input' category: 'Input'
}, },
@ -104,11 +105,25 @@ export const shortcuts: ShortcutRegistry = {
category: 'Input', category: 'Input',
tooltip: 'Only active when "Paste Large Text as File" setting is toggled on.' tooltip: 'Only active when "Paste Large Text as File" setting is toggled on.'
}, },
[Shortcut.NAVIGATE_PROMPT_HISTORY_UP]: { [Shortcut.SEND_MESSAGE_NORMAL]: {
name: 'Edit Last Message', name: 'Send Message',
keys: ['ArrowUp'], keys: ['Enter'],
category: 'Input', 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]: { [Shortcut.ATTACH_FILE]: {
name: 'Attach File From Knowledge', name: 'Attach File From Knowledge',
@ -127,14 +142,15 @@ export const shortcuts: ShortcutRegistry = {
}, },
//Message //Message
[Shortcut.COPY_LAST_CODE_BLOCK]: { [Shortcut.GENERATE_MESSAGE_PAIR]: {
name: 'Copy Last Code Block', name: 'Generate Message Pair',
keys: ['mod', 'shift', 'Semicolon'], keys: ['mod', 'shift', 'Enter'],
category: 'Message' category: 'Message',
tooltip: 'Only active when the chat input is in focus.'
}, },
[Shortcut.COPY_LAST_RESPONSE]: { [Shortcut.REGENERATE_RESPONSE]: {
name: 'Copy Last Response', name: 'Regenerate Response',
keys: ['mod', 'shift', 'KeyC'], keys: ['mod', 'KeyR'],
category: 'Message' category: 'Message'
}, },
[Shortcut.STOP_GENERATING]: { [Shortcut.STOP_GENERATING]: {
@ -142,5 +158,21 @@ export const shortcuts: ShortcutRegistry = {
keys: ['Escape'], keys: ['Escape'],
category: 'Message', category: 'Message',
tooltip: 'Only active when the chat input is in focus and an LLM is generating a response.' 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'
} }
}; };

View file

@ -187,7 +187,6 @@ type Settings = {
highContrastMode?: boolean; highContrastMode?: boolean;
title?: TitleSettings; title?: TitleSettings;
showChatTitleInTab?: boolean; showChatTitleInTab?: boolean;
showSidebarHotkeyHints?: boolean;
splitLargeDeltas?: boolean; splitLargeDeltas?: boolean;
chatDirection?: 'LTR' | 'RTL' | 'auto'; chatDirection?: 'LTR' | 'RTL' | 'auto';
ctrlEnterToSend?: boolean; ctrlEnterToSend?: boolean;

View file

@ -247,7 +247,7 @@ const setupKeyboardShortcuts = () => {
showShortcuts.set(false); showShortcuts.set(false);
break; break;
case Shortcut.NEW_TEMPORARY_CHAT: case Shortcut.NEW_TEMPORARY_CHAT:
event.preventDefault(); event.preventDefault();
if ($user?.role !== 'admin' && $user?.permissions?.chat?.temporary_enforced) { if ($user?.role !== 'admin' && $user?.permissions?.chat?.temporary_enforced) {
temporaryChatEnabled.set(true); temporaryChatEnabled.set(true);
} else { } else {
@ -258,14 +258,20 @@ const setupKeyboardShortcuts = () => {
document.getElementById('new-chat-button')?.click(); document.getElementById('new-chat-button')?.click();
}, 0); }, 0);
break; break;
case Shortcut.GENERATE_PROMPT_PAIR: case Shortcut.GENERATE_MESSAGE_PAIR:
// Placeholder for future implementation 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; break;
case Shortcut.STOP_GENERATING: case Shortcut.STOP_GENERATING:
// Placeholder for future implementation // Placeholder for future implementation
break; break;
case Shortcut.PREVENT_FILE_CREATION: 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; break;
} }
} }