mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-26 20:12:38 +08:00
fix: extract the ChatContext
This commit is contained in:
parent
d7aedbf14e
commit
bd9fe847ab
@ -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;
|
||||
|
||||
156
src/renderer/src/pages/home/Messages/ChatContext.tsx
Normal file
156
src/renderer/src/pages/home/Messages/ChatContext.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@ -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[]) => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user