feat: 添加消息菜单栏配置和按钮渲染逻辑

This commit is contained in:
suyao 2025-09-23 05:05:31 +08:00
parent f5a41e9c78
commit 56dbe6b050
No known key found for this signature in database
2 changed files with 526 additions and 258 deletions

View File

@ -0,0 +1,60 @@
import { TopicType } from '@renderer/types'
export type MessageMenubarScope = TopicType
export type MessageMenubarButtonId =
| 'user-regenerate'
| 'user-edit'
| 'copy'
| 'assistant-regenerate'
| 'assistant-mention-model'
| 'translate'
| 'useful'
| 'notes'
| 'delete'
| 'trace'
| 'more-menu'
export type MessageMenubarScopeConfig = {
buttonIds: MessageMenubarButtonId[]
dropdownRootAllowKeys?: string[]
}
export const DEFAULT_MESSAGE_MENUBAR_SCOPE: MessageMenubarScope = TopicType.Chat
export const DEFAULT_MESSAGE_MENUBAR_BUTTON_IDS: MessageMenubarButtonId[] = [
'user-regenerate',
'user-edit',
'copy',
'assistant-regenerate',
'assistant-mention-model',
'translate',
'useful',
'notes',
'delete',
'trace',
'more-menu'
]
export const SESSION_MESSAGE_MENUBAR_BUTTON_IDS: MessageMenubarButtonId[] = ['copy', 'translate', 'notes', 'more-menu']
const messageMenubarRegistry = new Map<MessageMenubarScope, MessageMenubarScopeConfig>([
[DEFAULT_MESSAGE_MENUBAR_SCOPE, { buttonIds: [...DEFAULT_MESSAGE_MENUBAR_BUTTON_IDS] }],
[TopicType.Chat, { buttonIds: [...DEFAULT_MESSAGE_MENUBAR_BUTTON_IDS] }],
[TopicType.Session, { buttonIds: [...SESSION_MESSAGE_MENUBAR_BUTTON_IDS], dropdownRootAllowKeys: ['save', 'export'] }]
])
export const registerMessageMenubarConfig = (scope: MessageMenubarScope, config: MessageMenubarScopeConfig) => {
const clonedConfig: MessageMenubarScopeConfig = {
buttonIds: [...config.buttonIds],
dropdownRootAllowKeys: config.dropdownRootAllowKeys ? [...config.dropdownRootAllowKeys] : undefined
}
messageMenubarRegistry.set(scope, clonedConfig)
}
export const getMessageMenubarConfig = (scope: MessageMenubarScope): MessageMenubarScopeConfig => {
if (messageMenubarRegistry.has(scope)) {
return messageMenubarRegistry.get(scope) as MessageMenubarScopeConfig
}
return messageMenubarRegistry.get(DEFAULT_MESSAGE_MENUBAR_SCOPE) as MessageMenubarScopeConfig
}

View File

@ -5,6 +5,12 @@ import ObsidianExportPopup from '@renderer/components/Popups/ObsidianExportPopup
import SaveToKnowledgePopup from '@renderer/components/Popups/SaveToKnowledgePopup'
import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup'
import { isEmbeddingModel, isRerankModel, isVisionModel } from '@renderer/config/models'
import {
DEFAULT_MESSAGE_MENUBAR_SCOPE,
getMessageMenubarConfig,
MessageMenubarButtonId,
MessageMenubarScope
} from '@renderer/config/registry/messageMenubar'
import { useMessageEditing } from '@renderer/context/MessageEditingContext'
import { useChatContext } from '@renderer/hooks/useChatContext'
import { useMessageOperations } from '@renderer/hooks/useMessageOperations'
@ -40,8 +46,10 @@ import {
findTranslationBlocksById,
getMainTextContent
} from '@renderer/utils/messageUtils/find'
import type { MenuProps } from 'antd'
import { Dropdown, Popconfirm, Tooltip } from 'antd'
import dayjs from 'dayjs'
import type { TFunction } from 'i18next'
import {
AtSign,
Check,
@ -55,7 +63,8 @@ import {
ThumbsUp,
Upload
} from 'lucide-react'
import { FC, memo, useCallback, useMemo, useState } from 'react'
import type { Dispatch, ReactNode, SetStateAction } from 'react'
import { FC, Fragment, memo, useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import styled from 'styled-components'
@ -78,6 +87,43 @@ interface Props {
const logger = loggerService.withContext('MessageMenubar')
type MessageOperationsHandlers = ReturnType<typeof useMessageOperations>
type MessageMenubarButtonContext = {
assistant: Assistant
blockEntities: ReturnType<typeof messageBlocksSelectors.selectEntities>
confirmDeleteMessage: boolean
confirmRegenerateMessage: boolean
copied: boolean
deleteMessage: MessageOperationsHandlers['deleteMessage']
dropdownItems: MenuProps['items']
enableDeveloperMode: boolean
handleResendUserMessage: (messageUpdate?: Message) => Promise<void>
handleTraceUserMessage: () => void | Promise<void>
handleTranslate: (language: TranslateLanguage) => Promise<void>
hasTranslationBlocks: boolean
isAssistantMessage: boolean
isBubbleStyle: boolean
isGrouped?: boolean
isLastMessage: boolean
isUserMessage: boolean
message: Message
notesPath: string
onCopy: (e: React.MouseEvent) => void
onEdit: () => void | Promise<void>
onMentionModel: (e: React.MouseEvent) => void | Promise<void>
onRegenerate: (e?: React.MouseEvent) => void | Promise<void>
onUseful: (e: React.MouseEvent) => void
removeMessageBlock: MessageOperationsHandlers['removeMessageBlock']
setShowDeleteTooltip: Dispatch<SetStateAction<boolean>>
showDeleteTooltip: boolean
softHoverBg: boolean
t: TFunction
translateLanguages: TranslateLanguage[]
}
type MessageMenubarButtonRenderer = (ctx: MessageMenubarButtonContext) => ReactNode | null
const MessageMenubar: FC<Props> = (props) => {
const {
message,
@ -217,12 +263,15 @@ const MessageMenubar: FC<Props> = (props) => {
}
}, [message])
const menubarScope: MessageMenubarScope = topic?.type ?? DEFAULT_MESSAGE_MENUBAR_SCOPE
const { buttonIds, dropdownRootAllowKeys } = getMessageMenubarConfig(menubarScope)
const isEditable = useMemo(() => {
return findMainTextBlocks(message).length > 0 // 使用 MCP Server 后会有大于一段 MatinTextBlock
}, [message])
const dropdownItems = useMemo(
() => [
const dropdownItems = useMemo(() => {
const items: MenuProps['items'] = [
...(isEditable
? [
{
@ -342,7 +391,7 @@ const MessageMenubar: FC<Props> = (props) => {
label: t('chat.topics.export.obsidian'),
key: 'obsidian',
onClick: async () => {
const title = topic.name?.replace(/\//g, '_') || 'Untitled'
const title = topic.name?.replace(/\\/g, '_') || 'Untitled'
await ObsidianExportPopup.show({ title, message, processingMethod: '1' })
}
},
@ -365,29 +414,47 @@ const MessageMenubar: FC<Props> = (props) => {
}
].filter(Boolean)
}
],
[
isEditable,
t,
onEdit,
onNewBranch,
exportMenuOptions.plain_text,
exportMenuOptions.image,
exportMenuOptions.markdown,
exportMenuOptions.markdown_reason,
exportMenuOptions.docx,
exportMenuOptions.notion,
exportMenuOptions.yuque,
exportMenuOptions.obsidian,
exportMenuOptions.joplin,
exportMenuOptions.siyuan,
toggleMultiSelectMode,
message,
mainTextContent,
messageContainerRef,
topic.name
]
)
].filter(Boolean)
if (!dropdownRootAllowKeys || dropdownRootAllowKeys.length === 0) {
return items
}
const allowSet = new Set(dropdownRootAllowKeys)
return items.filter((item) => {
if (!item || typeof item !== 'object') {
return false
}
if ('type' in item && item.type === 'divider') {
return false
}
if ('key' in item && item.key) {
return allowSet.has(String(item.key))
}
return false
})
}, [
dropdownRootAllowKeys,
exportMenuOptions.docx,
exportMenuOptions.image,
exportMenuOptions.joplin,
exportMenuOptions.markdown,
exportMenuOptions.markdown_reason,
exportMenuOptions.notion,
exportMenuOptions.obsidian,
exportMenuOptions.plain_text,
exportMenuOptions.siyuan,
exportMenuOptions.yuque,
isEditable,
mainTextContent,
message,
messageContainerRef,
onEdit,
onNewBranch,
t,
toggleMultiSelectMode,
topic.name
])
const onRegenerate = async (e: React.MouseEvent | undefined) => {
e?.stopPropagation?.()
@ -461,237 +528,56 @@ const MessageMenubar: FC<Props> = (props) => {
const showMessageTokens = !isBubbleStyle
const isUserBubbleStyleMessage = isBubbleStyle && isUserMessage
const buttonContext: MessageMenubarButtonContext = {
assistant,
blockEntities,
confirmDeleteMessage,
confirmRegenerateMessage,
copied,
deleteMessage,
dropdownItems,
enableDeveloperMode,
handleResendUserMessage,
handleTraceUserMessage,
handleTranslate,
hasTranslationBlocks,
isAssistantMessage,
isBubbleStyle,
isGrouped,
isLastMessage,
isUserMessage,
message,
notesPath,
onCopy,
onEdit,
onMentionModel,
onRegenerate,
onUseful,
removeMessageBlock,
setShowDeleteTooltip,
showDeleteTooltip,
softHoverBg,
t,
translateLanguages
}
return (
<>
{showMessageTokens && <MessageTokens message={message} />}
<MenusBar
className={classNames({ menubar: true, show: isLastMessage, 'user-bubble-style': isUserBubbleStyleMessage })}>
{message.role === 'user' &&
(confirmRegenerateMessage ? (
<Popconfirm
title={t('message.regenerate.confirm')}
okButtonProps={{ danger: true }}
onConfirm={() => handleResendUserMessage()}
onOpenChange={(open) => open && setShowDeleteTooltip(false)}>
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
<ActionButton
className="message-action-button"
onClick={(e) => e.stopPropagation()}
$softHoverBg={isBubbleStyle}>
<RefreshIcon size={15} />
</ActionButton>
</Tooltip>
</Popconfirm>
) : (
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
<ActionButton
className="message-action-button"
onClick={() => handleResendUserMessage()}
$softHoverBg={isBubbleStyle}>
<RefreshIcon size={15} />
</ActionButton>
</Tooltip>
))}
{message.role === 'user' && (
<Tooltip title={t('common.edit')} mouseEnterDelay={0.8}>
<ActionButton className="message-action-button" onClick={onEdit} $softHoverBg={softHoverBg}>
<EditIcon size={15} />
</ActionButton>
</Tooltip>
)}
<Tooltip title={t('common.copy')} mouseEnterDelay={0.8}>
<ActionButton className="message-action-button" onClick={onCopy} $softHoverBg={softHoverBg}>
{!copied && <CopyIcon size={15} />}
{copied && <Check size={15} color="var(--color-primary)" />}
</ActionButton>
</Tooltip>
{isAssistantMessage &&
(confirmRegenerateMessage ? (
<Popconfirm
title={t('message.regenerate.confirm')}
okButtonProps={{ danger: true }}
onConfirm={onRegenerate}
onOpenChange={(open) => open && setShowDeleteTooltip(false)}>
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
<ActionButton
className="message-action-button"
onClick={(e) => e.stopPropagation()}
$softHoverBg={softHoverBg}>
<RefreshIcon size={15} />
</ActionButton>
</Tooltip>
</Popconfirm>
) : (
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
<ActionButton className="message-action-button" onClick={onRegenerate} $softHoverBg={softHoverBg}>
<RefreshIcon size={15} />
</ActionButton>
</Tooltip>
))}
{isAssistantMessage && (
<Tooltip title={t('message.mention.title')} mouseEnterDelay={0.8}>
<ActionButton className="message-action-button" onClick={onMentionModel} $softHoverBg={softHoverBg}>
<AtSign size={15} />
</ActionButton>
</Tooltip>
)}
{!isUserMessage && (
<Dropdown
menu={{
style: {
maxHeight: 250,
overflowY: 'auto',
backgroundClip: 'border-box'
},
items: [
...translateLanguages.map((item) => ({
label: item.emoji + ' ' + item.label(),
key: item.langCode,
onClick: () => handleTranslate(item)
})),
...(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.toast.success(t('translate.copied'))
} else {
window.toast.warning(t('translate.empty'))
}
}
}
},
{
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.toast.success(t('translate.closed'))
}
}
}
]
: [])
],
onClick: (e) => e.domEvent.stopPropagation()
}}
trigger={['click']}
placement="top"
arrow>
<Tooltip title={t('chat.translate')} mouseEnterDelay={1.2}>
<ActionButton
className="message-action-button"
onClick={(e) => e.stopPropagation()}
$softHoverBg={softHoverBg}>
<Languages size={15} />
</ActionButton>
</Tooltip>
</Dropdown>
)}
{isAssistantMessage && isGrouped && (
<Tooltip title={t('chat.message.useful.label')} mouseEnterDelay={0.8}>
<ActionButton className="message-action-button" onClick={onUseful} $softHoverBg={softHoverBg}>
{message.useful ? (
<ThumbsUp size={17.5} fill="var(--color-primary)" strokeWidth={0} />
) : (
<ThumbsUp size={15} />
)}
</ActionButton>
</Tooltip>
)}
{isAssistantMessage && (
<Tooltip title={t('notes.save')} mouseEnterDelay={0.8}>
<ActionButton
className="message-action-button"
onClick={async (e) => {
e.stopPropagation()
const title = await getMessageTitle(message)
const markdown = messageToMarkdown(message)
exportMessageToNotes(title, markdown, notesPath)
}}
$softHoverBg={softHoverBg}>
<NotebookPen size={15} />
</ActionButton>
</Tooltip>
)}
{confirmDeleteMessage ? (
<Popconfirm
title={t('message.message.delete.content')}
okButtonProps={{ danger: true }}
onConfirm={() => deleteMessage(message.id, message.traceId, message.model?.name)}
onOpenChange={(open) => open && setShowDeleteTooltip(false)}>
<ActionButton
className="message-action-button"
onClick={(e) => e.stopPropagation()}
$softHoverBg={softHoverBg}>
<Tooltip
title={t('common.delete')}
mouseEnterDelay={1}
open={showDeleteTooltip}
onOpenChange={setShowDeleteTooltip}>
<DeleteIcon size={15} />
</Tooltip>
</ActionButton>
</Popconfirm>
) : (
<ActionButton
className="message-action-button"
onClick={(e) => {
e.stopPropagation()
deleteMessage(message.id, message.traceId, message.model?.name)
}}
$softHoverBg={softHoverBg}>
<Tooltip
title={t('common.delete')}
mouseEnterDelay={1}
open={showDeleteTooltip}
onOpenChange={setShowDeleteTooltip}>
<DeleteIcon size={15} />
</Tooltip>
</ActionButton>
)}
{enableDeveloperMode && message.traceId && (
<Tooltip title={t('trace.label')} mouseEnterDelay={0.8}>
<ActionButton className="message-action-button" onClick={() => handleTraceUserMessage()}>
<TraceIcon size={16} className={'lucide lucide-trash'} />
</ActionButton>
</Tooltip>
)}
{!isUserMessage && (
<Dropdown
menu={{ items: dropdownItems, onClick: (e) => e.domEvent.stopPropagation() }}
trigger={['click']}
placement="topRight">
<ActionButton
className="message-action-button"
onClick={(e) => e.stopPropagation()}
$softHoverBg={softHoverBg}>
<Menu size={19} />
</ActionButton>
</Dropdown>
)}
{buttonIds.map((buttonId) => {
const renderFn = buttonRenderers[buttonId]
if (!renderFn) {
logger.warn(`No renderer registered for MessageMenubar button id: ${buttonId}`)
return null
}
const element = renderFn(buttonContext)
if (!element) {
return null
}
return <Fragment key={buttonId}>{element}</Fragment>
})}
</MenusBar>
</>
)
@ -739,10 +625,332 @@ const ActionButton = styled.div<{ $softHoverBg?: boolean }>`
}
`
// const ReSendButton = styled(Button)`
// position: absolute;
// top: 10px;
// left: 0;
// `
const buttonRenderers: Record<MessageMenubarButtonId, MessageMenubarButtonRenderer> = {
'user-regenerate': ({
message,
confirmRegenerateMessage,
handleResendUserMessage,
setShowDeleteTooltip,
t,
isBubbleStyle
}) => {
if (message.role !== 'user') {
return null
}
if (confirmRegenerateMessage) {
return (
<Popconfirm
title={t('message.regenerate.confirm')}
okButtonProps={{ danger: true }}
onConfirm={() => handleResendUserMessage()}
onOpenChange={(open) => open && setShowDeleteTooltip(false)}>
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
<ActionButton
className="message-action-button"
onClick={(e) => e.stopPropagation()}
$softHoverBg={isBubbleStyle}>
<RefreshIcon size={15} />
</ActionButton>
</Tooltip>
</Popconfirm>
)
}
return (
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
<ActionButton
className="message-action-button"
onClick={() => handleResendUserMessage()}
$softHoverBg={isBubbleStyle}>
<RefreshIcon size={15} />
</ActionButton>
</Tooltip>
)
},
'user-edit': ({ message, onEdit, softHoverBg, t }) => {
if (message.role !== 'user') {
return null
}
return (
<Tooltip title={t('common.edit')} mouseEnterDelay={0.8}>
<ActionButton className="message-action-button" onClick={onEdit} $softHoverBg={softHoverBg}>
<EditIcon size={15} />
</ActionButton>
</Tooltip>
)
},
copy: ({ onCopy, softHoverBg, copied, t }) => (
<Tooltip title={t('common.copy')} mouseEnterDelay={0.8}>
<ActionButton className="message-action-button" onClick={onCopy} $softHoverBg={softHoverBg}>
{!copied && <CopyIcon size={15} />}
{copied && <Check size={15} color="var(--color-primary)" />}
</ActionButton>
</Tooltip>
),
'assistant-regenerate': ({
isAssistantMessage,
confirmRegenerateMessage,
onRegenerate,
setShowDeleteTooltip,
softHoverBg,
t
}) => {
if (!isAssistantMessage) {
return null
}
if (confirmRegenerateMessage) {
return (
<Popconfirm
title={t('message.regenerate.confirm')}
okButtonProps={{ danger: true }}
onConfirm={() => onRegenerate()}
onOpenChange={(open) => open && setShowDeleteTooltip(false)}>
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
<ActionButton
className="message-action-button"
onClick={(e) => e.stopPropagation()}
$softHoverBg={softHoverBg}>
<RefreshIcon size={15} />
</ActionButton>
</Tooltip>
</Popconfirm>
)
}
return (
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
<ActionButton className="message-action-button" onClick={onRegenerate} $softHoverBg={softHoverBg}>
<RefreshIcon size={15} />
</ActionButton>
</Tooltip>
)
},
'assistant-mention-model': ({ isAssistantMessage, onMentionModel, softHoverBg, t }) => {
if (!isAssistantMessage) {
return null
}
return (
<Tooltip title={t('message.mention.title')} mouseEnterDelay={0.8}>
<ActionButton className="message-action-button" onClick={onMentionModel} $softHoverBg={softHoverBg}>
<AtSign size={15} />
</ActionButton>
</Tooltip>
)
},
translate: ({
isUserMessage,
translateLanguages,
handleTranslate,
hasTranslationBlocks,
message,
blockEntities,
removeMessageBlock,
softHoverBg,
t
}) => {
if (isUserMessage) {
return null
}
const items: MenuProps['items'] = [
...translateLanguages.map((item) => ({
label: item.emoji + ' ' + item.label(),
key: item.langCode,
onClick: () => handleTranslate(item)
})),
...(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.toast.success(t('translate.copied'))
} else {
window.toast.warning(t('translate.empty'))
}
}
}
},
{
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.toast.success(t('translate.closed'))
}
}
}
]
: [])
]
return (
<Dropdown
menu={{
style: {
maxHeight: 250,
overflowY: 'auto',
backgroundClip: 'border-box'
},
items,
onClick: (e) => e.domEvent.stopPropagation()
}}
trigger={['click']}
placement="top"
arrow>
<Tooltip title={t('chat.translate')} mouseEnterDelay={1.2}>
<ActionButton
className="message-action-button"
onClick={(e) => e.stopPropagation()}
$softHoverBg={softHoverBg}>
<Languages size={15} />
</ActionButton>
</Tooltip>
</Dropdown>
)
},
useful: ({ isAssistantMessage, isGrouped, onUseful, softHoverBg, message, t }) => {
if (!isAssistantMessage || !isGrouped) {
return null
}
return (
<Tooltip title={t('chat.message.useful.label')} mouseEnterDelay={0.8}>
<ActionButton className="message-action-button" onClick={onUseful} $softHoverBg={softHoverBg}>
{message.useful ? (
<ThumbsUp size={17.5} fill="var(--color-primary)" strokeWidth={0} />
) : (
<ThumbsUp size={15} />
)}
</ActionButton>
</Tooltip>
)
},
notes: ({ isAssistantMessage, softHoverBg, message, notesPath, t }) => {
if (!isAssistantMessage) {
return null
}
return (
<Tooltip title={t('notes.save')} mouseEnterDelay={0.8}>
<ActionButton
className="message-action-button"
onClick={async (e) => {
e.stopPropagation()
const title = await getMessageTitle(message)
const markdown = messageToMarkdown(message)
exportMessageToNotes(title, markdown, notesPath)
}}
$softHoverBg={softHoverBg}>
<NotebookPen size={15} />
</ActionButton>
</Tooltip>
)
},
delete: ({
confirmDeleteMessage,
deleteMessage,
message,
setShowDeleteTooltip,
showDeleteTooltip,
softHoverBg,
t
}) => {
const deleteTooltip = (
<Tooltip
title={t('common.delete')}
mouseEnterDelay={1}
open={showDeleteTooltip}
onOpenChange={setShowDeleteTooltip}>
<DeleteIcon size={15} />
</Tooltip>
)
if (confirmDeleteMessage) {
return (
<Popconfirm
title={t('message.message.delete.content')}
okButtonProps={{ danger: true }}
onConfirm={() => deleteMessage(message.id, message.traceId, message.model?.name)}
onOpenChange={(open) => open && setShowDeleteTooltip(false)}>
<ActionButton
className="message-action-button"
onClick={(e) => e.stopPropagation()}
$softHoverBg={softHoverBg}>
{deleteTooltip}
</ActionButton>
</Popconfirm>
)
}
return (
<ActionButton
className="message-action-button"
onClick={(e) => {
e.stopPropagation()
deleteMessage(message.id, message.traceId, message.model?.name)
}}
$softHoverBg={softHoverBg}>
{deleteTooltip}
</ActionButton>
)
},
trace: ({ enableDeveloperMode, message, handleTraceUserMessage, t }) => {
if (!enableDeveloperMode || !message.traceId) {
return null
}
return (
<Tooltip title={t('trace.label')} mouseEnterDelay={0.8}>
<ActionButton className="message-action-button" onClick={() => handleTraceUserMessage()}>
<TraceIcon size={16} className={'lucide lucide-trash'} />
</ActionButton>
</Tooltip>
)
},
'more-menu': ({ isUserMessage, dropdownItems, softHoverBg }) => {
if (isUserMessage) {
return null
}
return (
<Dropdown
menu={{ items: dropdownItems, onClick: (e) => e.domEvent.stopPropagation() }}
trigger={['click']}
placement="topRight">
<ActionButton className="message-action-button" onClick={(e) => e.stopPropagation()} $softHoverBg={softHoverBg}>
<Menu size={19} />
</ActionButton>
</Dropdown>
)
}
}
export default memo(MessageMenubar)