diff --git a/src/renderer/src/pages/home/components/Inputbar.tsx b/src/renderer/src/pages/home/components/Inputbar.tsx index a64ba5281c..ba13633a8d 100644 --- a/src/renderer/src/pages/home/components/Inputbar.tsx +++ b/src/renderer/src/pages/home/components/Inputbar.tsx @@ -40,7 +40,7 @@ const Inputbar: FC = ({ assistant, setActiveTopic }) => { const inputRef = useRef(null) const { t } = useTranslation() - const sendMessage = () => { + const sendMessage = useCallback(() => { if (generating) { return } @@ -64,19 +64,17 @@ const Inputbar: FC = ({ assistant, setActiveTopic }) => { setText('') setExpend(false) - } + }, [assistant.id, assistant.topics, generating, text]) const inputTokenCount = useMemo(() => estimateInputTokenCount(text), [text]) const handleKeyDown = (event: React.KeyboardEvent) => { if (expended) { if (event.key === 'Escape') { - setExpend(false) - return + return setExpend(false) } if (event.key === 'Enter' && event.shiftKey) { - sendMessage() - return + return sendMessage() } return } diff --git a/src/renderer/src/pages/home/components/Message.tsx b/src/renderer/src/pages/home/components/Message.tsx index 37de10a003..90a0d43290 100644 --- a/src/renderer/src/pages/home/components/Message.tsx +++ b/src/renderer/src/pages/home/components/Message.tsx @@ -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 = ({ 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 = ({ 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: , - 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: , + 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( - () => ( - - - - {message.role === 'assistant' ? ( - - {firstLetter(assistant?.name).toUpperCase()} - - ) : ( - + return ( + + + + {isAssistantMessage ? ( + + {avatarName} + + ) : ( + + )} + + {username} + {dayjs(message.createdAt).format('MM/DD HH:mm')} + + + + + {message.status === 'sending' && ( + + + + )} + {message.status !== 'sending' && } + {message.usage && !generating && ( + + Tokens: {message.usage.total_tokens} | ↑{message.usage.prompt_tokens}↓{message.usage.completion_tokens} + + )} + {showMenu && ( + + {message.role === 'user' && ( + + + + + )} - - {removeLeadingEmoji(getUserName())} - {dayjs(message.createdAt).format('MM/DD HH:mm')} - - - - - {message.status === 'sending' && ( - - - - )} - {message.status !== 'sending' && } - {message.usage && !generating && ( - - Tokens: {message.usage.total_tokens} | ↑{message.usage.prompt_tokens}↓{message.usage.completion_tokens} - - )} - {showMenu && ( - - {message.role === 'user' && ( - - - - - - )} - - - {!copied && } - {copied && } + + + {!copied && } + {copied && } + + + + + + + + {canRegenerate && ( + + + - - - + )} + {!isUserMessage && ( + + + - - {canRegenerate && ( - - - - - - )} - {!isUserMessage && ( - - - - - - )} - - )} - - - ), - [ - assistant?.name, - avatar, - canRegenerate, - copied, - fontFamily, - generating, - getDropdownMenus, - getUserName, - isLastMessage, - isUserMessage, - message, - messageBorder, - onCopy, - onDelete, - onEdit, - onRegenerate, - showMenu, - t - ] + + )} + + )} + + ) } @@ -301,4 +274,4 @@ const ActionButton = styled.div` } ` -export default MessageItem +export default memo(MessageItem) diff --git a/src/renderer/src/pages/home/components/Messages.tsx b/src/renderer/src/pages/home/components/Messages.tsx index cac1954c80..e588fa2ab6 100644 --- a/src/renderer/src/pages/home/components/Messages.tsx +++ b/src/renderer/src/pages/home/components/Messages.tsx @@ -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 = ({ assistant, topic }) => { const [messages, setMessages] = useState([]) const [lastMessage, setLastMessage] = useState(null) - const { updateTopic } = useAssistant(assistant.id) const provider = useProviderByAssistant(assistant) const containerRef = useRef(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 = ({ 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 = ({ 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 = ({ 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)) diff --git a/src/renderer/src/pages/home/components/TopicsTab.tsx b/src/renderer/src/pages/home/components/TopicsTab.tsx index d5085896f1..26ec808ddd 100644 --- a/src/renderer/src/pages/home/components/TopicsTab.tsx +++ b/src/renderer/src/pages/home/components/TopicsTab.tsx @@ -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 = ({ 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: , - 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: , + 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: , + 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: , - 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: , + 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: , - 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 ( diff --git a/src/renderer/src/pages/home/components/markdown/CodeBlock.tsx b/src/renderer/src/pages/home/components/markdown/CodeBlock.tsx index 634eab073f..7b102635d8 100644 --- a/src/renderer/src/pages/home/components/markdown/CodeBlock.tsx +++ b/src/renderer/src/pages/home/components/markdown/CodeBlock.tsx @@ -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 diff --git a/src/renderer/src/pages/home/components/Mermaid.tsx b/src/renderer/src/pages/home/components/markdown/Mermaid.tsx similarity index 100% rename from src/renderer/src/pages/home/components/Mermaid.tsx rename to src/renderer/src/pages/home/components/markdown/Mermaid.tsx