mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-11 08:19:01 +08:00
fix: unified the behavior of SendMessage shortcut (#7276)
This commit is contained in:
parent
68c1a3e1cc
commit
37dac7f6ea
@ -183,7 +183,7 @@
|
|||||||
"input.new.context": "Clear Context {{Command}}",
|
"input.new.context": "Clear Context {{Command}}",
|
||||||
"input.new_topic": "New Topic {{Command}}",
|
"input.new_topic": "New Topic {{Command}}",
|
||||||
"input.pause": "Pause",
|
"input.pause": "Pause",
|
||||||
"input.placeholder": "Type your message here...",
|
"input.placeholder": "Type your message here, press {{key}} to send...",
|
||||||
"input.send": "Send",
|
"input.send": "Send",
|
||||||
"input.settings": "Settings",
|
"input.settings": "Settings",
|
||||||
"input.topics": " Topics ",
|
"input.topics": " Topics ",
|
||||||
|
|||||||
@ -183,7 +183,7 @@
|
|||||||
"input.new.context": "コンテキストをクリア {{Command}}",
|
"input.new.context": "コンテキストをクリア {{Command}}",
|
||||||
"input.new_topic": "新しいトピック {{Command}}",
|
"input.new_topic": "新しいトピック {{Command}}",
|
||||||
"input.pause": "一時停止",
|
"input.pause": "一時停止",
|
||||||
"input.placeholder": "ここにメッセージを入力...",
|
"input.placeholder": "ここにメッセージを入力し、{{key}} を押して送信...",
|
||||||
"input.send": "送信",
|
"input.send": "送信",
|
||||||
"input.settings": "設定",
|
"input.settings": "設定",
|
||||||
"input.topics": " トピック ",
|
"input.topics": " トピック ",
|
||||||
|
|||||||
@ -183,7 +183,7 @@
|
|||||||
"input.new.context": "Очистить контекст {{Command}}",
|
"input.new.context": "Очистить контекст {{Command}}",
|
||||||
"input.new_topic": "Новый топик {{Command}}",
|
"input.new_topic": "Новый топик {{Command}}",
|
||||||
"input.pause": "Остановить",
|
"input.pause": "Остановить",
|
||||||
"input.placeholder": "Введите ваше сообщение здесь...",
|
"input.placeholder": "Введите ваше сообщение здесь, нажмите {{key}} для отправки...",
|
||||||
"input.send": "Отправить",
|
"input.send": "Отправить",
|
||||||
"input.settings": "Настройки",
|
"input.settings": "Настройки",
|
||||||
"input.topics": " Топики ",
|
"input.topics": " Топики ",
|
||||||
|
|||||||
@ -183,7 +183,7 @@
|
|||||||
"input.new.context": "清除上下文 {{Command}}",
|
"input.new.context": "清除上下文 {{Command}}",
|
||||||
"input.new_topic": "新话题 {{Command}}",
|
"input.new_topic": "新话题 {{Command}}",
|
||||||
"input.pause": "暂停",
|
"input.pause": "暂停",
|
||||||
"input.placeholder": "在这里输入消息...",
|
"input.placeholder": "在这里输入消息,按 {{key}} 发送...",
|
||||||
"input.translating": "翻译中...",
|
"input.translating": "翻译中...",
|
||||||
"input.send": "发送",
|
"input.send": "发送",
|
||||||
"input.settings": "设置",
|
"input.settings": "设置",
|
||||||
|
|||||||
@ -183,7 +183,7 @@
|
|||||||
"input.new.context": "清除上下文 {{Command}}",
|
"input.new.context": "清除上下文 {{Command}}",
|
||||||
"input.new_topic": "新話題 {{Command}}",
|
"input.new_topic": "新話題 {{Command}}",
|
||||||
"input.pause": "暫停",
|
"input.pause": "暫停",
|
||||||
"input.placeholder": "在此輸入您的訊息...",
|
"input.placeholder": "在此輸入您的訊息,按 {{key}} 傳送...",
|
||||||
"input.send": "傳送",
|
"input.send": "傳送",
|
||||||
"input.settings": "設定",
|
"input.settings": "設定",
|
||||||
"input.topics": " 話題 ",
|
"input.topics": " 話題 ",
|
||||||
|
|||||||
@ -36,6 +36,7 @@ import type { MessageInputBaseParams } from '@renderer/types/newMessage'
|
|||||||
import { classNames, delay, formatFileSize, getFileExtension } from '@renderer/utils'
|
import { classNames, delay, formatFileSize, getFileExtension } from '@renderer/utils'
|
||||||
import { formatQuotedText } from '@renderer/utils/formats'
|
import { formatQuotedText } from '@renderer/utils/formats'
|
||||||
import { getFilesFromDropEvent } from '@renderer/utils/input'
|
import { getFilesFromDropEvent } from '@renderer/utils/input'
|
||||||
|
import { getSendMessageShortcutLabel, isSendMessageKeyPressed } from '@renderer/utils/input'
|
||||||
import { documentExts, imageExts, textExts } from '@shared/config/constant'
|
import { documentExts, imageExts, textExts } from '@shared/config/constant'
|
||||||
import { IpcChannel } from '@shared/IpcChannel'
|
import { IpcChannel } from '@shared/IpcChannel'
|
||||||
import { Button, Tooltip } from 'antd'
|
import { Button, Tooltip } from 'antd'
|
||||||
@ -309,8 +310,6 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
|||||||
}, [knowledgeBases, openKnowledgeFileList, quickPanel, t, inputbarToolsRef])
|
}, [knowledgeBases, openKnowledgeFileList, quickPanel, t, inputbarToolsRef])
|
||||||
|
|
||||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
const isEnterPressed = event.key === 'Enter' && !event.nativeEvent.isComposing
|
|
||||||
|
|
||||||
// 按下Tab键,自动选中${xxx}
|
// 按下Tab键,自动选中${xxx}
|
||||||
if (event.key === 'Tab' && inputFocus) {
|
if (event.key === 'Tab' && inputFocus) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
@ -366,32 +365,37 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isEnterPressed && !event.shiftKey && sendMessageShortcut === 'Enter') {
|
//to check if the SendMessage key is pressed
|
||||||
if (quickPanel.isVisible) return event.preventDefault()
|
//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()
|
const textArea = textareaRef.current?.resizableTextArea?.textArea
|
||||||
return event.preventDefault()
|
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) {
|
// update text by setState, not directly modify textarea.value
|
||||||
if (quickPanel.isVisible) return event.preventDefault()
|
setText(newText)
|
||||||
|
|
||||||
sendMessage()
|
// set cursor position in the next render cycle
|
||||||
return event.preventDefault()
|
setTimeout(() => {
|
||||||
}
|
textArea.selectionStart = textArea.selectionEnd = start + 1
|
||||||
|
onInput() // trigger resizeTextArea
|
||||||
if (sendMessageShortcut === 'Ctrl+Enter' && isEnterPressed && event.ctrlKey) {
|
}, 0)
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (enableBackspaceDeleteModel && event.key === 'Backspace' && text.trim() === '' && mentionModels.length > 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}
|
value={text}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onKeyDown={handleKeyDown}
|
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
|
autoFocus
|
||||||
contextMenu="true"
|
contextMenu="true"
|
||||||
variant="borderless"
|
variant="borderless"
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import PasteService from '@renderer/services/PasteService'
|
|||||||
import { FileType, FileTypes } from '@renderer/types'
|
import { FileType, FileTypes } from '@renderer/types'
|
||||||
import { Message, MessageBlock, MessageBlockStatus, MessageBlockType } from '@renderer/types/newMessage'
|
import { Message, MessageBlock, MessageBlockStatus, MessageBlockType } from '@renderer/types/newMessage'
|
||||||
import { classNames, getFileExtension } from '@renderer/utils'
|
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 { createFileBlock, createImageBlock } from '@renderer/utils/messageUtils/create'
|
||||||
import { findAllBlocks } from '@renderer/utils/messageUtils/find'
|
import { findAllBlocks } from '@renderer/utils/messageUtils/find'
|
||||||
import { documentExts, imageExts, textExts } from '@shared/config/constant'
|
import { documentExts, imageExts, textExts } from '@shared/config/constant'
|
||||||
@ -169,31 +169,39 @@ const MessageBlockEditor: FC<Props> = ({ message, onSave, onResend, onCancel })
|
|||||||
onResend(updatedBlocks)
|
onResend(updatedBlocks)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>, blockId: string) => {
|
||||||
if (message.role !== 'user') {
|
if (message.role !== 'user') {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// keep the same enter behavior as inputbar
|
||||||
const isEnterPressed = event.key === 'Enter' && !event.nativeEvent.isComposing
|
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') {
|
const textArea = textareaRef.current?.resizableTextArea?.textArea
|
||||||
handleResend()
|
if (textArea) {
|
||||||
return event.preventDefault()
|
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) {
|
//same with onChange()
|
||||||
handleResend()
|
handleTextChange(blockId, newText)
|
||||||
return event.preventDefault()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sendMessageShortcut === 'Ctrl+Enter' && isEnterPressed && event.ctrlKey) {
|
// set cursor position in the next render cycle
|
||||||
handleResend()
|
setTimeout(() => {
|
||||||
return event.preventDefault()
|
textArea.selectionStart = textArea.selectionEnd = start + 1
|
||||||
}
|
resizeTextArea() // trigger resizeTextArea
|
||||||
|
}, 0)
|
||||||
if (sendMessageShortcut === 'Command+Enter' && isEnterPressed && event.metaKey) {
|
}
|
||||||
handleResend()
|
}
|
||||||
return event.preventDefault()
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -212,7 +220,7 @@ const MessageBlockEditor: FC<Props> = ({ message, onSave, onResend, onCancel })
|
|||||||
handleTextChange(block.id, e.target.value)
|
handleTextChange(block.id, e.target.value)
|
||||||
resizeTextArea()
|
resizeTextArea()
|
||||||
}}
|
}}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={(e) => handleKeyDown(e, block.id)}
|
||||||
autoFocus
|
autoFocus
|
||||||
contextMenu="true"
|
contextMenu="true"
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
|
|||||||
@ -1,13 +1,7 @@
|
|||||||
import { CheckOutlined } from '@ant-design/icons'
|
import { CheckOutlined } from '@ant-design/icons'
|
||||||
import { HStack } from '@renderer/components/Layout'
|
import { HStack } from '@renderer/components/Layout'
|
||||||
import Scrollbar from '@renderer/components/Scrollbar'
|
import Scrollbar from '@renderer/components/Scrollbar'
|
||||||
import {
|
import { DEFAULT_CONTEXTCOUNT, DEFAULT_MAX_TOKENS, DEFAULT_TEMPERATURE } from '@renderer/config/constant'
|
||||||
DEFAULT_CONTEXTCOUNT,
|
|
||||||
DEFAULT_MAX_TOKENS,
|
|
||||||
DEFAULT_TEMPERATURE,
|
|
||||||
isMac,
|
|
||||||
isWindows
|
|
||||||
} from '@renderer/config/constant'
|
|
||||||
import {
|
import {
|
||||||
isOpenAIModel,
|
isOpenAIModel,
|
||||||
isSupportedFlexServiceTier,
|
isSupportedFlexServiceTier,
|
||||||
@ -59,6 +53,7 @@ import {
|
|||||||
TranslateLanguageVarious
|
TranslateLanguageVarious
|
||||||
} from '@renderer/types'
|
} from '@renderer/types'
|
||||||
import { modalConfirm } from '@renderer/utils'
|
import { modalConfirm } from '@renderer/utils'
|
||||||
|
import { getSendMessageShortcutLabel } from '@renderer/utils/input'
|
||||||
import { Button, Col, InputNumber, Row, Select, Slider, Switch, Tooltip } from 'antd'
|
import { Button, Col, InputNumber, Row, Select, Slider, Switch, Tooltip } from 'antd'
|
||||||
import { CircleHelp, Settings2 } from 'lucide-react'
|
import { CircleHelp, Settings2 } from 'lucide-react'
|
||||||
import { FC, useCallback, useEffect, useMemo, useState } from 'react'
|
import { FC, useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
@ -670,10 +665,11 @@ const SettingsTab: FC<Props> = (props) => {
|
|||||||
value={sendMessageShortcut}
|
value={sendMessageShortcut}
|
||||||
menuItemSelectedIcon={<CheckOutlined />}
|
menuItemSelectedIcon={<CheckOutlined />}
|
||||||
options={[
|
options={[
|
||||||
{ value: 'Enter', label: 'Enter' },
|
{ value: 'Enter', label: getSendMessageShortcutLabel('Enter') },
|
||||||
{ value: 'Shift+Enter', label: 'Shift + Enter' },
|
{ value: 'Ctrl+Enter', label: getSendMessageShortcutLabel('Ctrl+Enter') },
|
||||||
{ value: 'Ctrl+Enter', label: 'Ctrl + Enter' },
|
{ value: 'Alt+Enter', label: getSendMessageShortcutLabel('Alt+Enter') },
|
||||||
{ value: 'Command+Enter', label: `${isMac ? '⌘' : isWindows ? 'Win' : 'Super'} + Enter` }
|
{ value: 'Command+Enter', label: getSendMessageShortcutLabel('Command+Enter') },
|
||||||
|
{ value: 'Shift+Enter', label: getSendMessageShortcutLabel('Shift+Enter') }
|
||||||
]}
|
]}
|
||||||
onChange={(value) => setSendMessageShortcut(value as SendMessageShortcut)}
|
onChange={(value) => setSendMessageShortcut(value as SendMessageShortcut)}
|
||||||
style={{ width: 135 }}
|
style={{ width: 135 }}
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import {
|
|||||||
|
|
||||||
import { WebDAVSyncState } from './backup'
|
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'
|
export type SidebarIcon = 'assistants' | 'agents' | 'paintings' | 'translate' | 'minapp' | 'knowledge' | 'files'
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
|
import { isMac, isWindows } from '@renderer/config/constant'
|
||||||
import Logger from '@renderer/config/logger'
|
import Logger from '@renderer/config/logger'
|
||||||
|
import type { SendMessageShortcut } from '@renderer/store/settings'
|
||||||
import { FileType } from '@renderer/types'
|
import { FileType } from '@renderer/types'
|
||||||
|
|
||||||
export const getFilesFromDropEvent = async (e: React.DragEvent<HTMLDivElement>): Promise<FileType[]> => {
|
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
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user