From bd9fe847ab5b956a2999f512f73d2bf17c8fe07d Mon Sep 17 00:00:00 2001 From: Pleasurecruise <3196812536@qq.com> Date: Sun, 18 May 2025 15:21:36 +0800 Subject: [PATCH] fix: extract the ChatContext --- src/renderer/src/pages/home/Chat.tsx | 145 ++-------------- .../src/pages/home/Messages/ChatContext.tsx | 156 ++++++++++++++++++ .../pages/home/Messages/MessageMenubar.tsx | 4 +- .../src/pages/home/Messages/Messages.tsx | 20 +-- 4 files changed, 180 insertions(+), 145 deletions(-) create mode 100644 src/renderer/src/pages/home/Messages/ChatContext.tsx diff --git a/src/renderer/src/pages/home/Chat.tsx b/src/renderer/src/pages/home/Chat.tsx index e7b0caf6a5..1a611ef49e 100644 --- a/src/renderer/src/pages/home/Chat.tsx +++ b/src/renderer/src/pages/home/Chat.tsx @@ -5,20 +5,15 @@ import { useAssistant } from '@renderer/hooks/useAssistant' import { useSettings } from '@renderer/hooks/useSettings' import { useShortcut } from '@renderer/hooks/useShortcuts' import { useShowTopics } from '@renderer/hooks/useStore' -import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' -import { RootState } from '@renderer/store' -import { messageBlocksSelectors } from '@renderer/store/messageBlock' -import { newMessagesActions, selectMessagesForTopic } from '@renderer/store/newMessage' import { Assistant, Topic } from '@renderer/types' -import { Flex, Modal } from 'antd' +import { Flex } from 'antd' import { debounce } from 'lodash' -import React, { FC, useEffect, useMemo, useState } from 'react' +import React, { FC, useMemo, useState } from 'react' import { useHotkeys } from 'react-hotkeys-hook' -import { useTranslation } from 'react-i18next' -import { useDispatch, useSelector } from 'react-redux' import styled from 'styled-components' import Inputbar from './Inputbar/Inputbar' +import { ChatProvider, useChatContext } from './Messages/ChatContext' import Messages from './Messages/Messages' import Tabs from './Tabs' @@ -29,20 +24,11 @@ interface Props { setActiveAssistant: (assistant: Assistant) => void } -const Chat: FC = (props) => { +const ChatContent: FC = (props) => { const { assistant } = useAssistant(props.assistant.id) const { topicPosition, messageStyle, showAssistants } = useSettings() const { showTopics } = useShowTopics() - const { t } = useTranslation() - const [isMultiSelectMode, setIsMultiSelectMode] = useState(false) - const [confirmDeleteVisible, setConfirmDeleteVisible] = useState(false) - const [messagesToDelete, setMessagesToDelete] = useState([]) - - const dispatch = useDispatch() - // 从 Redux 中获取当前主题的消息 - const messages = useSelector((state: RootState) => selectMessagesForTopic(state, props.activeTopic.id)) - // 获取所有消息块 - const messageBlocks = useSelector(messageBlocksSelectors.selectEntities) + const { isMultiSelectMode, toggleMultiSelectMode, handleMultiSelectAction } = useChatContext() const mainRef = React.useRef(null) const contentSearchRef = React.useRef(null) @@ -119,106 +105,6 @@ const Chat: FC = (props) => { firstUpdateOrNoFirstUpdateHandler() } - useEffect(() => { - const handleToggleMultiSelect = (value: boolean) => { - setIsMultiSelectMode(value) - } - - EventEmitter.on(EVENT_NAMES.MESSAGE_MULTI_SELECT, handleToggleMultiSelect) - - return () => { - EventEmitter.off(EVENT_NAMES.MESSAGE_MULTI_SELECT, handleToggleMultiSelect) - } - }, []) - - const handleMultiSelectAction = (actionType: string, messageIds: string[]) => { - if (messageIds.length === 0) { - window.message.warning(t('chat.multiple.select.empty')) - return - } - switch (actionType) { - case 'delete': - setMessagesToDelete(messageIds) - setConfirmDeleteVisible(true) - break - case 'save': { - const assistantMessages = messages.filter((msg) => messageIds.includes(msg.id)) - if (assistantMessages.length > 0) { - const contentToSave = assistantMessages - .map((msg) => { - return msg.blocks - .map((blockId) => { - const block = messageBlocks[blockId] - return block && 'content' in block ? block.content : '' - }) - .filter(Boolean) - .join('\n') - .trim() - }) - .join('\n\n---\n\n') - const fileName = `chat_export_${new Date().toISOString().slice(0, 19).replace(/[T:]/g, '-')}.md` - window.api.file.save(fileName, contentToSave) - window.message.success({ content: t('message.save.success.title'), key: 'save-messages' }) - EventEmitter.emit(EVENT_NAMES.MESSAGE_MULTI_SELECT, false) - } else { - window.message.warning(t('message.save.no.assistant')) - } - break - } - case 'copy': { - const assistantMessages = messages.filter((msg) => messageIds.includes(msg.id)) - if (assistantMessages.length > 0) { - const contentToCopy = assistantMessages - .map((msg) => { - return msg.blocks - .map((blockId) => { - const block = messageBlocks[blockId] - return block && 'content' in block ? block.content : '' - }) - .filter(Boolean) - .join('\n') - .trim() - }) - .join('\n\n---\n\n') - navigator.clipboard.writeText(contentToCopy) - window.message.success({ content: t('message.copied'), key: 'copy-messages' }) - EventEmitter.emit(EVENT_NAMES.MESSAGE_MULTI_SELECT, false) - } else { - window.message.warning(t('message.copy.no.assistant')) - } - break - } - default: - break - } - } - - const confirmDelete = async () => { - try { - dispatch( - newMessagesActions.removeMessages({ - topicId: props.activeTopic.id, - messageIds: messagesToDelete - }) - ) - window.message.success(t('message.delete.success')) - setMessagesToDelete([]) - setIsMultiSelectMode(false) - EventEmitter.emit(EVENT_NAMES.MESSAGE_MULTI_SELECT, false) - } catch (error) { - console.error('Failed to delete messages:', error) - window.message.error(t('message.delete.failed')) - } finally { - setConfirmDeleteVisible(false) - setIsMultiSelectMode(false) - } - } - - const cancelDelete = () => { - setConfirmDeleteVisible(false) - setMessagesToDelete([]) - } - return (
@@ -244,7 +130,7 @@ const Chat: FC = (props) => { {isMultiSelectMode && ( setIsMultiSelectMode(false)} + onClose={() => toggleMultiSelectMode(false)} onAction={handleMultiSelectAction} topic={props.activeTopic} /> @@ -260,21 +146,18 @@ const Chat: FC = (props) => { position="right" /> )} - -

{t('message.delete.confirm.content', { count: messagesToDelete.length })}

-
) } +const Chat: FC = (props) => { + return ( + + + + ) +} + const MessagesContainer = styled.div` display: flex; flex-direction: column; diff --git a/src/renderer/src/pages/home/Messages/ChatContext.tsx b/src/renderer/src/pages/home/Messages/ChatContext.tsx new file mode 100644 index 0000000000..09edd7100a --- /dev/null +++ b/src/renderer/src/pages/home/Messages/ChatContext.tsx @@ -0,0 +1,156 @@ +import { RootState } from '@renderer/store' +import { messageBlocksSelectors } from '@renderer/store/messageBlock' +import { newMessagesActions, selectMessagesForTopic } from '@renderer/store/newMessage' +import { Topic } from '@renderer/types' +import { Modal } from 'antd' +import { createContext, FC, ReactNode, use, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useDispatch, useSelector } from 'react-redux' + +interface ChatContextProps { + isMultiSelectMode: boolean + toggleMultiSelectMode: (value: boolean) => void + handleMultiSelectAction: (actionType: string, messageIds: string[]) => void + activeTopic: Topic +} + +const ChatContext = createContext(undefined) + +export const useChatContext = () => { + const context = use(ChatContext) + if (!context) { + throw new Error('useChatContext 必须在 ChatProvider 内使用') + } + return context +} + +interface ChatProviderProps { + children: ReactNode + activeTopic: Topic +} + +export const ChatProvider: FC = ({ children, activeTopic }) => { + const { t } = useTranslation() + const dispatch = useDispatch() + const [isMultiSelectMode, setIsMultiSelectMode] = useState(false) + const [confirmDeleteVisible, setConfirmDeleteVisible] = useState(false) + const [messagesToDelete, setMessagesToDelete] = useState([]) + + const messages = useSelector((state: RootState) => selectMessagesForTopic(state, activeTopic.id)) + const messageBlocks = useSelector(messageBlocksSelectors.selectEntities) + + const toggleMultiSelectMode = (value: boolean) => { + setIsMultiSelectMode(value) + } + + const handleMultiSelectAction = (actionType: string, messageIds: string[]) => { + if (messageIds.length === 0) { + window.message.warning(t('chat.multiple.select.empty')) + return + } + + switch (actionType) { + case 'delete': + setMessagesToDelete(messageIds) + setConfirmDeleteVisible(true) + break + case 'save': { + const assistantMessages = messages.filter((msg) => messageIds.includes(msg.id)) + if (assistantMessages.length > 0) { + const contentToSave = assistantMessages + .map((msg) => { + return msg.blocks + .map((blockId) => { + const block = messageBlocks[blockId] + return block && 'content' in block ? block.content : '' + }) + .filter(Boolean) + .join('\n') + .trim() + }) + .join('\n\n---\n\n') + const fileName = `chat_export_${new Date().toISOString().slice(0, 19).replace(/[T:]/g, '-')}.md` + window.api.file.save(fileName, contentToSave) + window.message.success({ content: t('message.save.success.title'), key: 'save-messages' }) + toggleMultiSelectMode(false) + } else { + window.message.warning(t('message.save.no.assistant')) + } + break + } + case 'copy': { + const assistantMessages = messages.filter((msg) => messageIds.includes(msg.id)) + if (assistantMessages.length > 0) { + const contentToCopy = assistantMessages + .map((msg) => { + return msg.blocks + .map((blockId) => { + const block = messageBlocks[blockId] + return block && 'content' in block ? block.content : '' + }) + .filter(Boolean) + .join('\n') + .trim() + }) + .join('\n\n---\n\n') + navigator.clipboard.writeText(contentToCopy) + window.message.success({ content: t('message.copied'), key: 'copy-messages' }) + toggleMultiSelectMode(false) + } else { + window.message.warning(t('message.copy.no.assistant')) + } + break + } + default: + break + } + } + + const confirmDelete = async () => { + try { + dispatch( + newMessagesActions.removeMessages({ + topicId: activeTopic.id, + messageIds: messagesToDelete + }) + ) + window.message.success(t('message.delete.success')) + setMessagesToDelete([]) + toggleMultiSelectMode(false) + } catch (error) { + console.error('Failed to delete messages:', error) + window.message.error(t('message.delete.failed')) + } finally { + setConfirmDeleteVisible(false) + } + } + + const cancelDelete = () => { + setConfirmDeleteVisible(false) + setMessagesToDelete([]) + } + + const value = { + isMultiSelectMode, + toggleMultiSelectMode, + handleMultiSelectAction, + activeTopic + } + + return ( + + {children} + +

{t('message.delete.confirm.content', { count: messagesToDelete.length })}

+
+
+ ) +} diff --git a/src/renderer/src/pages/home/Messages/MessageMenubar.tsx b/src/renderer/src/pages/home/Messages/MessageMenubar.tsx index 1cd2a4c76e..d0ba0e2cec 100644 --- a/src/renderer/src/pages/home/Messages/MessageMenubar.tsx +++ b/src/renderer/src/pages/home/Messages/MessageMenubar.tsx @@ -37,6 +37,7 @@ import { FC, memo, useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' import styled from 'styled-components' +import { useChatContext } from './ChatContext' interface Props { message: Message @@ -55,6 +56,7 @@ const MessageMenubar: FC = (props) => { const { message, index, isGrouped, isLastMessage, isAssistantMessage, assistant, topic, model, messageContainerRef } = props const { t } = useTranslation() + const { toggleMultiSelectMode } = useChatContext() const [copied, setCopied] = useState(false) const [isTranslating, setIsTranslating] = useState(false) const [showRegenerateTooltip, setShowRegenerateTooltip] = useState(false) @@ -264,7 +266,7 @@ const MessageMenubar: FC = (props) => { key: 'multi-select', icon: , onClick: () => { - EventEmitter.emit(EVENT_NAMES.MESSAGE_MULTI_SELECT, true) + toggleMultiSelectMode(true) } }, { diff --git a/src/renderer/src/pages/home/Messages/Messages.tsx b/src/renderer/src/pages/home/Messages/Messages.tsx index 9c75ced355..28779fa19c 100644 --- a/src/renderer/src/pages/home/Messages/Messages.tsx +++ b/src/renderer/src/pages/home/Messages/Messages.tsx @@ -31,6 +31,7 @@ import { useTranslation } from 'react-i18next' import InfiniteScroll from 'react-infinite-scroll-component' import styled from 'styled-components' +import { useChatContext } from './ChatContext' import ChatNavigation from './ChatNavigation' import MessageAnchorLine from './MessageAnchorLine' import MessageGroup from './MessageGroup' @@ -55,8 +56,10 @@ const Messages: FC = ({ assistant, topic, setActiveTopic, onCompo const [hasMore, setHasMore] = useState(false) const [isLoadingMore, setIsLoadingMore] = useState(false) const [isProcessingContext, setIsProcessingContext] = useState(false) + + const { isMultiSelectMode } = useChatContext() + const [selectedMessages, setSelectedMessages] = useState>(new Set()) - const [isMultiSelectMode, setIsMultiSelectMode] = useState(false) const [isDragging, setIsDragging] = useState(false) const [dragStart, setDragStart] = useState({ x: 0, y: 0 }) const [dragCurrent, setDragCurrent] = useState({ x: 0, y: 0 }) @@ -155,19 +158,10 @@ const Messages: FC = ({ assistant, topic, setActiveTopic, onCompo }, []) useEffect(() => { - const handleToggleMultiSelect = (value: boolean) => { - setIsMultiSelectMode(value) - if (!value) { - setSelectedMessages(new Set()) - } + if (!isMultiSelectMode) { + setSelectedMessages(new Set()) } - - EventEmitter.on(EVENT_NAMES.MESSAGE_MULTI_SELECT, handleToggleMultiSelect) - - return () => { - EventEmitter.off(EVENT_NAMES.MESSAGE_MULTI_SELECT, handleToggleMultiSelect) - } - }, []) + }, [isMultiSelectMode]) useEffect(() => { const handleRequestSelectedMessageDetails = (messageIds: string[]) => {