mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-12 00:49:14 +08:00
feat: 添加消息菜单栏配置和按钮渲染逻辑
This commit is contained in:
parent
f5a41e9c78
commit
56dbe6b050
60
src/renderer/src/config/registry/messageMenubar.ts
Normal file
60
src/renderer/src/config/registry/messageMenubar.ts
Normal 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
|
||||
}
|
||||
@ -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)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user