refactor: message component
This commit is contained in:
parent
6845ee1664
commit
fa3d7f7f4a
@ -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<Props> = ({ message, index, showMenu, onDeleteMessage }) => {
|
||||
const MessageItem: FC<Props> = ({ 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<Props> = ({ message, index, showMenu, onDeleteMessage }) =
|
||||
|
||||
const username = useMemo(() => removeLeadingEmoji(getUserName()), [getUserName])
|
||||
|
||||
const dropdownItems = useMemo(
|
||||
() => [
|
||||
{
|
||||
label: t('chat.save'),
|
||||
key: 'save',
|
||||
icon: <SaveOutlined />,
|
||||
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<Props> = ({ message, index, showMenu, onDeleteMessage }) =
|
||||
<MessageContent message={message} />
|
||||
<MessageFooter style={{ border: messageBorder, flexDirection: isLastMessage ? 'row-reverse' : undefined }}>
|
||||
<MessgeTokens message={message} />
|
||||
{showMenu && (
|
||||
<MenusBar className={`menubar ${isLastMessage && 'show'}`}>
|
||||
{message.role === 'user' && (
|
||||
<Tooltip title="Edit" mouseEnterDelay={0.8}>
|
||||
<ActionButton onClick={onEdit}>
|
||||
<EditOutlined />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip title={t('common.copy')} mouseEnterDelay={0.8}>
|
||||
<ActionButton onClick={onCopy}>
|
||||
{!copied && <i className="iconfont icon-copy"></i>}
|
||||
{copied && <CheckOutlined style={{ color: 'var(--color-primary)' }} />}
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
{canRegenerate && (
|
||||
<SelectModelDropdown model={model} onSelect={onRegenerate} placement="topLeft">
|
||||
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
|
||||
<ActionButton>
|
||||
<SyncOutlined />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
</SelectModelDropdown>
|
||||
)}
|
||||
{isAssistantMessage && (
|
||||
<Tooltip title={t('chat.message.new.branch')} mouseEnterDelay={0.8}>
|
||||
<ActionButton onClick={onNewBranch}>
|
||||
<ForkOutlined />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Popconfirm
|
||||
title={t('message.message.delete.content')}
|
||||
okButtonProps={{ danger: true }}
|
||||
icon={<QuestionCircleOutlined style={{ color: 'red' }} />}
|
||||
onConfirm={() => onDeleteMessage?.(message)}>
|
||||
<Tooltip title={t('common.delete')} mouseEnterDelay={1}>
|
||||
<ActionButton>
|
||||
<DeleteOutlined />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
</Popconfirm>
|
||||
{!isUserMessage && (
|
||||
<Dropdown menu={{ items: dropdownItems }} trigger={['click']} placement="topRight" arrow>
|
||||
<ActionButton>
|
||||
<MenuOutlined />
|
||||
</ActionButton>
|
||||
</Dropdown>
|
||||
)}
|
||||
</MenusBar>
|
||||
)}
|
||||
<MessageMenubar
|
||||
message={message}
|
||||
model={model}
|
||||
index={index}
|
||||
isLastMessage={isLastMessage}
|
||||
isAssistantMessage={isAssistantMessage}
|
||||
setModel={setModel}
|
||||
onDeleteMessage={onDeleteMessage}
|
||||
/>
|
||||
</MessageFooter>
|
||||
</MessageContentContainer>
|
||||
</MessageContainer>
|
||||
@ -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)
|
||||
|
||||
164
src/renderer/src/pages/home/Messages/MessageMenubar.tsx
Normal file
164
src/renderer/src/pages/home/Messages/MessageMenubar.tsx
Normal file
@ -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> = (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: <SaveOutlined />,
|
||||
onClick: () => {
|
||||
const fileName = message.createdAt + '.md'
|
||||
window.api.saveFile(fileName, message.content)
|
||||
}
|
||||
}
|
||||
],
|
||||
[t, message]
|
||||
)
|
||||
|
||||
return (
|
||||
<MenusBar className={`menubar ${isLastMessage && 'show'}`}>
|
||||
{message.role === 'user' && (
|
||||
<Tooltip title="Edit" mouseEnterDelay={0.8}>
|
||||
<ActionButton onClick={onEdit}>
|
||||
<EditOutlined />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip title={t('common.copy')} mouseEnterDelay={0.8}>
|
||||
<ActionButton onClick={onCopy}>
|
||||
{!copied && <i className="iconfont icon-copy"></i>}
|
||||
{copied && <CheckOutlined style={{ color: 'var(--color-primary)' }} />}
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
{canRegenerate && (
|
||||
<SelectModelDropdown model={model} onSelect={onRegenerate} placement="topLeft">
|
||||
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
|
||||
<ActionButton>
|
||||
<SyncOutlined />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
</SelectModelDropdown>
|
||||
)}
|
||||
{isAssistantMessage && (
|
||||
<Tooltip title={t('chat.message.new.branch')} mouseEnterDelay={0.8}>
|
||||
<ActionButton onClick={onNewBranch}>
|
||||
<ForkOutlined />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Popconfirm
|
||||
title={t('message.message.delete.content')}
|
||||
okButtonProps={{ danger: true }}
|
||||
icon={<QuestionCircleOutlined style={{ color: 'red' }} />}
|
||||
onConfirm={() => onDeleteMessage?.(message)}>
|
||||
<Tooltip title={t('common.delete')} mouseEnterDelay={1}>
|
||||
<ActionButton>
|
||||
<DeleteOutlined />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
</Popconfirm>
|
||||
{!isUserMessage && (
|
||||
<Dropdown menu={{ items: dropdownItems }} trigger={['click']} placement="topRight" arrow>
|
||||
<ActionButton>
|
||||
<MenuOutlined />
|
||||
</ActionButton>
|
||||
</Dropdown>
|
||||
)}
|
||||
</MenusBar>
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
@ -185,7 +185,7 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
|
||||
<Suggestions assistant={assistant} messages={messages} lastMessage={lastMessage} />
|
||||
{lastMessage && <MessageItem key={lastMessage.id} message={lastMessage} />}
|
||||
{reverse([...messages]).map((message, index) => (
|
||||
<MessageItem key={message.id} message={message} showMenu index={index} onDeleteMessage={onDeleteMessage} />
|
||||
<MessageItem key={message.id} message={message} index={index} onDeleteMessage={onDeleteMessage} />
|
||||
))}
|
||||
<Prompt assistant={assistant} key={assistant.prompt} />
|
||||
</Container>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user