From 37dac7f6ea2aca72fd234533e9c11316873e37b8 Mon Sep 17 00:00:00 2001 From: fullex <106392080+0xfullex@users.noreply.github.com> Date: Tue, 17 Jun 2025 14:38:05 +0800 Subject: [PATCH] fix: unified the behavior of SendMessage shortcut (#7276) --- src/renderer/src/i18n/locales/en-us.json | 2 +- src/renderer/src/i18n/locales/ja-jp.json | 2 +- src/renderer/src/i18n/locales/ru-ru.json | 2 +- src/renderer/src/i18n/locales/zh-cn.json | 2 +- src/renderer/src/i18n/locales/zh-tw.json | 2 +- .../src/pages/home/Inputbar/Inputbar.tsx | 60 +++++++++++-------- .../src/pages/home/Messages/MessageEditor.tsx | 46 ++++++++------ .../src/pages/home/Tabs/SettingsTab.tsx | 18 +++--- src/renderer/src/store/settings.ts | 2 +- src/renderer/src/utils/input.ts | 46 ++++++++++++++ 10 files changed, 120 insertions(+), 62 deletions(-) diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index fd2c5fbfa9..a9fcd9e713 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -183,7 +183,7 @@ "input.new.context": "Clear Context {{Command}}", "input.new_topic": "New Topic {{Command}}", "input.pause": "Pause", - "input.placeholder": "Type your message here...", + "input.placeholder": "Type your message here, press {{key}} to send...", "input.send": "Send", "input.settings": "Settings", "input.topics": " Topics ", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 5462f40a84..ea826b7027 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -183,7 +183,7 @@ "input.new.context": "コンテキストをクリア {{Command}}", "input.new_topic": "新しいトピック {{Command}}", "input.pause": "一時停止", - "input.placeholder": "ここにメッセージを入力...", + "input.placeholder": "ここにメッセージを入力し、{{key}} を押して送信...", "input.send": "送信", "input.settings": "設定", "input.topics": " トピック ", diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index c4e96a5604..d0d1422b40 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -183,7 +183,7 @@ "input.new.context": "Очистить контекст {{Command}}", "input.new_topic": "Новый топик {{Command}}", "input.pause": "Остановить", - "input.placeholder": "Введите ваше сообщение здесь...", + "input.placeholder": "Введите ваше сообщение здесь, нажмите {{key}} для отправки...", "input.send": "Отправить", "input.settings": "Настройки", "input.topics": " Топики ", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 02a368b6e6..340a23a652 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -183,7 +183,7 @@ "input.new.context": "清除上下文 {{Command}}", "input.new_topic": "新话题 {{Command}}", "input.pause": "暂停", - "input.placeholder": "在这里输入消息...", + "input.placeholder": "在这里输入消息,按 {{key}} 发送...", "input.translating": "翻译中...", "input.send": "发送", "input.settings": "设置", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 7700c98b4a..3ed39f5311 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -183,7 +183,7 @@ "input.new.context": "清除上下文 {{Command}}", "input.new_topic": "新話題 {{Command}}", "input.pause": "暫停", - "input.placeholder": "在此輸入您的訊息...", + "input.placeholder": "在此輸入您的訊息,按 {{key}} 傳送...", "input.send": "傳送", "input.settings": "設定", "input.topics": " 話題 ", diff --git a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx index 5ce517624e..a0de1fb58e 100644 --- a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx +++ b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx @@ -36,6 +36,7 @@ import type { MessageInputBaseParams } from '@renderer/types/newMessage' import { classNames, delay, formatFileSize, getFileExtension } from '@renderer/utils' import { formatQuotedText } from '@renderer/utils/formats' import { getFilesFromDropEvent } from '@renderer/utils/input' +import { getSendMessageShortcutLabel, isSendMessageKeyPressed } from '@renderer/utils/input' import { documentExts, imageExts, textExts } from '@shared/config/constant' import { IpcChannel } from '@shared/IpcChannel' import { Button, Tooltip } from 'antd' @@ -309,8 +310,6 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = }, [knowledgeBases, openKnowledgeFileList, quickPanel, t, inputbarToolsRef]) const handleKeyDown = (event: React.KeyboardEvent) => { - const isEnterPressed = event.key === 'Enter' && !event.nativeEvent.isComposing - // 按下Tab键,自动选中${xxx} if (event.key === 'Tab' && inputFocus) { event.preventDefault() @@ -366,32 +365,37 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = } } - if (isEnterPressed && !event.shiftKey && sendMessageShortcut === 'Enter') { - if (quickPanel.isVisible) return event.preventDefault() + //to check if the SendMessage key is pressed + //other keys should be ignored + const isEnterPressed = event.key === 'Enter' && !event.nativeEvent.isComposing + if (isEnterPressed) { + if (isSendMessageKeyPressed(event, sendMessageShortcut)) { + if (quickPanel.isVisible) return event.preventDefault() + sendMessage() + return event.preventDefault() + } else { + //shift+enter's default behavior is to add a new line, ignore it + if (!event.shiftKey) { + event.preventDefault() - sendMessage() - return event.preventDefault() - } + const textArea = textareaRef.current?.resizableTextArea?.textArea + if (textArea) { + const start = textArea.selectionStart + const end = textArea.selectionEnd + const text = textArea.value + const newText = text.substring(0, start) + '\n' + text.substring(end) - if (sendMessageShortcut === 'Shift+Enter' && isEnterPressed && event.shiftKey) { - if (quickPanel.isVisible) return event.preventDefault() + // update text by setState, not directly modify textarea.value + setText(newText) - sendMessage() - return event.preventDefault() - } - - if (sendMessageShortcut === 'Ctrl+Enter' && isEnterPressed && event.ctrlKey) { - if (quickPanel.isVisible) return event.preventDefault() - - sendMessage() - return event.preventDefault() - } - - if (sendMessageShortcut === 'Command+Enter' && isEnterPressed && event.metaKey) { - if (quickPanel.isVisible) return event.preventDefault() - - sendMessage() - return event.preventDefault() + // set cursor position in the next render cycle + setTimeout(() => { + textArea.selectionStart = textArea.selectionEnd = start + 1 + onInput() // trigger resizeTextArea + }, 0) + } + } + } } if (enableBackspaceDeleteModel && event.key === 'Backspace' && text.trim() === '' && mentionModels.length > 0) { @@ -798,7 +802,11 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = value={text} onChange={onChange} onKeyDown={handleKeyDown} - placeholder={isTranslating ? t('chat.input.translating') : t('chat.input.placeholder')} + placeholder={ + isTranslating + ? t('chat.input.translating') + : t('chat.input.placeholder', { key: getSendMessageShortcutLabel(sendMessageShortcut) }) + } autoFocus contextMenu="true" variant="borderless" diff --git a/src/renderer/src/pages/home/Messages/MessageEditor.tsx b/src/renderer/src/pages/home/Messages/MessageEditor.tsx index 5dadcad5b2..895eb787d2 100644 --- a/src/renderer/src/pages/home/Messages/MessageEditor.tsx +++ b/src/renderer/src/pages/home/Messages/MessageEditor.tsx @@ -8,7 +8,7 @@ import PasteService from '@renderer/services/PasteService' import { FileType, FileTypes } from '@renderer/types' import { Message, MessageBlock, MessageBlockStatus, MessageBlockType } from '@renderer/types/newMessage' import { classNames, getFileExtension } from '@renderer/utils' -import { getFilesFromDropEvent } from '@renderer/utils/input' +import { getFilesFromDropEvent, isSendMessageKeyPressed } from '@renderer/utils/input' import { createFileBlock, createImageBlock } from '@renderer/utils/messageUtils/create' import { findAllBlocks } from '@renderer/utils/messageUtils/find' import { documentExts, imageExts, textExts } from '@shared/config/constant' @@ -169,31 +169,39 @@ const MessageBlockEditor: FC = ({ message, onSave, onResend, onCancel }) onResend(updatedBlocks) } - const handleKeyDown = (event: React.KeyboardEvent) => { + const handleKeyDown = (event: React.KeyboardEvent, blockId: string) => { if (message.role !== 'user') { return } + // keep the same enter behavior as inputbar const isEnterPressed = event.key === 'Enter' && !event.nativeEvent.isComposing + if (isEnterPressed) { + if (isSendMessageKeyPressed(event, sendMessageShortcut)) { + handleResend() + return event.preventDefault() + } else { + if (!event.shiftKey) { + event.preventDefault() - if (isEnterPressed && !event.shiftKey && sendMessageShortcut === 'Enter') { - handleResend() - return event.preventDefault() - } + const textArea = textareaRef.current?.resizableTextArea?.textArea + if (textArea) { + const start = textArea.selectionStart + const end = textArea.selectionEnd + const text = textArea.value + const newText = text.substring(0, start) + '\n' + text.substring(end) - if (sendMessageShortcut === 'Shift+Enter' && isEnterPressed && event.shiftKey) { - handleResend() - return event.preventDefault() - } + //same with onChange() + handleTextChange(blockId, newText) - if (sendMessageShortcut === 'Ctrl+Enter' && isEnterPressed && event.ctrlKey) { - handleResend() - return event.preventDefault() - } - - if (sendMessageShortcut === 'Command+Enter' && isEnterPressed && event.metaKey) { - handleResend() - return event.preventDefault() + // set cursor position in the next render cycle + setTimeout(() => { + textArea.selectionStart = textArea.selectionEnd = start + 1 + resizeTextArea() // trigger resizeTextArea + }, 0) + } + } + } } } @@ -212,7 +220,7 @@ const MessageBlockEditor: FC = ({ message, onSave, onResend, onCancel }) handleTextChange(block.id, e.target.value) resizeTextArea() }} - onKeyDown={handleKeyDown} + onKeyDown={(e) => handleKeyDown(e, block.id)} autoFocus contextMenu="true" spellCheck={false} diff --git a/src/renderer/src/pages/home/Tabs/SettingsTab.tsx b/src/renderer/src/pages/home/Tabs/SettingsTab.tsx index b06d3389f9..b3b7a844e7 100644 --- a/src/renderer/src/pages/home/Tabs/SettingsTab.tsx +++ b/src/renderer/src/pages/home/Tabs/SettingsTab.tsx @@ -1,13 +1,7 @@ import { CheckOutlined } from '@ant-design/icons' import { HStack } from '@renderer/components/Layout' import Scrollbar from '@renderer/components/Scrollbar' -import { - DEFAULT_CONTEXTCOUNT, - DEFAULT_MAX_TOKENS, - DEFAULT_TEMPERATURE, - isMac, - isWindows -} from '@renderer/config/constant' +import { DEFAULT_CONTEXTCOUNT, DEFAULT_MAX_TOKENS, DEFAULT_TEMPERATURE } from '@renderer/config/constant' import { isOpenAIModel, isSupportedFlexServiceTier, @@ -59,6 +53,7 @@ import { TranslateLanguageVarious } from '@renderer/types' import { modalConfirm } from '@renderer/utils' +import { getSendMessageShortcutLabel } from '@renderer/utils/input' import { Button, Col, InputNumber, Row, Select, Slider, Switch, Tooltip } from 'antd' import { CircleHelp, Settings2 } from 'lucide-react' import { FC, useCallback, useEffect, useMemo, useState } from 'react' @@ -670,10 +665,11 @@ const SettingsTab: FC = (props) => { value={sendMessageShortcut} menuItemSelectedIcon={} options={[ - { value: 'Enter', label: 'Enter' }, - { value: 'Shift+Enter', label: 'Shift + Enter' }, - { value: 'Ctrl+Enter', label: 'Ctrl + Enter' }, - { value: 'Command+Enter', label: `${isMac ? '⌘' : isWindows ? 'Win' : 'Super'} + Enter` } + { value: 'Enter', label: getSendMessageShortcutLabel('Enter') }, + { value: 'Ctrl+Enter', label: getSendMessageShortcutLabel('Ctrl+Enter') }, + { value: 'Alt+Enter', label: getSendMessageShortcutLabel('Alt+Enter') }, + { value: 'Command+Enter', label: getSendMessageShortcutLabel('Command+Enter') }, + { value: 'Shift+Enter', label: getSendMessageShortcutLabel('Shift+Enter') } ]} onChange={(value) => setSendMessageShortcut(value as SendMessageShortcut)} style={{ width: 135 }} diff --git a/src/renderer/src/store/settings.ts b/src/renderer/src/store/settings.ts index 5b35bc10d6..49cbee952c 100644 --- a/src/renderer/src/store/settings.ts +++ b/src/renderer/src/store/settings.ts @@ -14,7 +14,7 @@ import { import { WebDAVSyncState } from './backup' -export type SendMessageShortcut = 'Enter' | 'Shift+Enter' | 'Ctrl+Enter' | 'Command+Enter' +export type SendMessageShortcut = 'Enter' | 'Shift+Enter' | 'Ctrl+Enter' | 'Command+Enter' | 'Alt+Enter' export type SidebarIcon = 'assistants' | 'agents' | 'paintings' | 'translate' | 'minapp' | 'knowledge' | 'files' diff --git a/src/renderer/src/utils/input.ts b/src/renderer/src/utils/input.ts index 6952a24a51..649941072d 100644 --- a/src/renderer/src/utils/input.ts +++ b/src/renderer/src/utils/input.ts @@ -1,4 +1,6 @@ +import { isMac, isWindows } from '@renderer/config/constant' import Logger from '@renderer/config/logger' +import type { SendMessageShortcut } from '@renderer/store/settings' import { FileType } from '@renderer/types' export const getFilesFromDropEvent = async (e: React.DragEvent): Promise => { @@ -58,3 +60,47 @@ export const getFilesFromDropEvent = async (e: React.DragEvent): }) } } + +// convert send message shortcut to human readable label +export const getSendMessageShortcutLabel = (shortcut: SendMessageShortcut) => { + switch (shortcut) { + case 'Enter': + return 'Enter' + case 'Ctrl+Enter': + return 'Ctrl + Enter' + case 'Alt+Enter': + return `${isMac ? '⌥' : 'Alt'} + Enter` + case 'Command+Enter': + return `${isMac ? '⌘' : isWindows ? 'Win' : 'Super'} + Enter` + case 'Shift+Enter': + return 'Shift + Enter' + default: + return shortcut + } +} + +// check if the send message key is pressed in textarea +export const isSendMessageKeyPressed = ( + event: React.KeyboardEvent, + shortcut: SendMessageShortcut +) => { + let isSendMessageKeyPressed = false + switch (shortcut) { + case 'Enter': + if (!event.shiftKey && !event.ctrlKey && !event.metaKey && !event.altKey) isSendMessageKeyPressed = true + break + case 'Ctrl+Enter': + if (event.ctrlKey && !event.shiftKey && !event.metaKey && !event.altKey) isSendMessageKeyPressed = true + break + case 'Command+Enter': + if (event.metaKey && !event.shiftKey && !event.ctrlKey && !event.altKey) isSendMessageKeyPressed = true + break + case 'Alt+Enter': + if (event.altKey && !event.shiftKey && !event.ctrlKey && !event.metaKey) isSendMessageKeyPressed = true + break + case 'Shift+Enter': + if (event.shiftKey && !event.ctrlKey && !event.metaKey && !event.altKey) isSendMessageKeyPressed = true + break + } + return isSendMessageKeyPressed +}