feat: add message translate copy & close (#5684)

* feat: add message translate copy & close

* fix: remove blockEntity
This commit is contained in:
自由的世界人 2025-05-11 13:40:09 +08:00 committed by GitHub
parent 4f641c294b
commit 1375d1a1c2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 145 additions and 11 deletions

View File

@ -333,6 +333,36 @@ export function useMessageOperations(topic: Topic) {
[dispatch, topic.id]
)
/**
* Removes a specific block from a message.
*/
const removeMessageBlock = useCallback(
async (messageId: string, blockIdToRemove: string) => {
if (!topic?.id) {
console.error('[removeMessageBlock] Topic prop is not valid.')
return
}
const state = store.getState()
const message = state.messages.entities[messageId]
if (!message || !message.blocks) {
console.error('[removeMessageBlock] Message not found or has no blocks:', messageId)
return
}
const updatedBlocks = message.blocks.filter((blockId) => blockId !== blockIdToRemove)
const messageUpdates: Partial<Message> & Pick<Message, 'id'> = {
id: messageId,
updatedAt: new Date().toISOString(),
blocks: updatedBlocks
}
await dispatch(updateMessageAndBlocksThunk(topic.id, messageUpdates, []))
},
[dispatch, topic?.id]
)
return {
displayCount,
deleteMessage,
@ -348,7 +378,8 @@ export function useMessageOperations(topic: Topic) {
resumeMessage,
getTranslationUpdater,
createTopicBranch,
editMessageBlocks
editMessageBlocks,
removeMessageBlock
}
}

View File

@ -1624,6 +1624,10 @@
"any.language": "Any language",
"button.translate": "Translate",
"close": "Close",
"closed": "Translation closed",
"copied": "Translation content copied",
"empty": "Translation content is empty",
"not.found": "Translation content not found",
"confirm": {
"content": "Translation will replace the original text, continue?",
"title": "Translation Confirmation"

View File

@ -1624,6 +1624,10 @@
"any.language": "任意の言語",
"button.translate": "翻訳",
"close": "閉じる",
"closed": "翻訳は閉じられました",
"copied": "翻訳内容がコピーされました",
"empty": "翻訳内容が空です",
"not.found": "翻訳内容が見つかりません",
"confirm": {
"content": "翻訳すると元のテキストが上書きされます。続行しますか?",
"title": "翻訳確認"

View File

@ -1624,6 +1624,10 @@
"any.language": "Любой язык",
"button.translate": "Перевести",
"close": "Закрыть",
"closed": "Перевод закрыт",
"copied": "Содержимое перевода скопировано",
"empty": "Содержимое перевода пусто",
"not.found": "Содержимое перевода не найдено",
"confirm": {
"content": "Перевод заменит исходный текст, продолжить?",
"title": "Перевод подтверждение"

View File

@ -1624,6 +1624,10 @@
"any.language": "任意语言",
"button.translate": "翻译",
"close": "关闭",
"closed": "翻译已关闭",
"copied": "翻译内容已复制",
"empty": "翻译内容为空",
"not.found": "未找到翻译内容",
"confirm": {
"content": "翻译后将覆盖原文,是否继续?",
"title": "翻译确认"

View File

@ -1624,6 +1624,10 @@
"any.language": "任意語言",
"button.translate": "翻譯",
"close": "關閉",
"closed": "翻譯已關閉",
"copied": "翻譯內容已複製",
"empty": "翻譯內容為空",
"not.found": "未找到翻譯內容",
"confirm": {
"content": "翻譯後將覆蓋原文,是否繼續?",
"title": "翻譯確認"

View File

@ -8,6 +8,7 @@ import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { getMessageTitle } from '@renderer/services/MessagesService'
import { translateText } from '@renderer/services/TranslateService'
import { RootState } from '@renderer/store'
import { messageBlocksSelectors } from '@renderer/store/messageBlock'
import type { Model } from '@renderer/types'
import type { Assistant, Topic } from '@renderer/types'
import type { Message } from '@renderer/types/newMessage'
@ -22,7 +23,12 @@ import {
} from '@renderer/utils/export'
// import { withMessageThought } from '@renderer/utils/formats'
import { removeTrailingDoubleSpaces } from '@renderer/utils/markdown'
import { findImageBlocks, findMainTextBlocks, getMainTextContent } from '@renderer/utils/messageUtils/find'
import {
findImageBlocks,
findMainTextBlocks,
findTranslationBlocks,
getMainTextContent
} from '@renderer/utils/messageUtils/find'
import { Button, Dropdown, Popconfirm, Tooltip } from 'antd'
import dayjs from 'dayjs'
import { AtSign, Copy, Languages, Menu, RefreshCw, Save, Share, Split, ThumbsUp, Trash } from 'lucide-react'
@ -62,7 +68,8 @@ const MessageMenubar: FC<Props> = (props) => {
resendUserMessageWithEdit,
getTranslationUpdater,
appendAssistantResponse,
editMessageBlocks
editMessageBlocks,
removeMessageBlock
} = useMessageOperations(topic)
const loading = useTopicLoading(topic)
@ -377,6 +384,12 @@ const MessageMenubar: FC<Props> = (props) => {
[message, editMessage]
)
const blockEntities = useSelector(messageBlocksSelectors.selectEntities)
const hasTranslationBlocks = useMemo(() => {
const translationBlocks = findTranslationBlocks(message)
return translationBlocks.length > 0
}, [message])
return (
<MenusBar className={`menubar ${isLastMessage && 'show'}`}>
{message.role === 'user' && (
@ -432,13 +445,52 @@ const MessageMenubar: FC<Props> = (props) => {
label: item.emoji + ' ' + item.label,
key: item.value,
onClick: () => handleTranslate(item.value)
}))
// {
// TODO 删除翻译块可以放在翻译块内
// label: '✖ ' + t('translate.close'),
// key: 'translate-close',
// onClick: () => editMessage(message.id, { translatedContent: undefined })
// }
})),
...(hasTranslationBlocks
? [
{ type: 'divider' as const },
{
label: '📋 ' + t('common.copy'),
key: 'translate-copy',
onClick: () => {
const translationBlocks = message.blocks
.map((blockId) => blockEntities[blockId])
.filter((block) => block?.type === 'translation')
if (translationBlocks.length > 0) {
const translationContent = translationBlocks
.map((block) => block?.content || '')
.join('\n\n')
.trim()
if (translationContent) {
navigator.clipboard.writeText(translationContent)
window.message.success({ content: t('translate.copied'), key: 'translate-copy' })
} else {
window.message.warning({ content: t('translate.empty'), key: 'translate-copy' })
}
}
}
},
{
label: '✖ ' + t('translate.close'),
key: 'translate-close',
onClick: () => {
const translationBlocks = message.blocks
.map((blockId) => blockEntities[blockId])
.filter((block) => block?.type === 'translation')
.map((block) => block?.id)
if (translationBlocks.length > 0) {
translationBlocks.forEach((blockId) => {
if (blockId) removeMessageBlock(message.id, blockId)
})
window.message.success({ content: t('translate.closed'), key: 'translate-close' })
}
}
}
]
: [])
],
onClick: (e) => e.domEvent.stopPropagation()
}}

View File

@ -6,7 +6,8 @@ import type {
ImageMessageBlock,
MainTextMessageBlock,
Message,
ThinkingMessageBlock
ThinkingMessageBlock,
TranslationMessageBlock
} from '@renderer/types/newMessage'
import { MessageBlockType } from '@renderer/types/newMessage'
@ -30,6 +31,11 @@ export const findMainTextBlocks = (message: Message): MainTextMessageBlock[] =>
return textBlocks
}
/**
* Finds all ThinkingMessageBlocks associated with a given message.
* @param message - The message object.
* @returns An array of ThinkingMessageBlocks (empty if none found).
*/
export const findThinkingBlocks = (message: Message): ThinkingMessageBlock[] => {
if (!message || !message.blocks || message.blocks.length === 0) {
return []
@ -95,6 +101,11 @@ export const getMainTextContent = (message: Message): string => {
return textBlocks.map((block) => block.content).join('\n\n')
}
/**
* Gets the concatenated content string from all ThinkingMessageBlocks of a message, in order.
* @param message
* @returns The concatenated content string or an empty string if no thinking blocks are found.
*/
export const getThinkingContent = (message: Message): string => {
const thinkingBlocks = findThinkingBlocks(message)
return thinkingBlocks.map((block) => block.content).join('\n\n')
@ -131,6 +142,26 @@ export const findCitationBlocks = (message: Message): CitationMessageBlock[] =>
return citationBlocks
}
/**
* Finds all TranslationMessageBlocks associated with a given message.
* @param message - The message object.
* @returns An array of TranslationMessageBlocks (empty if none found).
*/
export const findTranslationBlocks = (message: Message): TranslationMessageBlock[] => {
if (!message || !message.blocks || message.blocks.length === 0) {
return []
}
const state = store.getState()
const translationBlocks: TranslationMessageBlock[] = []
for (const blockId of message.blocks) {
const block = messageBlocksSelectors.selectById(state, blockId)
if (block && block.type === 'translation') {
translationBlocks.push(block as TranslationMessageBlock)
}
}
return translationBlocks
}
/**
* Finds the WebSearchMessageBlock associated with a given message.
* Assumes only one web search block per message.