fix: unified the behavior of SendMessage shortcut (#7276)

This commit is contained in:
fullex 2025-06-17 14:38:05 +08:00 committed by GitHub
parent 68c1a3e1cc
commit 37dac7f6ea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 120 additions and 62 deletions

View File

@ -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 ",

View File

@ -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": " トピック ",

View File

@ -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": " Топики ",

View File

@ -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": "设置",

View File

@ -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": " 話題 ",

View File

@ -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<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
}, [knowledgeBases, openKnowledgeFileList, quickPanel, t, inputbarToolsRef])
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
const isEnterPressed = event.key === 'Enter' && !event.nativeEvent.isComposing
// 按下Tab键自动选中${xxx}
if (event.key === 'Tab' && inputFocus) {
event.preventDefault()
@ -366,32 +365,37 @@ const Inputbar: FC<Props> = ({ 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<Props> = ({ 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"

View File

@ -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<Props> = ({ message, onSave, onResend, onCancel })
onResend(updatedBlocks)
}
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>, 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<Props> = ({ message, onSave, onResend, onCancel })
handleTextChange(block.id, e.target.value)
resizeTextArea()
}}
onKeyDown={handleKeyDown}
onKeyDown={(e) => handleKeyDown(e, block.id)}
autoFocus
contextMenu="true"
spellCheck={false}

View File

@ -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> = (props) => {
value={sendMessageShortcut}
menuItemSelectedIcon={<CheckOutlined />}
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 }}

View File

@ -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'

View File

@ -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<HTMLDivElement>): Promise<FileType[]> => {
@ -58,3 +60,47 @@ export const getFilesFromDropEvent = async (e: React.DragEvent<HTMLDivElement>):
})
}
}
// 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<HTMLTextAreaElement>,
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
}