fix: extract the ChatContext

This commit is contained in:
Pleasurecruise 2025-05-18 15:21:36 +08:00
parent d7aedbf14e
commit bd9fe847ab
No known key found for this signature in database
GPG Key ID: E6385136096279B6
4 changed files with 180 additions and 145 deletions

View File

@ -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> = (props) => {
const ChatContent: FC<Props> = (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<string[]>([])
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<HTMLDivElement>(null)
const contentSearchRef = React.useRef<ContentSearchRef>(null)
@ -119,106 +105,6 @@ const Chat: FC<Props> = (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 (
<Container id="chat" className={messageStyle}>
<Main ref={mainRef} id="chat-main" vertical flex={1} justify="space-between" style={{ maxWidth }}>
@ -244,7 +130,7 @@ const Chat: FC<Props> = (props) => {
{isMultiSelectMode && (
<MultiSelectActionPopup
visible={isMultiSelectMode}
onClose={() => setIsMultiSelectMode(false)}
onClose={() => toggleMultiSelectMode(false)}
onAction={handleMultiSelectAction}
topic={props.activeTopic}
/>
@ -260,21 +146,18 @@ const Chat: FC<Props> = (props) => {
position="right"
/>
)}
<Modal
title={t('message.delete.confirm.title')}
open={confirmDeleteVisible}
onOk={confirmDelete}
onCancel={cancelDelete}
okText={t('common.confirm')}
cancelText={t('common.cancel')}
okButtonProps={{ danger: true }}
centered={true}>
<p>{t('message.delete.confirm.content', { count: messagesToDelete.length })}</p>
</Modal>
</Container>
)
}
const Chat: FC<Props> = (props) => {
return (
<ChatProvider activeTopic={props.activeTopic}>
<ChatContent {...props} />
</ChatProvider>
)
}
const MessagesContainer = styled.div`
display: flex;
flex-direction: column;

View File

@ -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<ChatContextProps | undefined>(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<ChatProviderProps> = ({ children, activeTopic }) => {
const { t } = useTranslation()
const dispatch = useDispatch()
const [isMultiSelectMode, setIsMultiSelectMode] = useState(false)
const [confirmDeleteVisible, setConfirmDeleteVisible] = useState(false)
const [messagesToDelete, setMessagesToDelete] = useState<string[]>([])
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 (
<ChatContext value={value}>
{children}
<Modal
title={t('message.delete.confirm.title')}
open={confirmDeleteVisible}
onOk={confirmDelete}
onCancel={cancelDelete}
okText={t('common.confirm')}
cancelText={t('common.cancel')}
okButtonProps={{ danger: true }}
centered={true}>
<p>{t('message.delete.confirm.content', { count: messagesToDelete.length })}</p>
</Modal>
</ChatContext>
)
}

View File

@ -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> = (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> = (props) => {
key: 'multi-select',
icon: <MenuOutlined size={16} />,
onClick: () => {
EventEmitter.emit(EVENT_NAMES.MESSAGE_MULTI_SELECT, true)
toggleMultiSelectMode(true)
}
},
{

View File

@ -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<MessagesProps> = ({ 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<Set<string>>(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<MessagesProps> = ({ 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[]) => {