mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-28 05:11:24 +08:00
perf(messages): usememo & usecallback message component
This commit is contained in:
parent
cda105a568
commit
1c17e104b1
@ -40,7 +40,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
||||
const inputRef = useRef<TextAreaRef>(null)
|
||||
const { t } = useTranslation()
|
||||
|
||||
const sendMessage = () => {
|
||||
const sendMessage = useCallback(() => {
|
||||
if (generating) {
|
||||
return
|
||||
}
|
||||
@ -64,19 +64,17 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
||||
setText('')
|
||||
|
||||
setExpend(false)
|
||||
}
|
||||
}, [assistant.id, assistant.topics, generating, text])
|
||||
|
||||
const inputTokenCount = useMemo(() => estimateInputTokenCount(text), [text])
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (expended) {
|
||||
if (event.key === 'Escape') {
|
||||
setExpend(false)
|
||||
return
|
||||
return setExpend(false)
|
||||
}
|
||||
if (event.key === 'Enter' && event.shiftKey) {
|
||||
sendMessage()
|
||||
return
|
||||
return sendMessage()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@ -18,7 +18,7 @@ import { firstLetter, removeLeadingEmoji } from '@renderer/utils'
|
||||
import { Avatar, Dropdown, Tooltip } from 'antd'
|
||||
import dayjs from 'dayjs'
|
||||
import { upperFirst } from 'lodash'
|
||||
import { FC, useCallback, useMemo, useState } from 'react'
|
||||
import { FC, memo, useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
import Markdown from './markdown/Markdown'
|
||||
@ -41,7 +41,8 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
|
||||
|
||||
const isLastMessage = index === 0
|
||||
const isUserMessage = message.role === 'user'
|
||||
const canRegenerate = isLastMessage && message.role === 'assistant'
|
||||
const isAssistantMessage = message.role === 'assistant'
|
||||
const canRegenerate = isLastMessage && isAssistantMessage
|
||||
|
||||
const onCopy = useCallback(() => {
|
||||
navigator.clipboard.writeText(message.content)
|
||||
@ -69,128 +70,100 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
|
||||
}, [message, onDeleteMessage])
|
||||
|
||||
const getUserName = useCallback(() => {
|
||||
if (message.id === 'assistant') {
|
||||
return assistant?.name
|
||||
}
|
||||
|
||||
if (message.role === 'assistant') {
|
||||
return upperFirst(message.modelId)
|
||||
}
|
||||
|
||||
if (message.id === 'assistant') return assistant?.name
|
||||
if (message.role === 'assistant') return upperFirst(message.modelId)
|
||||
return userName || t('common.you')
|
||||
}, [assistant?.name, message.id, message.modelId, message.role, t, userName])
|
||||
|
||||
const getDropdownMenus = useCallback(
|
||||
(message: Message) => {
|
||||
return [
|
||||
{
|
||||
label: t('chat.save'),
|
||||
key: 'save',
|
||||
icon: <SaveOutlined />,
|
||||
onClick: () => {
|
||||
const fileName = message.createdAt + '.md'
|
||||
window.api.saveFile(fileName, message.content)
|
||||
}
|
||||
const serifFonts = "Georgia, Cambria, 'Times New Roman', Times, serif"
|
||||
const fontFamily = messageFont === 'serif' ? serifFonts : 'Poppins, -apple-system, sans-serif'
|
||||
const messageBorder = showMessageDivider ? undefined : 'none'
|
||||
const avatarSource = useMemo(() => (message.modelId ? getModelLogo(message.modelId) : undefined), [message.modelId])
|
||||
const avatarName = useMemo(() => firstLetter(assistant?.name).toUpperCase(), [assistant?.name])
|
||||
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]
|
||||
}
|
||||
],
|
||||
[t, message]
|
||||
)
|
||||
|
||||
const fontFamily =
|
||||
messageFont === 'serif' ? "Georgia, Cambria, 'Times New Roman', Times, serif" : 'Poppins, -apple-system, sans-serif'
|
||||
|
||||
const messageBorder = showMessageDivider ? undefined : 'none'
|
||||
|
||||
return useMemo(
|
||||
() => (
|
||||
<MessageContainer key={message.id} className="message" style={{ border: messageBorder }}>
|
||||
<MessageHeader>
|
||||
<AvatarWrapper>
|
||||
{message.role === 'assistant' ? (
|
||||
<Avatar src={message.modelId ? getModelLogo(message.modelId) : undefined} size={35}>
|
||||
{firstLetter(assistant?.name).toUpperCase()}
|
||||
</Avatar>
|
||||
) : (
|
||||
<Avatar src={avatar} size={35} />
|
||||
return (
|
||||
<MessageContainer key={message.id} className="message" style={{ border: messageBorder }}>
|
||||
<MessageHeader>
|
||||
<AvatarWrapper>
|
||||
{isAssistantMessage ? (
|
||||
<Avatar src={avatarSource} size={35}>
|
||||
{avatarName}
|
||||
</Avatar>
|
||||
) : (
|
||||
<Avatar src={avatar} size={35} />
|
||||
)}
|
||||
<UserWrap>
|
||||
<UserName>{username}</UserName>
|
||||
<MessageTime>{dayjs(message.createdAt).format('MM/DD HH:mm')}</MessageTime>
|
||||
</UserWrap>
|
||||
</AvatarWrapper>
|
||||
</MessageHeader>
|
||||
<MessageContent style={{ fontFamily }}>
|
||||
{message.status === 'sending' && (
|
||||
<MessageContentLoading>
|
||||
<SyncOutlined spin size={24} />
|
||||
</MessageContentLoading>
|
||||
)}
|
||||
{message.status !== 'sending' && <Markdown message={message} />}
|
||||
{message.usage && !generating && (
|
||||
<MessageMetadata>
|
||||
Tokens: {message.usage.total_tokens} | ↑{message.usage.prompt_tokens}↓{message.usage.completion_tokens}
|
||||
</MessageMetadata>
|
||||
)}
|
||||
{showMenu && (
|
||||
<MenusBar className={`menubar ${isLastMessage && 'show'} ${(!isLastMessage || isUserMessage) && 'user'}`}>
|
||||
{message.role === 'user' && (
|
||||
<Tooltip title="Edit" mouseEnterDelay={0.8}>
|
||||
<ActionButton onClick={onEdit}>
|
||||
<EditOutlined />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
<UserWrap>
|
||||
<UserName>{removeLeadingEmoji(getUserName())}</UserName>
|
||||
<MessageTime>{dayjs(message.createdAt).format('MM/DD HH:mm')}</MessageTime>
|
||||
</UserWrap>
|
||||
</AvatarWrapper>
|
||||
</MessageHeader>
|
||||
<MessageContent style={{ fontFamily }}>
|
||||
{message.status === 'sending' && (
|
||||
<MessageContentLoading>
|
||||
<SyncOutlined spin size={24} />
|
||||
</MessageContentLoading>
|
||||
)}
|
||||
{message.status !== 'sending' && <Markdown message={message} />}
|
||||
{message.usage && !generating && (
|
||||
<MessageMetadata>
|
||||
Tokens: {message.usage.total_tokens} | ↑{message.usage.prompt_tokens}↓{message.usage.completion_tokens}
|
||||
</MessageMetadata>
|
||||
)}
|
||||
{showMenu && (
|
||||
<MenusBar className={`menubar ${isLastMessage && 'show'} ${(!isLastMessage || isUserMessage) && 'user'}`}>
|
||||
{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 && <CopyOutlined />}
|
||||
{copied && <CheckOutlined style={{ color: 'var(--color-primary)' }} />}
|
||||
<Tooltip title={t('common.copy')} mouseEnterDelay={0.8}>
|
||||
<ActionButton onClick={onCopy}>
|
||||
{!copied && <CopyOutlined />}
|
||||
{copied && <CheckOutlined style={{ color: 'var(--color-primary)' }} />}
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={t('common.delete')} mouseEnterDelay={0.8}>
|
||||
<ActionButton onClick={onDelete}>
|
||||
<DeleteOutlined />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
{canRegenerate && (
|
||||
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
|
||||
<ActionButton onClick={onRegenerate}>
|
||||
<SyncOutlined />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={t('common.delete')} mouseEnterDelay={0.8}>
|
||||
<ActionButton onClick={onDelete}>
|
||||
<DeleteOutlined />
|
||||
)}
|
||||
{!isUserMessage && (
|
||||
<Dropdown menu={{ items: dropdownItems }} trigger={['click']} placement="topRight" arrow>
|
||||
<ActionButton>
|
||||
<MenuOutlined />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
{canRegenerate && (
|
||||
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
|
||||
<ActionButton onClick={onRegenerate}>
|
||||
<SyncOutlined />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{!isUserMessage && (
|
||||
<Dropdown menu={{ items: getDropdownMenus(message) }} trigger={['click']} placement="topRight" arrow>
|
||||
<ActionButton>
|
||||
<MenuOutlined />
|
||||
</ActionButton>
|
||||
</Dropdown>
|
||||
)}
|
||||
</MenusBar>
|
||||
)}
|
||||
</MessageContent>
|
||||
</MessageContainer>
|
||||
),
|
||||
[
|
||||
assistant?.name,
|
||||
avatar,
|
||||
canRegenerate,
|
||||
copied,
|
||||
fontFamily,
|
||||
generating,
|
||||
getDropdownMenus,
|
||||
getUserName,
|
||||
isLastMessage,
|
||||
isUserMessage,
|
||||
message,
|
||||
messageBorder,
|
||||
onCopy,
|
||||
onDelete,
|
||||
onEdit,
|
||||
onRegenerate,
|
||||
showMenu,
|
||||
t
|
||||
]
|
||||
</Dropdown>
|
||||
)}
|
||||
</MenusBar>
|
||||
)}
|
||||
</MessageContent>
|
||||
</MessageContainer>
|
||||
)
|
||||
}
|
||||
|
||||
@ -301,4 +274,4 @@ const ActionButton = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
export default MessageItem
|
||||
export default memo(MessageItem)
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
|
||||
import { Assistant, Message, Topic } from '@renderer/types'
|
||||
import localforage from 'localforage'
|
||||
import { FC, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import MessageItem from './Message'
|
||||
import { debounce, reverse } from 'lodash'
|
||||
@ -21,28 +21,28 @@ interface Props {
|
||||
const Messages: FC<Props> = ({ assistant, topic }) => {
|
||||
const [messages, setMessages] = useState<Message[]>([])
|
||||
const [lastMessage, setLastMessage] = useState<Message | null>(null)
|
||||
const { updateTopic } = useAssistant(assistant.id)
|
||||
const provider = useProviderByAssistant(assistant)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const { updateTopic } = useAssistant(assistant.id)
|
||||
|
||||
const assistantDefaultMessage: Message = {
|
||||
id: 'assistant',
|
||||
role: 'assistant',
|
||||
content: assistant.description || assistant.prompt || t('assistant.default.description'),
|
||||
assistantId: assistant.id,
|
||||
topicId: topic.id,
|
||||
status: 'pending',
|
||||
createdAt: new Date().toISOString()
|
||||
}
|
||||
const assistantDefaultMessage: Message = useMemo(
|
||||
() => ({
|
||||
id: 'assistant',
|
||||
role: 'assistant',
|
||||
content: assistant.description || assistant.prompt || t('assistant.default.description'),
|
||||
assistantId: assistant.id,
|
||||
topicId: topic.id,
|
||||
status: 'pending',
|
||||
createdAt: new Date().toISOString()
|
||||
}),
|
||||
[assistant.description, assistant.id, assistant.prompt, topic.id]
|
||||
)
|
||||
|
||||
const onSendMessage = useCallback(
|
||||
(message: Message) => {
|
||||
const _messages = [...messages, message]
|
||||
setMessages(_messages)
|
||||
localforage.setItem(`topic:${topic.id}`, {
|
||||
...topic,
|
||||
messages: _messages
|
||||
})
|
||||
localforage.setItem(`topic:${topic.id}`, { ...topic, messages: _messages })
|
||||
},
|
||||
[messages, topic]
|
||||
)
|
||||
@ -54,14 +54,14 @@ const Messages: FC<Props> = ({ assistant, topic }) => {
|
||||
}
|
||||
}, [assistant, messages, topic, updateTopic])
|
||||
|
||||
const onDeleteMessage = (message: Message) => {
|
||||
const _messages = messages.filter((m) => m.id !== message.id)
|
||||
setMessages(_messages)
|
||||
localforage.setItem(`topic:${topic.id}`, {
|
||||
id: topic.id,
|
||||
messages: _messages
|
||||
})
|
||||
}
|
||||
const onDeleteMessage = useCallback(
|
||||
(message: Message) => {
|
||||
const _messages = messages.filter((m) => m.id !== message.id)
|
||||
setMessages(_messages)
|
||||
localforage.setItem(`topic:${topic.id}`, { id: topic.id, messages: _messages })
|
||||
},
|
||||
[messages, topic.id]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribes = [
|
||||
@ -85,13 +85,10 @@ const Messages: FC<Props> = ({ assistant, topic }) => {
|
||||
})
|
||||
]
|
||||
return () => unsubscribes.forEach((unsub) => unsub())
|
||||
}, [assistant, autoRenameTopic, messages, onSendMessage, provider, topic, updateTopic])
|
||||
}, [assistant, messages, provider, topic, autoRenameTopic, updateTopic, onSendMessage])
|
||||
|
||||
useEffect(() => {
|
||||
runAsyncFunction(async () => {
|
||||
const messages = await LocalStorage.getTopicMessages(topic.id)
|
||||
setMessages(messages || [])
|
||||
})
|
||||
runAsyncFunction(async () => setMessages((await LocalStorage.getTopicMessages(topic.id)) || []))
|
||||
}, [topic.id])
|
||||
|
||||
const scrollTop = useCallback(
|
||||
@ -103,9 +100,8 @@ const Messages: FC<Props> = ({ assistant, topic }) => {
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(scrollTop, 100)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [messages, lastMessage])
|
||||
scrollTop()
|
||||
}, [messages, lastMessage, scrollTop])
|
||||
|
||||
useEffect(() => {
|
||||
EventEmitter.emit(EVENT_NAMES.ESTIMATED_TOKEN_COUNT, estimateHistoryTokenCount(assistant, messages))
|
||||
|
||||
@ -4,7 +4,7 @@ import { useShowRightSidebar } from '@renderer/hooks/useStore'
|
||||
import { fetchMessagesSummary } from '@renderer/services/api'
|
||||
import { Assistant, Topic } from '@renderer/types'
|
||||
import { Dropdown, MenuProps } from 'antd'
|
||||
import { FC } from 'react'
|
||||
import { FC, useCallback } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { DeleteOutlined, EditOutlined, OpenAIOutlined } from '@ant-design/icons'
|
||||
import LocalStorage from '@renderer/services/storage'
|
||||
@ -25,72 +25,81 @@ const TopicsTab: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTop
|
||||
const { t } = useTranslation()
|
||||
const generating = useAppSelector((state) => state.runtime.generating)
|
||||
|
||||
const getTopicMenuItems = (topic: Topic) => {
|
||||
const menus: MenuProps['items'] = [
|
||||
{
|
||||
label: t('assistant.topics.auto_rename'),
|
||||
key: 'auto-rename',
|
||||
icon: <OpenAIOutlined />,
|
||||
async onClick() {
|
||||
const messages = await LocalStorage.getTopicMessages(topic.id)
|
||||
if (messages.length >= 2) {
|
||||
const summaryText = await fetchMessagesSummary({ messages, assistant })
|
||||
if (summaryText) {
|
||||
updateTopic({ ...topic, name: summaryText })
|
||||
const getTopicMenuItems = useCallback(
|
||||
(topic: Topic) => {
|
||||
const menus: MenuProps['items'] = [
|
||||
{
|
||||
label: t('assistant.topics.auto_rename'),
|
||||
key: 'auto-rename',
|
||||
icon: <OpenAIOutlined />,
|
||||
async onClick() {
|
||||
const messages = await LocalStorage.getTopicMessages(topic.id)
|
||||
if (messages.length >= 2) {
|
||||
const summaryText = await fetchMessagesSummary({ messages, assistant })
|
||||
if (summaryText) {
|
||||
updateTopic({ ...topic, name: summaryText })
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t('assistant.topics.edit.title'),
|
||||
key: 'rename',
|
||||
icon: <EditOutlined />,
|
||||
async onClick() {
|
||||
const name = await PromptPopup.show({
|
||||
title: t('assistant.topics.edit.title'),
|
||||
message: '',
|
||||
defaultValue: topic?.name || ''
|
||||
})
|
||||
if (name && topic?.name !== name) {
|
||||
updateTopic({ ...topic, name })
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t('assistant.topics.edit.title'),
|
||||
key: 'rename',
|
||||
icon: <EditOutlined />,
|
||||
async onClick() {
|
||||
const name = await PromptPopup.show({
|
||||
title: t('assistant.topics.edit.title'),
|
||||
message: '',
|
||||
defaultValue: topic?.name || ''
|
||||
})
|
||||
if (name && topic?.name !== name) {
|
||||
updateTopic({ ...topic, name })
|
||||
]
|
||||
|
||||
if (assistant.topics.length > 1) {
|
||||
menus.push({ type: 'divider' })
|
||||
menus.push({
|
||||
label: t('common.delete'),
|
||||
danger: true,
|
||||
key: 'delete',
|
||||
icon: <DeleteOutlined />,
|
||||
onClick() {
|
||||
if (assistant.topics.length === 1) return
|
||||
removeTopic(topic)
|
||||
setActiveTopic(assistant.topics[0])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
]
|
||||
|
||||
if (assistant.topics.length > 1) {
|
||||
menus.push({ type: 'divider' })
|
||||
menus.push({
|
||||
label: t('common.delete'),
|
||||
danger: true,
|
||||
key: 'delete',
|
||||
icon: <DeleteOutlined />,
|
||||
onClick() {
|
||||
if (assistant.topics.length === 1) return
|
||||
removeTopic(topic)
|
||||
setActiveTopic(assistant.topics[0])
|
||||
}
|
||||
})
|
||||
}
|
||||
return menus
|
||||
},
|
||||
[assistant, removeTopic, setActiveTopic, t, updateTopic]
|
||||
)
|
||||
|
||||
return menus
|
||||
}
|
||||
const onDragEnd = useCallback(
|
||||
(result: DropResult) => {
|
||||
if (result.destination) {
|
||||
const sourceIndex = result.source.index
|
||||
const destIndex = result.destination.index
|
||||
updateTopics(droppableReorder(assistant.topics, sourceIndex, destIndex))
|
||||
}
|
||||
},
|
||||
[assistant.topics, updateTopics]
|
||||
)
|
||||
|
||||
const onDragEnd = (result: DropResult) => {
|
||||
if (result.destination) {
|
||||
const sourceIndex = result.source.index
|
||||
const destIndex = result.destination.index
|
||||
updateTopics(droppableReorder(assistant.topics, sourceIndex, destIndex))
|
||||
}
|
||||
}
|
||||
|
||||
const onSwitchTopic = (topic: Topic) => {
|
||||
if (generating) {
|
||||
window.message.warning({ content: t('message.switch.disabled'), key: 'switch-assistant' })
|
||||
return
|
||||
}
|
||||
setActiveTopic(topic)
|
||||
}
|
||||
const onSwitchTopic = useCallback(
|
||||
(topic: Topic) => {
|
||||
if (generating) {
|
||||
window.message.warning({ content: t('message.switch.disabled'), key: 'switch-assistant' })
|
||||
return
|
||||
}
|
||||
setActiveTopic(topic)
|
||||
},
|
||||
[generating, setActiveTopic, t]
|
||||
)
|
||||
|
||||
return (
|
||||
<Container style={{ display: rightSidebarShown ? 'block' : 'none' }}>
|
||||
|
||||
@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
|
||||
import { atomDark, oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism'
|
||||
import styled from 'styled-components'
|
||||
import Mermaid from '../Mermaid'
|
||||
import Mermaid from './Mermaid'
|
||||
|
||||
interface CodeBlockProps {
|
||||
children: string
|
||||
|
||||
Loading…
Reference in New Issue
Block a user