perf(messages): usememo & usecallback message component

This commit is contained in:
kangfenmao 2024-08-01 23:53:58 +08:00
parent cda105a568
commit 1c17e104b1
6 changed files with 188 additions and 212 deletions

View File

@ -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
}

View File

@ -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)

View File

@ -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))

View File

@ -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' }}>

View File

@ -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