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:
parent
d62ff69351
commit
4ca2d61ccc
@ -109,6 +109,7 @@
|
|||||||
"message.new.context": "New Context",
|
"message.new.context": "New Context",
|
||||||
"message.regenerate.model": "Switch Model",
|
"message.regenerate.model": "Switch Model",
|
||||||
"message.useful": "Helpful",
|
"message.useful": "Helpful",
|
||||||
|
"message.quote": "Quote",
|
||||||
"resend": "Resend",
|
"resend": "Resend",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"settings.code_collapsible": "Code block collapsible",
|
"settings.code_collapsible": "Code block collapsible",
|
||||||
@ -392,7 +393,7 @@
|
|||||||
"message.code_style": "Code style",
|
"message.code_style": "Code style",
|
||||||
"message.delete.content": "Are you sure you want to delete this message?",
|
"message.delete.content": "Are you sure you want to delete this message?",
|
||||||
"message.delete.title": "Delete Message",
|
"message.delete.title": "Delete Message",
|
||||||
"message.multi_model_style": "Multi-model response style",
|
"message.multi_model_style": "Multi-model response style",
|
||||||
"message.multi_model_style.fold": "Fold view",
|
"message.multi_model_style.fold": "Fold view",
|
||||||
"message.multi_model_style.grid": "Grid layout",
|
"message.multi_model_style.grid": "Grid layout",
|
||||||
"message.multi_model_style.horizontal": "Side by side",
|
"message.multi_model_style.horizontal": "Side by side",
|
||||||
|
|||||||
@ -109,6 +109,7 @@
|
|||||||
"message.new.context": "新しいコンテキスト",
|
"message.new.context": "新しいコンテキスト",
|
||||||
"message.regenerate.model": "モデルを切り替え",
|
"message.regenerate.model": "モデルを切り替え",
|
||||||
"message.useful": "役立つ",
|
"message.useful": "役立つ",
|
||||||
|
"message.quote": "引用",
|
||||||
"resend": "再送信",
|
"resend": "再送信",
|
||||||
"save": "保存",
|
"save": "保存",
|
||||||
"settings.code_collapsible": "コードブロックを折りたたむ",
|
"settings.code_collapsible": "コードブロックを折りたたむ",
|
||||||
|
|||||||
@ -109,6 +109,7 @@
|
|||||||
"message.new.context": "Новый контекст",
|
"message.new.context": "Новый контекст",
|
||||||
"message.regenerate.model": "Переключить модель",
|
"message.regenerate.model": "Переключить модель",
|
||||||
"message.useful": "Полезно",
|
"message.useful": "Полезно",
|
||||||
|
"message.quote": "Цитата",
|
||||||
"resend": "Переотправить",
|
"resend": "Переотправить",
|
||||||
"save": "Сохранить",
|
"save": "Сохранить",
|
||||||
"settings.code_collapsible": "Блок кода свернут",
|
"settings.code_collapsible": "Блок кода свернут",
|
||||||
|
|||||||
@ -109,6 +109,7 @@
|
|||||||
"message.new.context": "清除上下文",
|
"message.new.context": "清除上下文",
|
||||||
"message.regenerate.model": "切换模型",
|
"message.regenerate.model": "切换模型",
|
||||||
"message.useful": "有用",
|
"message.useful": "有用",
|
||||||
|
"message.quote": "引用",
|
||||||
"resend": "重新发送",
|
"resend": "重新发送",
|
||||||
"save": "保存",
|
"save": "保存",
|
||||||
"settings.code_collapsible": "代码块可折叠",
|
"settings.code_collapsible": "代码块可折叠",
|
||||||
|
|||||||
@ -109,6 +109,7 @@
|
|||||||
"message.new.context": "新上下文",
|
"message.new.context": "新上下文",
|
||||||
"message.regenerate.model": "切換模型",
|
"message.regenerate.model": "切換模型",
|
||||||
"message.useful": "有用",
|
"message.useful": "有用",
|
||||||
|
"message.quote": "引用",
|
||||||
"resend": "重新發送",
|
"resend": "重新發送",
|
||||||
"save": "保存",
|
"save": "保存",
|
||||||
"settings.code_collapsible": "代码块可折叠",
|
"settings.code_collapsible": "代码块可折叠",
|
||||||
|
|||||||
@ -456,7 +456,15 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
|||||||
_setEstimateTokenCount(tokensCount)
|
_setEstimateTokenCount(tokensCount)
|
||||||
setContextCount(contextCount)
|
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())
|
return () => unsubscribes.forEach((unsub) => unsub())
|
||||||
}, [addNewTopic])
|
}, [addNewTopic])
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import { getModelUniqId } from '@renderer/services/ModelService'
|
|||||||
import { estimateHistoryTokens, estimateMessageUsage } from '@renderer/services/TokenService'
|
import { estimateHistoryTokens, estimateMessageUsage } from '@renderer/services/TokenService'
|
||||||
import { Message, Topic } from '@renderer/types'
|
import { Message, Topic } from '@renderer/types'
|
||||||
import { classNames, runAsyncFunction } from '@renderer/utils'
|
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 { Dispatch, FC, memo, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
@ -62,6 +62,8 @@ const MessageItem: FC<Props> = ({
|
|||||||
const { showMessageDivider, messageFont, fontSize } = useSettings()
|
const { showMessageDivider, messageFont, fontSize } = useSettings()
|
||||||
const messageContainerRef = useRef<HTMLDivElement>(null)
|
const messageContainerRef = useRef<HTMLDivElement>(null)
|
||||||
const topic = useTopic(assistant, _topic?.id)
|
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 isLastMessage = index === 0
|
||||||
const isAssistantMessage = message.role === 'assistant'
|
const isAssistantMessage = message.role === 'assistant'
|
||||||
@ -75,6 +77,30 @@ const MessageItem: FC<Props> = ({
|
|||||||
const messageBorder = showMessageDivider ? undefined : 'none'
|
const messageBorder = showMessageDivider ? undefined : 'none'
|
||||||
const messageBackground = getMessageBackground(isBubbleStyle, isAssistantMessage)
|
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(
|
const onEditMessage = useCallback(
|
||||||
async (msg: Message) => {
|
async (msg: Message) => {
|
||||||
const usage = await estimateMessageUsage(msg)
|
const usage = await estimateMessageUsage(msg)
|
||||||
@ -185,7 +211,44 @@ const MessageItem: FC<Props> = ({
|
|||||||
'message-user': !isAssistantMessage
|
'message-user': !isAssistantMessage
|
||||||
})}
|
})}
|
||||||
ref={messageContainerRef}
|
ref={messageContainerRef}
|
||||||
|
onContextMenu={handleContextMenu}
|
||||||
style={{ ...style, alignItems: isBubbleStyle ? (isAssistantMessage ? 'start' : 'end') : undefined }}>
|
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)} />
|
<MessageHeader message={message} assistant={assistant} model={model} key={getModelUniqId(model)} />
|
||||||
<MessageContentContainer
|
<MessageContentContainer
|
||||||
className="message-content-container"
|
className="message-content-container"
|
||||||
@ -270,4 +333,8 @@ const NewContextMessage = styled.div`
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const ContextMenuOverlay = styled.div`
|
||||||
|
position: fixed;
|
||||||
|
`
|
||||||
|
|
||||||
export default memo(MessageItem)
|
export default memo(MessageItem)
|
||||||
|
|||||||
@ -24,5 +24,6 @@ export const EVENT_NAMES = {
|
|||||||
LOCATE_MESSAGE: 'LOCATE_MESSAGE',
|
LOCATE_MESSAGE: 'LOCATE_MESSAGE',
|
||||||
ADD_NEW_TOPIC: 'ADD_NEW_TOPIC',
|
ADD_NEW_TOPIC: 'ADD_NEW_TOPIC',
|
||||||
RESEND_MESSAGE: 'RESEND_MESSAGE',
|
RESEND_MESSAGE: 'RESEND_MESSAGE',
|
||||||
SHOW_MODEL_SELECTOR: 'SHOW_MODEL_SELECTOR'
|
SHOW_MODEL_SELECTOR: 'SHOW_MODEL_SELECTOR',
|
||||||
|
QUOTE_TEXT: 'QUOTE_TEXT'
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user