feat: configurable send shortcut (Enter vs Ctrl+Enter)
Add toggle in chat input to switch between Enter-to-send (default) and Ctrl+Enter-to-send modes. Preference persists in localStorage. Keyboard shortcuts modal reflects the current setting.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { useState, useRef, useEffect, useCallback, lazy, Suspense } from 'react';
|
||||
import { Send, Square, Paperclip, X, FileText, Eye, EyeOff } from 'lucide-react';
|
||||
import { useT } from '../hooks/useLocale';
|
||||
import { useSendShortcut } from '../hooks/useSendShortcut';
|
||||
|
||||
const ReactMarkdown = lazy(() => import('react-markdown'));
|
||||
const remarkGfm = import('remark-gfm').then(m => m.default);
|
||||
@@ -87,6 +88,7 @@ function formatSize(bytes: number): string {
|
||||
|
||||
export function ChatInput({ onSend, onAbort, isGenerating, disabled, sessionKey }: Props) {
|
||||
const t = useT();
|
||||
const { sendOnEnter, toggle: toggleSendShortcut } = useSendShortcut();
|
||||
const [text, setText] = useState('');
|
||||
const [files, setFiles] = useState<FileAttachment[]>([]);
|
||||
const [isDragOver, setIsDragOver] = useState(false);
|
||||
@@ -179,9 +181,20 @@ export function ChatInput({ onSend, onAbort, isGenerating, disabled, sessionKey
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSubmit();
|
||||
if (e.key === 'Enter') {
|
||||
if (sendOnEnter) {
|
||||
// Enter sends, Shift+Enter for newline
|
||||
if (!e.shiftKey && !e.ctrlKey && !e.metaKey) {
|
||||
e.preventDefault();
|
||||
handleSubmit();
|
||||
}
|
||||
} else {
|
||||
// Ctrl+Enter (or Cmd+Enter on Mac) sends, Enter for newline
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
e.preventDefault();
|
||||
handleSubmit();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -305,6 +318,14 @@ export function ChatInput({ onSend, onAbort, isGenerating, disabled, sessionKey
|
||||
rows={1}
|
||||
className="flex-1 bg-transparent resize-none rounded-2xl border border-pc-border bg-pc-input/35 px-4 py-3 text-sm text-pc-text placeholder:text-pc-text-muted outline-none transition-all max-h-[200px]"
|
||||
/>
|
||||
{/* Send shortcut toggle */}
|
||||
<button
|
||||
onClick={toggleSendShortcut}
|
||||
className="hidden sm:flex shrink-0 h-7 rounded-xl border border-pc-border bg-pc-elevated/30 px-2 items-center gap-1 text-[10px] text-pc-text-muted hover:text-pc-text-secondary hover:bg-[var(--pc-hover)] transition-colors"
|
||||
title={`${t('settings.sendShortcut')}: ${sendOnEnter ? t('settings.sendEnter') : t('settings.sendCtrlEnter')}`}
|
||||
>
|
||||
<span>{sendOnEnter ? '↵' : (navigator.userAgent.includes('Mac') ? '⌘↵' : 'Ctrl↵')}</span>
|
||||
</button>
|
||||
{isGenerating ? (
|
||||
<button
|
||||
onClick={onAbort}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect } from 'react';
|
||||
import { X, Keyboard } from 'lucide-react';
|
||||
import { useT } from '../hooks/useLocale';
|
||||
import { useSendShortcut } from '../hooks/useSendShortcut';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
@@ -37,6 +38,7 @@ const mod = isMac ? '⌘' : 'Ctrl';
|
||||
|
||||
export function KeyboardShortcuts({ open, onClose }: Props) {
|
||||
const t = useT();
|
||||
const { sendOnEnter } = useSendShortcut();
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
@@ -79,11 +81,11 @@ export function KeyboardShortcuts({ open, onClose }: Props) {
|
||||
<div className="pb-3">
|
||||
<SectionTitle>{t('shortcuts.chatSection')}</SectionTitle>
|
||||
<ShortcutRow
|
||||
keys={<Kbd>Enter</Kbd>}
|
||||
keys={sendOnEnter ? <Kbd>Enter</Kbd> : <><Kbd>{mod}</Kbd><span className="text-pc-text-faint">+</span><Kbd>Enter</Kbd></>}
|
||||
label={t('shortcuts.send')}
|
||||
/>
|
||||
<ShortcutRow
|
||||
keys={<><Kbd>Shift</Kbd><span className="text-pc-text-faint">+</span><Kbd>Enter</Kbd></>}
|
||||
keys={sendOnEnter ? <><Kbd>Shift</Kbd><span className="text-pc-text-faint">+</span><Kbd>Enter</Kbd></> : <Kbd>Enter</Kbd>}
|
||||
label={t('shortcuts.newline')}
|
||||
/>
|
||||
<ShortcutRow
|
||||
|
||||
21
src/hooks/useSendShortcut.ts
Normal file
21
src/hooks/useSendShortcut.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
|
||||
const STORAGE_KEY = 'pinchchat-send-on-enter';
|
||||
|
||||
/** Hook to manage the send shortcut preference (Enter vs Ctrl+Enter). */
|
||||
export function useSendShortcut() {
|
||||
const [sendOnEnter, setSendOnEnter] = useState(() => {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
return stored === null ? true : stored === 'true';
|
||||
});
|
||||
|
||||
const toggle = useCallback(() => {
|
||||
setSendOnEnter(prev => {
|
||||
const next = !prev;
|
||||
localStorage.setItem(STORAGE_KEY, String(next));
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
return { sendOnEnter, toggle };
|
||||
}
|
||||
@@ -137,6 +137,11 @@ const en = {
|
||||
'search.prev': 'Previous match',
|
||||
'search.next': 'Next match',
|
||||
'shortcuts.searchMessages': 'Search messages',
|
||||
|
||||
// Send shortcut setting
|
||||
'settings.sendShortcut': 'Send with',
|
||||
'settings.sendEnter': 'Enter',
|
||||
'settings.sendCtrlEnter': 'Ctrl+Enter',
|
||||
} as const;
|
||||
|
||||
const fr: Record<keyof typeof en, string> = {
|
||||
@@ -255,6 +260,10 @@ const fr: Record<keyof typeof en, string> = {
|
||||
'search.prev': 'Résultat précédent',
|
||||
'search.next': 'Résultat suivant',
|
||||
'shortcuts.searchMessages': 'Rechercher dans les messages',
|
||||
|
||||
'settings.sendShortcut': 'Envoyer avec',
|
||||
'settings.sendEnter': 'Entrée',
|
||||
'settings.sendCtrlEnter': 'Ctrl+Entrée',
|
||||
};
|
||||
|
||||
export type TranslationKey = keyof typeof en;
|
||||
|
||||
Reference in New Issue
Block a user