refactor: message component
This commit is contained in:
parent
6845ee1664
commit
fa3d7f7f4a
@ -1,13 +1,4 @@
|
|||||||
import {
|
import { SyncOutlined } from '@ant-design/icons'
|
||||||
CheckOutlined,
|
|
||||||
DeleteOutlined,
|
|
||||||
EditOutlined,
|
|
||||||
ForkOutlined,
|
|
||||||
MenuOutlined,
|
|
||||||
QuestionCircleOutlined,
|
|
||||||
SaveOutlined,
|
|
||||||
SyncOutlined
|
|
||||||
} from '@ant-design/icons'
|
|
||||||
import UserPopup from '@renderer/components/Popups/UserPopup'
|
import UserPopup from '@renderer/components/Popups/UserPopup'
|
||||||
import { FONT_FAMILY } from '@renderer/config/constant'
|
import { FONT_FAMILY } from '@renderer/config/constant'
|
||||||
import { APP_NAME, AppLogo, isLocalAi } from '@renderer/config/env'
|
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 useAvatar from '@renderer/hooks/useAvatar'
|
||||||
import { useModel } from '@renderer/hooks/useModel'
|
import { useModel } from '@renderer/hooks/useModel'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
|
import { Message } from '@renderer/types'
|
||||||
import { Message, Model } from '@renderer/types'
|
import { firstLetter, removeLeadingEmoji } from '@renderer/utils'
|
||||||
import { firstLetter, removeLeadingEmoji, removeTrailingDoubleSpaces } from '@renderer/utils'
|
import { Alert, Avatar, Divider } from 'antd'
|
||||||
import { Alert, Avatar, Divider, Dropdown, Popconfirm, Tooltip } from 'antd'
|
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { upperFirst } from 'lodash'
|
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 { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import SelectModelDropdown from '../components/SelectModelDropdown'
|
|
||||||
import Markdown from '../Markdown/Markdown'
|
import Markdown from '../Markdown/Markdown'
|
||||||
import MessageAttachments from './MessageAttachments'
|
import MessageAttachments from './MessageAttachments'
|
||||||
|
import MessageMenubar from './MessageMenubar'
|
||||||
import MessgeTokens from './MessageTokens'
|
import MessgeTokens from './MessageTokens'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
message: Message
|
message: Message
|
||||||
index?: number
|
index?: number
|
||||||
total?: number
|
total?: number
|
||||||
showMenu?: boolean
|
|
||||||
onDeleteMessage?: (message: Message) => void
|
onDeleteMessage?: (message: Message) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) => {
|
const MessageItem: FC<Props> = ({ message, index, onDeleteMessage }) => {
|
||||||
const avatar = useAvatar()
|
const avatar = useAvatar()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { assistant, setModel } = useAssistant(message.assistantId)
|
const { assistant, setModel } = useAssistant(message.assistantId)
|
||||||
const model = useModel(message.modelId)
|
const model = useModel(message.modelId)
|
||||||
const { userName, showMessageDivider, messageFont, fontSize } = useSettings()
|
const { userName, showMessageDivider, messageFont, fontSize } = useSettings()
|
||||||
const [copied, setCopied] = useState(false)
|
|
||||||
|
|
||||||
const isLastMessage = index === 0
|
const isLastMessage = index === 0
|
||||||
const isUserMessage = message.role === 'user'
|
|
||||||
const isAssistantMessage = message.role === 'assistant'
|
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(() => {
|
const getUserName = useCallback(() => {
|
||||||
if (isLocalAi && message.role !== 'user') return APP_NAME
|
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 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)
|
const showMiniApp = () => model?.provider && startMinAppById(model?.provider)
|
||||||
|
|
||||||
if (message.type === 'clear') {
|
if (message.type === 'clear') {
|
||||||
@ -154,57 +104,15 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
|
|||||||
<MessageContent message={message} />
|
<MessageContent message={message} />
|
||||||
<MessageFooter style={{ border: messageBorder, flexDirection: isLastMessage ? 'row-reverse' : undefined }}>
|
<MessageFooter style={{ border: messageBorder, flexDirection: isLastMessage ? 'row-reverse' : undefined }}>
|
||||||
<MessgeTokens message={message} />
|
<MessgeTokens message={message} />
|
||||||
{showMenu && (
|
<MessageMenubar
|
||||||
<MenusBar className={`menubar ${isLastMessage && 'show'}`}>
|
message={message}
|
||||||
{message.role === 'user' && (
|
model={model}
|
||||||
<Tooltip title="Edit" mouseEnterDelay={0.8}>
|
index={index}
|
||||||
<ActionButton onClick={onEdit}>
|
isLastMessage={isLastMessage}
|
||||||
<EditOutlined />
|
isAssistantMessage={isAssistantMessage}
|
||||||
</ActionButton>
|
setModel={setModel}
|
||||||
</Tooltip>
|
onDeleteMessage={onDeleteMessage}
|
||||||
)}
|
/>
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
</MessageFooter>
|
</MessageFooter>
|
||||||
</MessageContentContainer>
|
</MessageContentContainer>
|
||||||
</MessageContainer>
|
</MessageContainer>
|
||||||
@ -252,11 +160,6 @@ const MessageContainer = styled.div`
|
|||||||
&.show {
|
&.show {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
&.user {
|
|
||||||
position: absolute;
|
|
||||||
top: 10px;
|
|
||||||
right: 15px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
&:hover {
|
&:hover {
|
||||||
.menubar {
|
.menubar {
|
||||||
@ -323,40 +226,4 @@ const MessageContentLoading = styled.div`
|
|||||||
height: 32px;
|
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)
|
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} />
|
<Suggestions assistant={assistant} messages={messages} lastMessage={lastMessage} />
|
||||||
{lastMessage && <MessageItem key={lastMessage.id} message={lastMessage} />}
|
{lastMessage && <MessageItem key={lastMessage.id} message={lastMessage} />}
|
||||||
{reverse([...messages]).map((message, index) => (
|
{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} />
|
<Prompt assistant={assistant} key={assistant.prompt} />
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user