mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-24 18:50:56 +08:00
feat: add message translate copy & close (#5684)
* feat: add message translate copy & close * fix: remove blockEntity
This commit is contained in:
parent
4f641c294b
commit
1375d1a1c2
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -1624,6 +1624,10 @@
|
||||
"any.language": "任意の言語",
|
||||
"button.translate": "翻訳",
|
||||
"close": "閉じる",
|
||||
"closed": "翻訳は閉じられました",
|
||||
"copied": "翻訳内容がコピーされました",
|
||||
"empty": "翻訳内容が空です",
|
||||
"not.found": "翻訳内容が見つかりません",
|
||||
"confirm": {
|
||||
"content": "翻訳すると元のテキストが上書きされます。続行しますか?",
|
||||
"title": "翻訳確認"
|
||||
|
||||
@ -1624,6 +1624,10 @@
|
||||
"any.language": "Любой язык",
|
||||
"button.translate": "Перевести",
|
||||
"close": "Закрыть",
|
||||
"closed": "Перевод закрыт",
|
||||
"copied": "Содержимое перевода скопировано",
|
||||
"empty": "Содержимое перевода пусто",
|
||||
"not.found": "Содержимое перевода не найдено",
|
||||
"confirm": {
|
||||
"content": "Перевод заменит исходный текст, продолжить?",
|
||||
"title": "Перевод подтверждение"
|
||||
|
||||
@ -1624,6 +1624,10 @@
|
||||
"any.language": "任意语言",
|
||||
"button.translate": "翻译",
|
||||
"close": "关闭",
|
||||
"closed": "翻译已关闭",
|
||||
"copied": "翻译内容已复制",
|
||||
"empty": "翻译内容为空",
|
||||
"not.found": "未找到翻译内容",
|
||||
"confirm": {
|
||||
"content": "翻译后将覆盖原文,是否继续?",
|
||||
"title": "翻译确认"
|
||||
|
||||
@ -1624,6 +1624,10 @@
|
||||
"any.language": "任意語言",
|
||||
"button.translate": "翻譯",
|
||||
"close": "關閉",
|
||||
"closed": "翻譯已關閉",
|
||||
"copied": "翻譯內容已複製",
|
||||
"empty": "翻譯內容為空",
|
||||
"not.found": "未找到翻譯內容",
|
||||
"confirm": {
|
||||
"content": "翻譯後將覆蓋原文,是否繼續?",
|
||||
"title": "翻譯確認"
|
||||
|
||||
@ -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()
|
||||
}}
|
||||
|
||||
@ -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.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user