mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-07 22:10:21 +08:00
feat: enhance multi-select functionality with message registration
This commit is contained in:
parent
7ae21b6b26
commit
7b4f79aab5
@ -3,7 +3,7 @@ import { messageBlocksSelectors } from '@renderer/store/messageBlock'
|
|||||||
import { newMessagesActions, selectMessagesForTopic } from '@renderer/store/newMessage'
|
import { newMessagesActions, selectMessagesForTopic } from '@renderer/store/newMessage'
|
||||||
import { Topic } from '@renderer/types'
|
import { Topic } from '@renderer/types'
|
||||||
import { Modal } from 'antd'
|
import { Modal } from 'antd'
|
||||||
import { createContext, FC, ReactNode, use, useState } from 'react'
|
import { createContext, FC, ReactNode, use, useCallback, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useDispatch, useSelector } from 'react-redux'
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
|
|
||||||
@ -12,6 +12,8 @@ interface ChatContextProps {
|
|||||||
toggleMultiSelectMode: (value: boolean) => void
|
toggleMultiSelectMode: (value: boolean) => void
|
||||||
handleMultiSelectAction: (actionType: string, messageIds: string[]) => void
|
handleMultiSelectAction: (actionType: string, messageIds: string[]) => void
|
||||||
activeTopic: Topic
|
activeTopic: Topic
|
||||||
|
messageRefs: Map<string, HTMLElement>
|
||||||
|
registerMessageElement: (id: string, element: HTMLElement | null) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const ChatContext = createContext<ChatContextProps | undefined>(undefined)
|
const ChatContext = createContext<ChatContextProps | undefined>(undefined)
|
||||||
@ -35,6 +37,7 @@ export const ChatProvider: FC<ChatProviderProps> = ({ children, activeTopic }) =
|
|||||||
const [isMultiSelectMode, setIsMultiSelectMode] = useState(false)
|
const [isMultiSelectMode, setIsMultiSelectMode] = useState(false)
|
||||||
const [confirmDeleteVisible, setConfirmDeleteVisible] = useState(false)
|
const [confirmDeleteVisible, setConfirmDeleteVisible] = useState(false)
|
||||||
const [messagesToDelete, setMessagesToDelete] = useState<string[]>([])
|
const [messagesToDelete, setMessagesToDelete] = useState<string[]>([])
|
||||||
|
const [messageRefs, setMessageRefs] = useState<Map<string, HTMLElement>>(new Map())
|
||||||
|
|
||||||
const messages = useSelector((state: RootState) => selectMessagesForTopic(state, activeTopic.id))
|
const messages = useSelector((state: RootState) => selectMessagesForTopic(state, activeTopic.id))
|
||||||
const messageBlocks = useSelector(messageBlocksSelectors.selectEntities)
|
const messageBlocks = useSelector(messageBlocksSelectors.selectEntities)
|
||||||
@ -43,6 +46,18 @@ export const ChatProvider: FC<ChatProviderProps> = ({ children, activeTopic }) =
|
|||||||
setIsMultiSelectMode(value)
|
setIsMultiSelectMode(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 handleMultiSelectAction = (actionType: string, messageIds: string[]) => {
|
const handleMultiSelectAction = (actionType: string, messageIds: string[]) => {
|
||||||
if (messageIds.length === 0) {
|
if (messageIds.length === 0) {
|
||||||
window.message.warning(t('chat.multiple.select.empty'))
|
window.message.warning(t('chat.multiple.select.empty'))
|
||||||
@ -134,7 +149,9 @@ export const ChatProvider: FC<ChatProviderProps> = ({ children, activeTopic }) =
|
|||||||
isMultiSelectMode,
|
isMultiSelectMode,
|
||||||
toggleMultiSelectMode,
|
toggleMultiSelectMode,
|
||||||
handleMultiSelectAction,
|
handleMultiSelectAction,
|
||||||
activeTopic
|
activeTopic,
|
||||||
|
messageRefs,
|
||||||
|
registerMessageElement
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import Scrollbar from '@renderer/components/Scrollbar'
|
import Scrollbar from '@renderer/components/Scrollbar'
|
||||||
import { useMessageOperations } from '@renderer/hooks/useMessageOperations'
|
import { useMessageOperations } from '@renderer/hooks/useMessageOperations'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
|
||||||
import { MultiModelMessageStyle } from '@renderer/store/settings'
|
import { MultiModelMessageStyle } from '@renderer/store/settings'
|
||||||
import type { Topic } from '@renderer/types'
|
import type { Topic } from '@renderer/types'
|
||||||
import type { Message } from '@renderer/types/newMessage'
|
import type { Message } from '@renderer/types/newMessage'
|
||||||
@ -10,6 +9,7 @@ import { Popover } from 'antd'
|
|||||||
import { memo, useCallback, useEffect, useRef, useState } from 'react'
|
import { memo, useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import styled, { css } from 'styled-components'
|
import styled, { css } from 'styled-components'
|
||||||
|
|
||||||
|
import { useChatContext } from './ChatContext'
|
||||||
import MessageItem from './Message'
|
import MessageItem from './Message'
|
||||||
import MessageGroupMenuBar from './MessageGroupMenuBar'
|
import MessageGroupMenuBar from './MessageGroupMenuBar'
|
||||||
import SelectableMessage from './MessageSelect'
|
import SelectableMessage from './MessageSelect'
|
||||||
@ -18,9 +18,9 @@ interface Props {
|
|||||||
messages: (Message & { index: number })[]
|
messages: (Message & { index: number })[]
|
||||||
topic: Topic
|
topic: Topic
|
||||||
hidePresetMessages?: boolean
|
hidePresetMessages?: boolean
|
||||||
isMultiSelectMode?: boolean // 添加是否处于多选模式
|
isMultiSelectMode?: boolean
|
||||||
selectedMessages?: Set<string> // 已选择的消息ID集合
|
selectedMessages?: Set<string>
|
||||||
onSelectMessage?: (messageId: string, selected: boolean) => void // 消息选择回调
|
onSelectMessage?: (messageId: string, selected: boolean) => void
|
||||||
registerMessageElement?: (id: string, element: HTMLElement | null) => void
|
registerMessageElement?: (id: string, element: HTMLElement | null) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,6 +35,7 @@ const MessageGroup = ({
|
|||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { editMessage } = useMessageOperations(topic)
|
const { editMessage } = useMessageOperations(topic)
|
||||||
const { multiModelMessageStyle: multiModelMessageStyleSetting, gridColumns, gridPopoverTrigger } = useSettings()
|
const { multiModelMessageStyle: multiModelMessageStyleSetting, gridColumns, gridPopoverTrigger } = useSettings()
|
||||||
|
const { registerMessageElement: contextRegisterMessageElement } = useChatContext()
|
||||||
|
|
||||||
const [multiModelMessageStyle, setMultiModelMessageStyle] = useState<MultiModelMessageStyle>(
|
const [multiModelMessageStyle, setMultiModelMessageStyle] = useState<MultiModelMessageStyle>(
|
||||||
messages[0].multiModelMessageStyle || multiModelMessageStyleSetting
|
messages[0].multiModelMessageStyle || multiModelMessageStyleSetting
|
||||||
@ -123,41 +124,20 @@ const MessageGroup = ({
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [messages, selectedIndex, isGrouped, messageLength])
|
}, [messages, selectedIndex, isGrouped, messageLength])
|
||||||
|
|
||||||
// 添加对LOCATE_MESSAGE事件的监听
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 为每个消息注册一个定位事件监听器
|
|
||||||
const eventHandlers: { [key: string]: () => void } = {}
|
|
||||||
|
|
||||||
messages.forEach((message) => {
|
messages.forEach((message) => {
|
||||||
const eventName = EVENT_NAMES.LOCATE_MESSAGE + ':' + message.id
|
const element = document.getElementById(`message-${message.id}`)
|
||||||
const handler = () => {
|
if (element) {
|
||||||
// 检查消息是否处于可见状态
|
contextRegisterMessageElement(message.id, element)
|
||||||
const element = document.getElementById(`message-${message.id}`)
|
|
||||||
if (element) {
|
|
||||||
const display = window.getComputedStyle(element).display
|
|
||||||
|
|
||||||
if (display === 'none') {
|
|
||||||
// 如果消息隐藏,先切换标签
|
|
||||||
setSelectedMessage(message)
|
|
||||||
} else {
|
|
||||||
// 直接滚动
|
|
||||||
element.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
eventHandlers[eventName] = handler
|
|
||||||
EventEmitter.on(eventName, handler)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 清理函数
|
|
||||||
return () => {
|
return () => {
|
||||||
// 移除所有事件监听器
|
messages.forEach((message) => {
|
||||||
Object.entries(eventHandlers).forEach(([eventName, handler]) => {
|
contextRegisterMessageElement(message.id, null)
|
||||||
EventEmitter.off(eventName, handler)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, [messages, setSelectedMessage])
|
}, [messages, contextRegisterMessageElement])
|
||||||
|
|
||||||
const renderMessage = useCallback(
|
const renderMessage = useCallback(
|
||||||
(message: Message & { index: number }, index: number) => {
|
(message: Message & { index: number }, index: number) => {
|
||||||
@ -227,16 +207,17 @@ const MessageGroup = ({
|
|||||||
[
|
[
|
||||||
isGrid,
|
isGrid,
|
||||||
isGrouped,
|
isGrouped,
|
||||||
isHorizontal,
|
|
||||||
multiModelMessageStyle,
|
|
||||||
selectedIndex,
|
|
||||||
topic,
|
topic,
|
||||||
hidePresetMessages,
|
hidePresetMessages,
|
||||||
gridPopoverTrigger,
|
multiModelMessageStyle,
|
||||||
|
selectedIndex,
|
||||||
|
isHorizontal,
|
||||||
getSelectedMessageId,
|
getSelectedMessageId,
|
||||||
isMultiSelectMode, // 添加依赖项
|
isMultiSelectMode,
|
||||||
selectedMessages, // 添加依赖项
|
selectedMessages,
|
||||||
onSelectMessage // 添加依赖项
|
registerMessageElement,
|
||||||
|
onSelectMessage,
|
||||||
|
gridPopoverTrigger
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,8 @@ import { Checkbox } from 'antd'
|
|||||||
import { FC, ReactNode, useEffect, useRef } from 'react'
|
import { FC, ReactNode, useEffect, useRef } from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
import { useChatContext } from './ChatContext'
|
||||||
|
|
||||||
interface SelectableMessageProps {
|
interface SelectableMessageProps {
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
isMultiSelectMode: boolean
|
isMultiSelectMode: boolean
|
||||||
@ -20,14 +22,24 @@ const SelectableMessage: FC<SelectableMessageProps> = ({
|
|||||||
registerElement
|
registerElement
|
||||||
}) => {
|
}) => {
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
|
const { registerMessageElement: contextRegister } = useChatContext()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (registerElement && containerRef.current) {
|
if (containerRef.current) {
|
||||||
registerElement(messageId, containerRef.current)
|
if (registerElement) {
|
||||||
return () => registerElement(messageId, null)
|
registerElement(messageId, containerRef.current)
|
||||||
|
}
|
||||||
|
contextRegister(messageId, containerRef.current)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (registerElement) {
|
||||||
|
registerElement(messageId, null)
|
||||||
|
}
|
||||||
|
contextRegister(messageId, null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return undefined
|
return undefined
|
||||||
}, [messageId, registerElement])
|
}, [messageId, registerElement, contextRegister])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container ref={containerRef}>
|
<Container ref={containerRef}>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user