feat: enhance multi-select functionality with message registration

This commit is contained in:
Pleasurecruise 2025-05-19 00:51:40 +08:00
parent 7ae21b6b26
commit 7b4f79aab5
No known key found for this signature in database
GPG Key ID: E6385136096279B6
3 changed files with 54 additions and 44 deletions

View File

@ -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 (

View File

@ -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
] ]
) )

View File

@ -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}>