feat: Add quote feature (#2657)

* feat: Add text quoting functionality to messages

* feat(i18n): add quote message to multiple language files
This commit is contained in:
Asurada 2025-03-03 17:25:34 +08:00 committed by GitHub
parent d62ff69351
commit 4ca2d61ccc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 85 additions and 4 deletions

View File

@ -109,6 +109,7 @@
"message.new.context": "New Context",
"message.regenerate.model": "Switch Model",
"message.useful": "Helpful",
"message.quote": "Quote",
"resend": "Resend",
"save": "Save",
"settings.code_collapsible": "Code block collapsible",

View File

@ -109,6 +109,7 @@
"message.new.context": "新しいコンテキスト",
"message.regenerate.model": "モデルを切り替え",
"message.useful": "役立つ",
"message.quote": "引用",
"resend": "再送信",
"save": "保存",
"settings.code_collapsible": "コードブロックを折りたたむ",

View File

@ -109,6 +109,7 @@
"message.new.context": "Новый контекст",
"message.regenerate.model": "Переключить модель",
"message.useful": "Полезно",
"message.quote": "Цитата",
"resend": "Переотправить",
"save": "Сохранить",
"settings.code_collapsible": "Блок кода свернут",

View File

@ -109,6 +109,7 @@
"message.new.context": "清除上下文",
"message.regenerate.model": "切换模型",
"message.useful": "有用",
"message.quote": "引用",
"resend": "重新发送",
"save": "保存",
"settings.code_collapsible": "代码块可折叠",

View File

@ -109,6 +109,7 @@
"message.new.context": "新上下文",
"message.regenerate.model": "切換模型",
"message.useful": "有用",
"message.quote": "引用",
"resend": "重新發送",
"save": "保存",
"settings.code_collapsible": "代码块可折叠",

View File

@ -456,7 +456,15 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
_setEstimateTokenCount(tokensCount)
setContextCount(contextCount)
}),
EventEmitter.on(EVENT_NAMES.ADD_NEW_TOPIC, addNewTopic)
EventEmitter.on(EVENT_NAMES.ADD_NEW_TOPIC, addNewTopic),
EventEmitter.on(EVENT_NAMES.QUOTE_TEXT, (quotedText: string) => {
setText((prevText) => {
const newText = prevText ? `${prevText}\n${quotedText}\n` : `${quotedText}\n`
setTimeout(() => resizeTextArea(), 0)
return newText
})
textareaRef.current?.focus()
})
]
return () => unsubscribes.forEach((unsub) => unsub())
}, [addNewTopic])

View File

@ -11,7 +11,7 @@ import { getModelUniqId } from '@renderer/services/ModelService'
import { estimateHistoryTokens, estimateMessageUsage } from '@renderer/services/TokenService'
import { Message, Topic } from '@renderer/types'
import { classNames, runAsyncFunction } from '@renderer/utils'
import { Divider } from 'antd'
import { Divider, Dropdown } from 'antd'
import { Dispatch, FC, memo, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@ -62,6 +62,8 @@ const MessageItem: FC<Props> = ({
const { showMessageDivider, messageFont, fontSize } = useSettings()
const messageContainerRef = useRef<HTMLDivElement>(null)
const topic = useTopic(assistant, _topic?.id)
const [contextMenuPosition, setContextMenuPosition] = useState<{ x: number; y: number } | null>(null)
const [selectedQuoteText, setSelectedQuoteText] = useState<string>('')
const isLastMessage = index === 0
const isAssistantMessage = message.role === 'assistant'
@ -75,6 +77,30 @@ const MessageItem: FC<Props> = ({
const messageBorder = showMessageDivider ? undefined : 'none'
const messageBackground = getMessageBackground(isBubbleStyle, isAssistantMessage)
const handleContextMenu = useCallback((e: React.MouseEvent) => {
e.preventDefault()
const selectedText = window.getSelection()?.toString()
if (selectedText) {
const quotedText =
selectedText
.split('\n')
.map((line) => `> ${line}`)
.join('\n') + '\n-------------'
setSelectedQuoteText(quotedText)
setContextMenuPosition({ x: e.clientX, y: e.clientY })
}
}, [])
useEffect(() => {
const handleClick = () => {
setContextMenuPosition(null)
}
document.addEventListener('click', handleClick)
return () => {
document.removeEventListener('click', handleClick)
}
}, [])
const onEditMessage = useCallback(
async (msg: Message) => {
const usage = await estimateMessageUsage(msg)
@ -185,7 +211,44 @@ const MessageItem: FC<Props> = ({
'message-user': !isAssistantMessage
})}
ref={messageContainerRef}
onContextMenu={handleContextMenu}
style={{ ...style, alignItems: isBubbleStyle ? (isAssistantMessage ? 'start' : 'end') : undefined }}>
{contextMenuPosition && (
<ContextMenuOverlay
style={{
position: 'fixed',
left: contextMenuPosition.x,
top: contextMenuPosition.y,
zIndex: 1000
}}>
<Dropdown
menu={{
items: [
{
key: 'copy',
label: t('common.copy'),
onClick: () => {
navigator.clipboard.writeText(
selectedQuoteText.replace(/^> /gm, '').replace(/\n-------------$/, '')
)
window.message.success({ content: t('message.copied'), key: 'copy-message' })
}
},
{
key: 'quote',
label: t('chat.message.quote'),
onClick: () => {
EventEmitter.emit(EVENT_NAMES.QUOTE_TEXT, selectedQuoteText)
}
}
]
}}
open={true}
trigger={['contextMenu']}>
<div />
</Dropdown>
</ContextMenuOverlay>
)}
<MessageHeader message={message} assistant={assistant} model={model} key={getModelUniqId(model)} />
<MessageContentContainer
className="message-content-container"
@ -270,4 +333,8 @@ const NewContextMessage = styled.div`
cursor: pointer;
`
const ContextMenuOverlay = styled.div`
position: fixed;
`
export default memo(MessageItem)

View File

@ -24,5 +24,6 @@ export const EVENT_NAMES = {
LOCATE_MESSAGE: 'LOCATE_MESSAGE',
ADD_NEW_TOPIC: 'ADD_NEW_TOPIC',
RESEND_MESSAGE: 'RESEND_MESSAGE',
SHOW_MODEL_SELECTOR: 'SHOW_MODEL_SELECTOR'
SHOW_MODEL_SELECTOR: 'SHOW_MODEL_SELECTOR',
QUOTE_TEXT: 'QUOTE_TEXT'
}