mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-25 11:20:07 +08:00
refactor: migrate chat context to a custom hook and enhance multi-selection functionality
- Replaced the ChatContext with a custom hook `useChatContext` for better modularity and reusability. - Updated components to utilize the new hook, passing the active topic as an argument. - Enhanced multi-selection logic and state management for messages, improving user experience in the chat interface. - Removed the old ChatContext file to streamline the codebase.
This commit is contained in:
parent
46ae4f9b55
commit
b1839b722f
@ -1,13 +1,19 @@
|
||||
import { useChatContext } from '@renderer/pages/home/Messages/ChatContext'
|
||||
import { useChatContext } from '@renderer/hooks/useChatContext'
|
||||
import { Topic } from '@renderer/types'
|
||||
import { Button, Tooltip } from 'antd'
|
||||
import { Copy, Save, Trash, X } from 'lucide-react'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const MultiSelectActionPopup: FC = () => {
|
||||
interface Props {
|
||||
topic: Topic
|
||||
}
|
||||
|
||||
const MultiSelectActionPopup: FC<Props> = ({ topic }) => {
|
||||
const { t } = useTranslation()
|
||||
const { toggleMultiSelectMode, selectedMessageIds, isMultiSelectMode, handleMultiSelectAction } = useChatContext()
|
||||
const { toggleMultiSelectMode, selectedMessageIds, isMultiSelectMode, handleMultiSelectAction } =
|
||||
useChatContext(topic)
|
||||
|
||||
const handleAction = (action: string) => {
|
||||
handleMultiSelectAction(action, selectedMessageIds)
|
||||
|
||||
185
src/renderer/src/hooks/useChatContext.ts
Normal file
185
src/renderer/src/hooks/useChatContext.ts
Normal file
@ -0,0 +1,185 @@
|
||||
import { useMessageOperations } from '@renderer/hooks/useMessageOperations'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { RootState } from '@renderer/store'
|
||||
import { messageBlocksSelectors } from '@renderer/store/messageBlock'
|
||||
import { selectMessagesForTopic } from '@renderer/store/newMessage'
|
||||
import { setActiveTopic, setSelectedMessageIds, toggleMultiSelectMode } from '@renderer/store/runtime'
|
||||
import { Topic } from '@renderer/types'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useDispatch, useSelector, useStore } from 'react-redux'
|
||||
|
||||
export const useChatContext = (activeTopic: Topic) => {
|
||||
const { t } = useTranslation()
|
||||
const dispatch = useDispatch()
|
||||
const store = useStore<RootState>()
|
||||
const { deleteMessage } = useMessageOperations(activeTopic)
|
||||
|
||||
const [messageRefs, setMessageRefs] = useState<Map<string, HTMLElement>>(new Map())
|
||||
|
||||
const isMultiSelectMode = useSelector((state: RootState) => state.runtime.chat.isMultiSelectMode)
|
||||
const selectedMessageIds = useSelector((state: RootState) => state.runtime.chat.selectedMessageIds)
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = EventEmitter.on(EVENT_NAMES.CHANGE_TOPIC, () => {
|
||||
dispatch(toggleMultiSelectMode(false))
|
||||
})
|
||||
return () => unsubscribe()
|
||||
}, [dispatch])
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(setActiveTopic(activeTopic))
|
||||
}, [dispatch, activeTopic])
|
||||
|
||||
const handleToggleMultiSelectMode = useCallback(
|
||||
(value: boolean) => {
|
||||
dispatch(toggleMultiSelectMode(value))
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
const registerMessageElement = useCallback((id: string, element: HTMLElement | null) => {
|
||||
setMessageRefs((prev) => {
|
||||
const newRefs = new Map(prev)
|
||||
if (element) {
|
||||
newRefs.set(id, element)
|
||||
} else {
|
||||
newRefs.delete(id)
|
||||
}
|
||||
return newRefs
|
||||
})
|
||||
}, [])
|
||||
|
||||
const locateMessage = useCallback(
|
||||
(messageId: string) => {
|
||||
const messageElement = messageRefs.get(messageId)
|
||||
if (messageElement) {
|
||||
// 检查消息是否可见
|
||||
const display = window.getComputedStyle(messageElement).display
|
||||
|
||||
if (display === 'none') {
|
||||
// 如果消息隐藏,需要处理显示逻辑
|
||||
// 查找消息并设置为选中状态
|
||||
const state = store.getState()
|
||||
const messages = selectMessagesForTopic(state, activeTopic.id)
|
||||
const message = messages.find((m) => m.id === messageId)
|
||||
if (message) {
|
||||
// 这里需要实现设置消息为选中状态的逻辑
|
||||
// 可能需要调用其他函数或修改状态
|
||||
}
|
||||
}
|
||||
|
||||
// 滚动到消息位置
|
||||
messageElement.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
}
|
||||
},
|
||||
[messageRefs, store, activeTopic.id]
|
||||
)
|
||||
|
||||
const handleSelectMessage = useCallback(
|
||||
(messageId: string, selected: boolean) => {
|
||||
dispatch(
|
||||
setSelectedMessageIds(
|
||||
selected ? [...selectedMessageIds, messageId] : selectedMessageIds.filter((id) => id !== messageId)
|
||||
)
|
||||
)
|
||||
},
|
||||
[dispatch, selectedMessageIds]
|
||||
)
|
||||
|
||||
const handleMultiSelectAction = useCallback(
|
||||
async (actionType: string, messageIds: string[]) => {
|
||||
if (messageIds.length === 0) {
|
||||
window.message.warning(t('chat.multiple.select.empty'))
|
||||
return
|
||||
}
|
||||
|
||||
const state = store.getState()
|
||||
const messages = selectMessagesForTopic(state, activeTopic.id)
|
||||
const messageBlocks = messageBlocksSelectors.selectEntities(state)
|
||||
|
||||
switch (actionType) {
|
||||
case 'delete':
|
||||
window.modal.confirm({
|
||||
title: t('message.delete.confirm.title'),
|
||||
content: t('message.delete.confirm.content', { count: messageIds.length }),
|
||||
okButtonProps: { danger: true },
|
||||
centered: true,
|
||||
onOk: async () => {
|
||||
try {
|
||||
await Promise.all(messageIds.map((messageId) => deleteMessage(messageId)))
|
||||
window.message.success(t('message.delete.success'))
|
||||
handleToggleMultiSelectMode(false)
|
||||
} catch (error) {
|
||||
console.error('Failed to delete messages:', error)
|
||||
window.message.error(t('message.delete.failed'))
|
||||
}
|
||||
}
|
||||
})
|
||||
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`
|
||||
await window.api.file.save(fileName, contentToSave)
|
||||
window.message.success({ content: t('message.save.success.title'), key: 'save-messages' })
|
||||
handleToggleMultiSelectMode(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' })
|
||||
handleToggleMultiSelectMode(false)
|
||||
} else {
|
||||
window.message.warning(t('message.copy.no.assistant'))
|
||||
}
|
||||
break
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
},
|
||||
[t, store, activeTopic.id, deleteMessage, handleToggleMultiSelectMode]
|
||||
)
|
||||
|
||||
return {
|
||||
isMultiSelectMode,
|
||||
selectedMessageIds,
|
||||
toggleMultiSelectMode: handleToggleMultiSelectMode,
|
||||
handleMultiSelectAction,
|
||||
handleSelectMessage,
|
||||
activeTopic,
|
||||
locateMessage,
|
||||
messageRefs,
|
||||
registerMessageElement
|
||||
}
|
||||
}
|
||||
@ -3,7 +3,6 @@ import { HStack } from '@renderer/components/Layout'
|
||||
import { MessageEditingProvider } from '@renderer/context/MessageEditingContext'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { getTopicById } from '@renderer/hooks/useTopic'
|
||||
import { ChatProvider } from '@renderer/pages/home/Messages/ChatContext'
|
||||
import { default as MessageItem } from '@renderer/pages/home/Messages/Message'
|
||||
import { locateToMessage } from '@renderer/services/MessagesService'
|
||||
import NavigationService from '@renderer/services/NavigationService'
|
||||
@ -43,27 +42,25 @@ const SearchMessage: FC<Props> = ({ message, ...props }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<ChatProvider activeTopic={topic}>
|
||||
<MessageEditingProvider>
|
||||
<MessagesContainer {...props} className={messageStyle}>
|
||||
<ContainerWrapper style={{ paddingTop: 20, paddingBottom: 20, position: 'relative' }}>
|
||||
<MessageItem message={message} topic={topic} hideMenuBar={true} />
|
||||
<Button
|
||||
type="text"
|
||||
size="middle"
|
||||
style={{ color: 'var(--color-text-3)', position: 'absolute', right: 0, top: 10 }}
|
||||
onClick={() => locateToMessage(navigate, message)}
|
||||
icon={<ArrowRightOutlined />}
|
||||
/>
|
||||
<HStack mt="10px" justifyContent="center">
|
||||
<Button onClick={() => locateToMessage(navigate, message)} icon={<ArrowRightOutlined />}>
|
||||
{t('history.locate.message')}
|
||||
</Button>
|
||||
</HStack>
|
||||
</ContainerWrapper>
|
||||
</MessagesContainer>
|
||||
</MessageEditingProvider>
|
||||
</ChatProvider>
|
||||
<MessageEditingProvider>
|
||||
<MessagesContainer {...props} className={messageStyle}>
|
||||
<ContainerWrapper style={{ paddingTop: 20, paddingBottom: 20, position: 'relative' }}>
|
||||
<MessageItem message={message} topic={topic} hideMenuBar={true} />
|
||||
<Button
|
||||
type="text"
|
||||
size="middle"
|
||||
style={{ color: 'var(--color-text-3)', position: 'absolute', right: 0, top: 10 }}
|
||||
onClick={() => locateToMessage(navigate, message)}
|
||||
icon={<ArrowRightOutlined />}
|
||||
/>
|
||||
<HStack mt="10px" justifyContent="center">
|
||||
<Button onClick={() => locateToMessage(navigate, message)} icon={<ArrowRightOutlined />}>
|
||||
{t('history.locate.message')}
|
||||
</Button>
|
||||
</HStack>
|
||||
</ContainerWrapper>
|
||||
</MessagesContainer>
|
||||
</MessageEditingProvider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -4,7 +4,6 @@ import SearchPopup from '@renderer/components/Popups/SearchPopup'
|
||||
import { MessageEditingProvider } from '@renderer/context/MessageEditingContext'
|
||||
import useScrollPosition from '@renderer/hooks/useScrollPosition'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { ChatProvider } from '@renderer/pages/home/Messages/ChatContext'
|
||||
import { getAssistantById } from '@renderer/services/AssistantService'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { isGenerating, locateToMessage } from '@renderer/services/MessagesService'
|
||||
@ -48,35 +47,33 @@ const TopicMessages: FC<Props> = ({ topic, ...props }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<ChatProvider activeTopic={topic}>
|
||||
<MessageEditingProvider>
|
||||
<MessagesContainer {...props} ref={containerRef} onScroll={handleScroll} className={messageStyle}>
|
||||
<ContainerWrapper style={{ paddingTop: 30, paddingBottom: 30 }}>
|
||||
{topic?.messages.map((message) => (
|
||||
<div key={message.id} style={{ position: 'relative' }}>
|
||||
<MessageItem message={message} topic={topic} hideMenuBar={true} />
|
||||
<Button
|
||||
type="text"
|
||||
size="middle"
|
||||
style={{ color: 'var(--color-text-3)', position: 'absolute', right: 0, top: 5 }}
|
||||
onClick={() => locateToMessage(navigate, message)}
|
||||
icon={<ArrowRightOutlined />}
|
||||
/>
|
||||
<Divider style={{ margin: '8px auto 15px' }} variant="dashed" />
|
||||
</div>
|
||||
))}
|
||||
{isEmpty && <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />}
|
||||
{!isEmpty && (
|
||||
<HStack justifyContent="center">
|
||||
<Button onClick={() => onContinueChat(topic)} icon={<MessageOutlined />}>
|
||||
{t('history.continue_chat')}
|
||||
</Button>
|
||||
</HStack>
|
||||
)}
|
||||
</ContainerWrapper>
|
||||
</MessagesContainer>
|
||||
</MessageEditingProvider>
|
||||
</ChatProvider>
|
||||
<MessageEditingProvider>
|
||||
<MessagesContainer {...props} ref={containerRef} onScroll={handleScroll} className={messageStyle}>
|
||||
<ContainerWrapper style={{ paddingTop: 30, paddingBottom: 30 }}>
|
||||
{topic?.messages.map((message) => (
|
||||
<div key={message.id} style={{ position: 'relative' }}>
|
||||
<MessageItem message={message} topic={topic} hideMenuBar={true} />
|
||||
<Button
|
||||
type="text"
|
||||
size="middle"
|
||||
style={{ color: 'var(--color-text-3)', position: 'absolute', right: 0, top: 5 }}
|
||||
onClick={() => locateToMessage(navigate, message)}
|
||||
icon={<ArrowRightOutlined />}
|
||||
/>
|
||||
<Divider style={{ margin: '8px auto 15px' }} variant="dashed" />
|
||||
</div>
|
||||
))}
|
||||
{isEmpty && <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />}
|
||||
{!isEmpty && (
|
||||
<HStack justifyContent="center">
|
||||
<Button onClick={() => onContinueChat(topic)} icon={<MessageOutlined />}>
|
||||
{t('history.continue_chat')}
|
||||
</Button>
|
||||
</HStack>
|
||||
)}
|
||||
</ContainerWrapper>
|
||||
</MessagesContainer>
|
||||
</MessageEditingProvider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ import { ContentSearch, ContentSearchRef } from '@renderer/components/ContentSea
|
||||
import MultiSelectActionPopup from '@renderer/components/Popups/MultiSelectionPopup'
|
||||
import { QuickPanelProvider } from '@renderer/components/QuickPanel'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { useChatContext } from '@renderer/hooks/useChatContext'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useShortcut } from '@renderer/hooks/useShortcuts'
|
||||
import { useShowTopics } from '@renderer/hooks/useStore'
|
||||
@ -13,7 +14,6 @@ import { useHotkeys } from 'react-hotkeys-hook'
|
||||
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'
|
||||
|
||||
@ -28,7 +28,7 @@ const ChatContent: FC<Props> = (props) => {
|
||||
const { assistant } = useAssistant(props.assistant.id)
|
||||
const { topicPosition, messageStyle, showAssistants } = useSettings()
|
||||
const { showTopics } = useShowTopics()
|
||||
const { isMultiSelectMode } = useChatContext()
|
||||
const { isMultiSelectMode } = useChatContext(props.activeTopic)
|
||||
|
||||
const mainRef = React.useRef<HTMLDivElement>(null)
|
||||
const contentSearchRef = React.useRef<ContentSearchRef>(null)
|
||||
@ -127,7 +127,7 @@ const ChatContent: FC<Props> = (props) => {
|
||||
</MessagesContainer>
|
||||
<QuickPanelProvider>
|
||||
<Inputbar assistant={assistant} setActiveTopic={props.setActiveTopic} topic={props.activeTopic} />
|
||||
{isMultiSelectMode && <MultiSelectActionPopup />}
|
||||
{isMultiSelectMode && <MultiSelectActionPopup topic={props.activeTopic} />}
|
||||
</QuickPanelProvider>
|
||||
</Main>
|
||||
{topicPosition === 'right' && showTopics && (
|
||||
@ -144,11 +144,7 @@ const ChatContent: FC<Props> = (props) => {
|
||||
}
|
||||
|
||||
const Chat: FC<Props> = (props) => {
|
||||
return (
|
||||
<ChatProvider activeTopic={props.activeTopic}>
|
||||
<ChatContent {...props} />
|
||||
</ChatProvider>
|
||||
)
|
||||
return <ChatContent {...props} />
|
||||
}
|
||||
|
||||
const MessagesContainer = styled.div`
|
||||
|
||||
@ -1,200 +0,0 @@
|
||||
import { useMessageOperations } from '@renderer/hooks/useMessageOperations'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { RootState } from '@renderer/store'
|
||||
import { messageBlocksSelectors } from '@renderer/store/messageBlock'
|
||||
import { selectMessagesForTopic } from '@renderer/store/newMessage'
|
||||
import { Topic } from '@renderer/types'
|
||||
import { createContext, FC, ReactNode, use, useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStore } from 'react-redux'
|
||||
|
||||
interface ChatContextProps {
|
||||
isMultiSelectMode: boolean
|
||||
selectedMessageIds: string[]
|
||||
toggleMultiSelectMode: (value: boolean) => void
|
||||
handleMultiSelectAction: (actionType: string, messageIds: string[]) => void
|
||||
handleSelectMessage: (messageId: string, selected: boolean) => void
|
||||
activeTopic: Topic
|
||||
locateMessage: (messageId: string) => void
|
||||
messageRefs: Map<string, HTMLElement>
|
||||
registerMessageElement: (id: string, element: HTMLElement | null) => void
|
||||
}
|
||||
|
||||
interface ChatProviderProps {
|
||||
children: ReactNode
|
||||
activeTopic: Topic
|
||||
}
|
||||
|
||||
const ChatContext = createContext<ChatContextProps | undefined>(undefined)
|
||||
|
||||
export const useChatContext = () => {
|
||||
const context = use(ChatContext)
|
||||
if (!context) {
|
||||
throw new Error('useChatContext 必须在 ChatProvider 内使用')
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
export const ChatProvider: FC<ChatProviderProps> = ({ children, activeTopic }) => {
|
||||
const { t } = useTranslation()
|
||||
const { deleteMessage } = useMessageOperations(activeTopic)
|
||||
const [isMultiSelectMode, setIsMultiSelectMode] = useState(false)
|
||||
const [selectedMessageIds, setSelectedMessageIds] = useState<string[]>([])
|
||||
const [messageRefs, setMessageRefs] = useState<Map<string, HTMLElement>>(new Map())
|
||||
|
||||
const store = useStore<RootState>()
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = EventEmitter.on(EVENT_NAMES.CHANGE_TOPIC, () => setIsMultiSelectMode(false))
|
||||
return () => unsubscribe()
|
||||
}, [])
|
||||
|
||||
const toggleMultiSelectMode = (value: boolean) => {
|
||||
setIsMultiSelectMode(value)
|
||||
if (!value) {
|
||||
setSelectedMessageIds([])
|
||||
}
|
||||
}
|
||||
|
||||
const registerMessageElement = useCallback((id: string, element: HTMLElement | null) => {
|
||||
setMessageRefs((prev) => {
|
||||
const newRefs = new Map(prev)
|
||||
if (element) {
|
||||
newRefs.set(id, element)
|
||||
} else {
|
||||
newRefs.delete(id)
|
||||
}
|
||||
return newRefs
|
||||
})
|
||||
}, [])
|
||||
|
||||
const locateMessage = (messageId: string) => {
|
||||
const messageElement = messageRefs.get(messageId)
|
||||
if (messageElement) {
|
||||
// 检查消息是否可见
|
||||
const display = window.getComputedStyle(messageElement).display
|
||||
|
||||
if (display === 'none') {
|
||||
// 如果消息隐藏,需要处理显示逻辑
|
||||
// 查找消息并设置为选中状态
|
||||
const state = store.getState()
|
||||
const messages = selectMessagesForTopic(state, activeTopic.id)
|
||||
const message = messages.find((m) => m.id === messageId)
|
||||
if (message) {
|
||||
// 这里需要实现设置消息为选中状态的逻辑
|
||||
// 可能需要调用其他函数或修改状态
|
||||
}
|
||||
}
|
||||
|
||||
// 滚动到消息位置
|
||||
messageElement.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
}
|
||||
}
|
||||
|
||||
const handleSelectMessage = (messageId: string, selected: boolean) => {
|
||||
setSelectedMessageIds((prev) => {
|
||||
const newSet = new Set(prev)
|
||||
if (selected) {
|
||||
newSet.add(messageId)
|
||||
} else {
|
||||
newSet.delete(messageId)
|
||||
}
|
||||
return Array.from(newSet)
|
||||
})
|
||||
}
|
||||
|
||||
const handleMultiSelectAction = (actionType: string, messageIds: string[]) => {
|
||||
if (messageIds.length === 0) {
|
||||
window.message.warning(t('chat.multiple.select.empty'))
|
||||
return
|
||||
}
|
||||
|
||||
const state = store.getState()
|
||||
const messages = selectMessagesForTopic(state, activeTopic.id)
|
||||
const messageBlocks = messageBlocksSelectors.selectEntities(state)
|
||||
|
||||
switch (actionType) {
|
||||
case 'delete':
|
||||
window.modal.confirm({
|
||||
title: t('message.delete.confirm.title'),
|
||||
content: t('message.delete.confirm.content', { count: messageIds.length }),
|
||||
okButtonProps: { danger: true },
|
||||
centered: true,
|
||||
onOk: async () => {
|
||||
try {
|
||||
await Promise.all(messageIds.map((messageId) => deleteMessage(messageId)))
|
||||
window.message.success(t('message.delete.success'))
|
||||
toggleMultiSelectMode(false)
|
||||
} catch (error) {
|
||||
console.error('Failed to delete messages:', error)
|
||||
window.message.error(t('message.delete.failed'))
|
||||
}
|
||||
}
|
||||
})
|
||||
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 value = {
|
||||
isMultiSelectMode,
|
||||
selectedMessageIds,
|
||||
toggleMultiSelectMode,
|
||||
handleMultiSelectAction,
|
||||
handleSelectMessage,
|
||||
activeTopic,
|
||||
locateMessage,
|
||||
messageRefs,
|
||||
registerMessageElement
|
||||
}
|
||||
|
||||
return <ChatContext value={value}>{children}</ChatContext>
|
||||
}
|
||||
@ -162,6 +162,7 @@ const MessageGroup = ({ messages, topic, hidePresetMessages, registerMessageElem
|
||||
<SelectableMessage
|
||||
key={`selectable-${message.id}`}
|
||||
messageId={message.id}
|
||||
topic={topic}
|
||||
isClearMessage={message.type === 'clear'}>
|
||||
{messageContent}
|
||||
</SelectableMessage>
|
||||
|
||||
@ -3,6 +3,7 @@ import ObsidianExportPopup from '@renderer/components/Popups/ObsidianExportPopup
|
||||
import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup'
|
||||
import { TranslateLanguageOptions } from '@renderer/config/translate'
|
||||
import { useMessageEditing } from '@renderer/context/MessageEditingContext'
|
||||
import { useChatContext } from '@renderer/hooks/useChatContext'
|
||||
import { useMessageOperations, useTopicLoading } from '@renderer/hooks/useMessageOperations'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { getMessageTitle } from '@renderer/services/MessagesService'
|
||||
@ -33,8 +34,6 @@ import { useTranslation } from 'react-i18next'
|
||||
import { useSelector } from 'react-redux'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { useChatContext } from './ChatContext'
|
||||
|
||||
interface Props {
|
||||
message: Message
|
||||
assistant: Assistant
|
||||
@ -52,7 +51,7 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
const { message, index, isGrouped, isLastMessage, isAssistantMessage, assistant, topic, model, messageContainerRef } =
|
||||
props
|
||||
const { t } = useTranslation()
|
||||
const { toggleMultiSelectMode } = useChatContext()
|
||||
const { toggleMultiSelectMode } = useChatContext(props.topic)
|
||||
const [copied, setCopied] = useState(false)
|
||||
const [isTranslating, setIsTranslating] = useState(false)
|
||||
const [showRegenerateTooltip, setShowRegenerateTooltip] = useState(false)
|
||||
|
||||
@ -1,23 +1,24 @@
|
||||
import { useChatContext } from '@renderer/hooks/useChatContext'
|
||||
import { Topic } from '@renderer/types'
|
||||
import { Checkbox } from 'antd'
|
||||
import { FC, ReactNode, useEffect, useRef } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { useChatContext } from './ChatContext'
|
||||
|
||||
interface SelectableMessageProps {
|
||||
children: ReactNode
|
||||
messageId: string
|
||||
topic: Topic
|
||||
isClearMessage?: boolean
|
||||
}
|
||||
|
||||
const SelectableMessage: FC<SelectableMessageProps> = ({ children, messageId, isClearMessage = false }) => {
|
||||
const SelectableMessage: FC<SelectableMessageProps> = ({ children, messageId, topic, isClearMessage = false }) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const {
|
||||
registerMessageElement: contextRegister,
|
||||
isMultiSelectMode,
|
||||
selectedMessageIds,
|
||||
handleSelectMessage
|
||||
} = useChatContext()
|
||||
} = useChatContext(topic)
|
||||
|
||||
const isSelected = selectedMessageIds?.includes(messageId)
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ import SvgSpinners180Ring from '@renderer/components/Icons/SvgSpinners180Ring'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { LOAD_MORE_COUNT } from '@renderer/config/constant'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { useChatContext } from '@renderer/hooks/useChatContext'
|
||||
import { useMessageOperations, useTopicMessages } from '@renderer/hooks/useMessageOperations'
|
||||
import useScrollPosition from '@renderer/hooks/useScrollPosition'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
@ -32,7 +33,6 @@ 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'
|
||||
@ -53,7 +53,7 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic, o
|
||||
)
|
||||
const { t } = useTranslation()
|
||||
const { showPrompt, showTopics, topicPosition, showAssistants, messageNavigation } = useSettings()
|
||||
const { isMultiSelectMode, handleSelectMessage } = useChatContext()
|
||||
const { isMultiSelectMode, handleSelectMessage } = useChatContext(topic)
|
||||
const { updateTopic, addTopic } = useAssistant(assistant.id)
|
||||
const dispatch = useAppDispatch()
|
||||
const [displayMessages, setDisplayMessages] = useState<Message[]>([])
|
||||
|
||||
@ -1,7 +1,14 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||
import { AppLogo, UserAvatar } from '@renderer/config/env'
|
||||
import type { MinAppType } from '@renderer/types'
|
||||
import type { MinAppType, Topic } from '@renderer/types'
|
||||
import type { UpdateInfo } from 'builder-util-runtime'
|
||||
|
||||
export interface ChatState {
|
||||
isMultiSelectMode: boolean
|
||||
selectedMessageIds: string[]
|
||||
activeTopic: Topic | null
|
||||
}
|
||||
|
||||
export interface UpdateState {
|
||||
info: UpdateInfo | null
|
||||
checking: boolean
|
||||
@ -27,6 +34,7 @@ export interface RuntimeState {
|
||||
resourcesPath: string
|
||||
update: UpdateState
|
||||
export: ExportState
|
||||
chat: ChatState
|
||||
}
|
||||
|
||||
export interface ExportState {
|
||||
@ -53,6 +61,11 @@ const initialState: RuntimeState = {
|
||||
},
|
||||
export: {
|
||||
isExporting: false
|
||||
},
|
||||
chat: {
|
||||
isMultiSelectMode: false,
|
||||
selectedMessageIds: [],
|
||||
activeTopic: null
|
||||
}
|
||||
}
|
||||
|
||||
@ -92,6 +105,19 @@ const runtimeSlice = createSlice({
|
||||
},
|
||||
setExportState: (state, action: PayloadAction<Partial<ExportState>>) => {
|
||||
state.export = { ...state.export, ...action.payload }
|
||||
},
|
||||
// Chat related actions
|
||||
toggleMultiSelectMode: (state, action: PayloadAction<boolean>) => {
|
||||
state.chat.isMultiSelectMode = action.payload
|
||||
if (!action.payload) {
|
||||
state.chat.selectedMessageIds = []
|
||||
}
|
||||
},
|
||||
setSelectedMessageIds: (state, action: PayloadAction<string[]>) => {
|
||||
state.chat.selectedMessageIds = action.payload
|
||||
},
|
||||
setActiveTopic: (state, action: PayloadAction<Topic>) => {
|
||||
state.chat.activeTopic = action.payload
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -107,7 +133,11 @@ export const {
|
||||
setFilesPath,
|
||||
setResourcesPath,
|
||||
setUpdateState,
|
||||
setExportState
|
||||
setExportState,
|
||||
// Chat related actions
|
||||
toggleMultiSelectMode,
|
||||
setSelectedMessageIds,
|
||||
setActiveTopic
|
||||
} = runtimeSlice.actions
|
||||
|
||||
export default runtimeSlice.reducer
|
||||
|
||||
Loading…
Reference in New Issue
Block a user