diff --git a/src/renderer/src/pages/home/Messages/Message.tsx b/src/renderer/src/pages/home/Messages/Message.tsx index 36fe2577..f7d41735 100644 --- a/src/renderer/src/pages/home/Messages/Message.tsx +++ b/src/renderer/src/pages/home/Messages/Message.tsx @@ -1,13 +1,4 @@ -import { - CheckOutlined, - DeleteOutlined, - EditOutlined, - ForkOutlined, - MenuOutlined, - QuestionCircleOutlined, - SaveOutlined, - SyncOutlined -} from '@ant-design/icons' +import { SyncOutlined } from '@ant-design/icons' import UserPopup from '@renderer/components/Popups/UserPopup' import { FONT_FAMILY } from '@renderer/config/constant' import { APP_NAME, AppLogo, isLocalAi } from '@renderer/config/env' @@ -17,62 +8,36 @@ import { useAssistant } from '@renderer/hooks/useAssistant' import useAvatar from '@renderer/hooks/useAvatar' import { useModel } from '@renderer/hooks/useModel' import { useSettings } from '@renderer/hooks/useSettings' -import { EVENT_NAMES, EventEmitter } from '@renderer/services/event' -import { Message, Model } from '@renderer/types' -import { firstLetter, removeLeadingEmoji, removeTrailingDoubleSpaces } from '@renderer/utils' -import { Alert, Avatar, Divider, Dropdown, Popconfirm, Tooltip } from 'antd' +import { Message } from '@renderer/types' +import { firstLetter, removeLeadingEmoji } from '@renderer/utils' +import { Alert, Avatar, Divider } from 'antd' import dayjs from 'dayjs' import { upperFirst } from 'lodash' -import { FC, memo, useCallback, useMemo, useState } from 'react' +import { FC, memo, useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' -import SelectModelDropdown from '../components/SelectModelDropdown' import Markdown from '../Markdown/Markdown' import MessageAttachments from './MessageAttachments' +import MessageMenubar from './MessageMenubar' import MessgeTokens from './MessageTokens' interface Props { message: Message index?: number total?: number - showMenu?: boolean onDeleteMessage?: (message: Message) => void } -const MessageItem: FC = ({ message, index, showMenu, onDeleteMessage }) => { +const MessageItem: FC = ({ message, index, onDeleteMessage }) => { const avatar = useAvatar() const { t } = useTranslation() const { assistant, setModel } = useAssistant(message.assistantId) const model = useModel(message.modelId) const { userName, showMessageDivider, messageFont, fontSize } = useSettings() - const [copied, setCopied] = useState(false) const isLastMessage = index === 0 - const isUserMessage = message.role === 'user' const isAssistantMessage = message.role === 'assistant' - const canRegenerate = isLastMessage && isAssistantMessage - - const onCopy = useCallback(() => { - navigator.clipboard.writeText(removeTrailingDoubleSpaces(message.content)) - window.message.success({ content: t('message.copied'), key: 'copy-message' }) - setCopied(true) - setTimeout(() => setCopied(false), 2000) - }, [message.content, t]) - - const onEdit = useCallback(() => EventEmitter.emit(EVENT_NAMES.EDIT_MESSAGE, message), [message]) - - const onRegenerate = useCallback( - (model: Model) => { - setModel(model) - setTimeout(() => EventEmitter.emit(EVENT_NAMES.REGENERATE_MESSAGE, model), 100) - }, - [setModel] - ) - - const onNewBranch = useCallback(() => { - EventEmitter.emit(EVENT_NAMES.NEW_BRANCH, index) - }, [index]) const getUserName = useCallback(() => { if (isLocalAi && message.role !== 'user') return APP_NAME @@ -95,21 +60,6 @@ const MessageItem: FC = ({ message, index, showMenu, onDeleteMessage }) = const username = useMemo(() => removeLeadingEmoji(getUserName()), [getUserName]) - const dropdownItems = useMemo( - () => [ - { - label: t('chat.save'), - key: 'save', - icon: , - onClick: () => { - const fileName = message.createdAt + '.md' - window.api.saveFile(fileName, message.content) - } - } - ], - [t, message] - ) - const showMiniApp = () => model?.provider && startMinAppById(model?.provider) if (message.type === 'clear') { @@ -154,57 +104,15 @@ const MessageItem: FC = ({ message, index, showMenu, onDeleteMessage }) = - {showMenu && ( - - {message.role === 'user' && ( - - - - - - )} - - - {!copied && } - {copied && } - - - {canRegenerate && ( - - - - - - - - )} - {isAssistantMessage && ( - - - - - - )} - } - onConfirm={() => onDeleteMessage?.(message)}> - - - - - - - {!isUserMessage && ( - - - - - - )} - - )} + @@ -252,11 +160,6 @@ const MessageContainer = styled.div` &.show { opacity: 1; } - &.user { - position: absolute; - top: 10px; - right: 15px; - } } &:hover { .menubar { @@ -323,40 +226,4 @@ const MessageContentLoading = styled.div` height: 32px; ` -const MenusBar = styled.div` - display: flex; - flex-direction: row; - justify-content: flex-end; - align-items: center; - gap: 6px; - margin-left: -5px; -` - -const ActionButton = styled.div` - cursor: pointer; - border-radius: 8px; - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - width: 30px; - height: 30px; - transition: all 0.3s ease; - &:hover { - background-color: var(--color-background-mute); - .anticon { - color: var(--color-text-1); - } - } - .anticon, - .iconfont { - cursor: pointer; - font-size: 14px; - color: var(--color-icon); - } - &:hover { - color: var(--color-text-1); - } -` - export default memo(MessageItem) diff --git a/src/renderer/src/pages/home/Messages/MessageMenubar.tsx b/src/renderer/src/pages/home/Messages/MessageMenubar.tsx new file mode 100644 index 00000000..a4f3fe36 --- /dev/null +++ b/src/renderer/src/pages/home/Messages/MessageMenubar.tsx @@ -0,0 +1,164 @@ +import { + CheckOutlined, + DeleteOutlined, + EditOutlined, + ForkOutlined, + MenuOutlined, + QuestionCircleOutlined, + SaveOutlined, + SyncOutlined +} from '@ant-design/icons' +import { EVENT_NAMES, EventEmitter } from '@renderer/services/event' +import { Message, Model } from '@renderer/types' +import { removeTrailingDoubleSpaces } from '@renderer/utils' +import { Dropdown, Popconfirm, Tooltip } from 'antd' +import { FC, useCallback, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' + +import SelectModelDropdown from '../components/SelectModelDropdown' + +interface Props { + message: Message + model?: Model + index?: number + isLastMessage: boolean + isAssistantMessage: boolean + setModel: (model: Model) => void + onDeleteMessage?: (message: Message) => void +} + +const MessageMenubar: FC = (props) => { + const { message, index, model, isLastMessage, isAssistantMessage, setModel, onDeleteMessage } = props + const { t } = useTranslation() + const [copied, setCopied] = useState(false) + + const isUserMessage = message.role === 'user' + const canRegenerate = isLastMessage && isAssistantMessage + + const onEdit = useCallback(() => EventEmitter.emit(EVENT_NAMES.EDIT_MESSAGE, message), [message]) + + const onCopy = useCallback(() => { + navigator.clipboard.writeText(removeTrailingDoubleSpaces(message.content)) + window.message.success({ content: t('message.copied'), key: 'copy-message' }) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + }, [message.content, t]) + + const onRegenerate = useCallback( + (model: Model) => { + setModel(model) + setTimeout(() => EventEmitter.emit(EVENT_NAMES.REGENERATE_MESSAGE, model), 100) + }, + [setModel] + ) + + const onNewBranch = useCallback(() => { + EventEmitter.emit(EVENT_NAMES.NEW_BRANCH, index) + }, [index]) + + const dropdownItems = useMemo( + () => [ + { + label: t('chat.save'), + key: 'save', + icon: , + onClick: () => { + const fileName = message.createdAt + '.md' + window.api.saveFile(fileName, message.content) + } + } + ], + [t, message] + ) + + return ( + + {message.role === 'user' && ( + + + + + + )} + + + {!copied && } + {copied && } + + + {canRegenerate && ( + + + + + + + + )} + {isAssistantMessage && ( + + + + + + )} + } + onConfirm={() => onDeleteMessage?.(message)}> + + + + + + + {!isUserMessage && ( + + + + + + )} + + ) +} + +const MenusBar = styled.div` + display: flex; + flex-direction: row; + justify-content: flex-end; + align-items: center; + gap: 6px; + margin-left: -5px; +` + +const ActionButton = styled.div` + cursor: pointer; + border-radius: 8px; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + width: 30px; + height: 30px; + transition: all 0.2s ease; + &:hover { + background-color: var(--color-background-mute); + .anticon { + color: var(--color-text-1); + } + } + .anticon, + .iconfont { + cursor: pointer; + font-size: 14px; + color: var(--color-icon); + } + &:hover { + color: var(--color-text-1); + } +` + +export default MessageMenubar diff --git a/src/renderer/src/pages/home/Messages/Messages.tsx b/src/renderer/src/pages/home/Messages/Messages.tsx index 1284cd66..fe4e71b2 100644 --- a/src/renderer/src/pages/home/Messages/Messages.tsx +++ b/src/renderer/src/pages/home/Messages/Messages.tsx @@ -185,7 +185,7 @@ const Messages: FC = ({ assistant, topic, setActiveTopic }) => { {lastMessage && } {reverse([...messages]).map((message, index) => ( - + ))}