feat: implement message location functionality and refactor multi-select handling

- Added event listeners for LOCATE_MESSAGE events to scroll to specific messages in the MessageGroup component.
- Introduced a new SelectionBox component to handle multi-select functionality, allowing users to select multiple messages with drag actions.
- Refactored Messages component to remove unused multi-select logic and improve overall structure.
- Cleaned up code by removing commented-out sections and unnecessary state management related to dragging.
This commit is contained in:
kangfenmao 2025-05-22 15:14:13 +08:00
parent a5738fdae5
commit fd1cf1331f
4 changed files with 190 additions and 136 deletions

View File

@ -24,7 +24,7 @@ interface Props {
setActiveAssistant: (assistant: Assistant) => void
}
const ChatContent: FC<Props> = (props) => {
const Chat: FC<Props> = (props) => {
const { assistant } = useAssistant(props.assistant.id)
const { topicPosition, messageStyle, showAssistants } = useSettings()
const { showTopics } = useShowTopics()
@ -143,10 +143,6 @@ const ChatContent: FC<Props> = (props) => {
)
}
const Chat: FC<Props> = (props) => {
return <ChatContent {...props} />
}
const MessagesContainer = styled.div`
display: flex;
flex-direction: column;

View File

@ -3,6 +3,7 @@ import { MessageEditingProvider } from '@renderer/context/MessageEditingContext'
import { useChatContext } from '@renderer/hooks/useChatContext'
import { useMessageOperations } from '@renderer/hooks/useMessageOperations'
import { useSettings } from '@renderer/hooks/useSettings'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { MultiModelMessageStyle } from '@renderer/store/settings'
import type { Topic } from '@renderer/types'
import type { Message } from '@renderer/types/newMessage'
@ -115,19 +116,48 @@ const MessageGroup = ({ messages, topic, hidePresetMessages, registerMessageElem
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [messages, selectedIndex, isGrouped, messageLength])
// 添加对LOCATE_MESSAGE事件的监听
useEffect(() => {
// 为每个消息注册一个定位事件监听器
const eventHandlers: { [key: string]: () => void } = {}
messages.forEach((message) => {
const eventName = EVENT_NAMES.LOCATE_MESSAGE + ':' + message.id
const handler = () => {
// 检查消息是否处于可见状态
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 () => {
// 移除所有事件监听器
Object.entries(eventHandlers).forEach(([eventName, handler]) => {
EventEmitter.off(eventName, handler)
})
}
}, [messages, setSelectedMessage])
useEffect(() => {
messages.forEach((message) => {
const element = document.getElementById(`message-${message.id}`)
if (element) {
registerMessageElement?.(message.id, element)
}
element && registerMessageElement?.(message.id, element)
})
return () => {
messages.forEach((message) => {
registerMessageElement?.(message.id, null)
})
}
return () => messages.forEach((message) => registerMessageElement?.(message.id, null))
}, [messages, registerMessageElement])
const renderMessage = useCallback(

View File

@ -2,7 +2,6 @@ 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'
@ -53,7 +52,6 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic, o
)
const { t } = useTranslation()
const { showPrompt, showTopics, topicPosition, showAssistants, messageNavigation } = useSettings()
const { isMultiSelectMode, handleSelectMessage } = useChatContext(topic)
const { updateTopic, addTopic } = useAssistant(assistant.id)
const dispatch = useAppDispatch()
const [displayMessages, setDisplayMessages] = useState<Message[]>([])
@ -61,117 +59,17 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic, o
const [isLoadingMore, setIsLoadingMore] = useState(false)
const [isProcessingContext, setIsProcessingContext] = useState(false)
const [isDragging, setIsDragging] = useState(false)
const [dragStart, setDragStart] = useState({ x: 0, y: 0 })
const [dragCurrent, setDragCurrent] = useState({ x: 0, y: 0 })
const messageElements = useRef<Map<string, HTMLElement>>(new Map())
const messages = useTopicMessages(topic.id)
const { displayCount, clearTopicMessages, deleteMessage, createTopicBranch } = useMessageOperations(topic)
const messagesRef = useRef<Message[]>(messages)
// const { isMultiSelectMode, handleSelectMessage } = useChatContext(topic)
useEffect(() => {
messagesRef.current = messages
}, [messages])
useEffect(() => {
if (!isMultiSelectMode) return
const updateDragPos = (e: MouseEvent) => {
const container = scrollContainerRef.current!
if (!container) return { x: 0, y: 0 }
const rect = container.getBoundingClientRect()
const x = e.clientX - rect.left + container.scrollLeft
const y = e.clientY - rect.top + container.scrollTop
return { x, y }
}
const handleMouseDown = (e: MouseEvent) => {
if ((e.target as HTMLElement).closest('.ant-checkbox-wrapper')) return
if ((e.target as HTMLElement).closest('.MessageFooter')) return
setIsDragging(true)
const pos = updateDragPos(e)
setDragStart(pos)
setDragCurrent(pos)
document.body.classList.add('no-select')
}
const handleMouseMove = (e: MouseEvent) => {
if (!isDragging) return
setDragCurrent(updateDragPos(e))
const container = scrollContainerRef.current!
if (container) {
const { top, bottom } = container.getBoundingClientRect()
const scrollSpeed = 15
if (e.clientY < top + 50) {
container.scrollBy(0, -scrollSpeed)
} else if (e.clientY > bottom - 50) {
container.scrollBy(0, scrollSpeed)
}
}
}
const handleMouseUp = () => {
if (!isDragging) return
const left = Math.min(dragStart.x, dragCurrent.x)
const right = Math.max(dragStart.x, dragCurrent.x)
const top = Math.min(dragStart.y, dragCurrent.y)
const bottom = Math.max(dragStart.y, dragCurrent.y)
const MIN_SELECTION_SIZE = 5
const isValidSelection =
Math.abs(right - left) > MIN_SELECTION_SIZE && Math.abs(bottom - top) > MIN_SELECTION_SIZE
if (isValidSelection) {
// 处理元素选择
messageElements.current.forEach((element, messageId) => {
try {
const rect = element.getBoundingClientRect()
const container = scrollContainerRef.current!
const elementTop = rect.top - container.getBoundingClientRect().top + container.scrollTop
const elementLeft = rect.left - container.getBoundingClientRect().left + container.scrollLeft
const elementBottom = elementTop + rect.height
const elementRight = elementLeft + rect.width
const isIntersecting = !(
elementRight < left ||
elementLeft > right ||
elementBottom < top ||
elementTop > bottom
)
if (isIntersecting) {
handleSelectMessage(messageId, true)
element.classList.add('selection-highlight')
setTimeout(() => element.classList.remove('selection-highlight'), 300)
}
} catch (error) {
console.error('Error calculating element intersection:', error)
}
})
}
setIsDragging(false)
document.body.classList.remove('no-select')
}
const container = scrollContainerRef.current!
if (container) {
container.addEventListener('mousedown', handleMouseDown)
window.addEventListener('mousemove', handleMouseMove)
window.addEventListener('mouseup', handleMouseUp)
}
return () => {
if (container) {
container.removeEventListener('mousedown', handleMouseDown)
window.removeEventListener('mousemove', handleMouseMove)
window.removeEventListener('mouseup', handleMouseUp)
document.body.classList.remove('no-select')
}
}
}, [isMultiSelectMode, isDragging, dragStart, dragCurrent, handleSelectMessage, scrollContainerRef])
const registerMessageElement = useCallback((id: string, element: HTMLElement | null) => {
if (element) {
messageElements.current.set(id, element)
@ -415,16 +313,13 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic, o
</NarrowLayout>
{messageNavigation === 'anchor' && <MessageAnchorLine messages={displayMessages} />}
{messageNavigation === 'buttons' && <ChatNavigation containerId="messages" />}
{isDragging && isMultiSelectMode && (
<SelectionBox
style={{
left: Math.min(dragStart.x, dragCurrent.x),
top: Math.min(dragStart.y, dragCurrent.y),
width: Math.abs(dragCurrent.x - dragStart.x),
height: Math.abs(dragCurrent.y - dragStart.y)
}}
/>
)}
{/* TODO: 多选功能实现有问题,需要重新改改 */}
{/* <SelectionBox
isMultiSelectMode={isMultiSelectMode}
scrollContainerRef={scrollContainerRef}
messageElements={messageElements.current}
handleSelectMessage={handleSelectMessage}
/> */}
</Container>
)
}
@ -492,12 +387,4 @@ const Container = styled(Scrollbar)<ContainerProps>`
z-index: 1;
`
const SelectionBox = styled.div`
position: absolute;
border: 1px dashed var(--color-primary);
background-color: rgba(0, 114, 245, 0.1);
pointer-events: none;
z-index: 100;
`
export default Messages

View File

@ -0,0 +1,141 @@
import { useEffect, useState } from 'react'
import styled from 'styled-components'
interface SelectionBoxProps {
isMultiSelectMode: boolean
scrollContainerRef: React.RefObject<HTMLDivElement | null>
messageElements: Map<string, HTMLElement>
handleSelectMessage: (messageId: string, selected: boolean) => void
}
const SelectionBox: React.FC<SelectionBoxProps> = ({
isMultiSelectMode,
scrollContainerRef,
messageElements,
handleSelectMessage
}) => {
const [isDragging, setIsDragging] = useState(false)
const [dragStart, setDragStart] = useState({ x: 0, y: 0 })
const [dragCurrent, setDragCurrent] = useState({ x: 0, y: 0 })
useEffect(() => {
if (!isMultiSelectMode) return
const updateDragPos = (e: MouseEvent) => {
const container = scrollContainerRef.current!
if (!container) return { x: 0, y: 0 }
const rect = container.getBoundingClientRect()
const x = e.clientX - rect.left + container.scrollLeft
const y = e.clientY - rect.top + container.scrollTop
return { x, y }
}
const handleMouseDown = (e: MouseEvent) => {
if ((e.target as HTMLElement).closest('.ant-checkbox-wrapper')) return
if ((e.target as HTMLElement).closest('.MessageFooter')) return
setIsDragging(true)
const pos = updateDragPos(e)
setDragStart(pos)
setDragCurrent(pos)
document.body.classList.add('no-select')
}
const handleMouseMove = (e: MouseEvent) => {
if (!isDragging) return
setDragCurrent(updateDragPos(e))
const container = scrollContainerRef.current!
if (container) {
const { top, bottom } = container.getBoundingClientRect()
const scrollSpeed = 15
if (e.clientY < top + 50) {
container.scrollBy(0, -scrollSpeed)
} else if (e.clientY > bottom - 50) {
container.scrollBy(0, scrollSpeed)
}
}
}
const handleMouseUp = () => {
if (!isDragging) return
const left = Math.min(dragStart.x, dragCurrent.x)
const right = Math.max(dragStart.x, dragCurrent.x)
const top = Math.min(dragStart.y, dragCurrent.y)
const bottom = Math.max(dragStart.y, dragCurrent.y)
const MIN_SELECTION_SIZE = 5
const isValidSelection =
Math.abs(right - left) > MIN_SELECTION_SIZE && Math.abs(bottom - top) > MIN_SELECTION_SIZE
if (isValidSelection) {
messageElements.forEach((element, messageId) => {
try {
const rect = element.getBoundingClientRect()
const container = scrollContainerRef.current!
const elementTop = rect.top - container.getBoundingClientRect().top + container.scrollTop
const elementLeft = rect.left - container.getBoundingClientRect().left + container.scrollLeft
const elementBottom = elementTop + rect.height
const elementRight = elementLeft + rect.width
const isIntersecting = !(
elementRight < left ||
elementLeft > right ||
elementBottom < top ||
elementTop > bottom
)
if (isIntersecting) {
handleSelectMessage(messageId, true)
element.classList.add('selection-highlight')
setTimeout(() => element.classList.remove('selection-highlight'), 300)
}
} catch (error) {
console.error('Error calculating element intersection:', error)
}
})
}
setIsDragging(false)
document.body.classList.remove('no-select')
}
const container = scrollContainerRef.current!
if (container) {
container.addEventListener('mousedown', handleMouseDown)
window.addEventListener('mousemove', handleMouseMove)
window.addEventListener('mouseup', handleMouseUp)
}
return () => {
if (container) {
container.removeEventListener('mousedown', handleMouseDown)
window.removeEventListener('mousemove', handleMouseMove)
window.removeEventListener('mouseup', handleMouseUp)
document.body.classList.remove('no-select')
}
}
}, [isMultiSelectMode, isDragging, dragStart, dragCurrent, handleSelectMessage, scrollContainerRef, messageElements])
if (!isDragging || !isMultiSelectMode) return null
return (
<SelectionBoxContainer
style={{
left: Math.min(dragStart.x, dragCurrent.x),
top: Math.min(dragStart.y, dragCurrent.y),
width: Math.abs(dragCurrent.x - dragStart.x),
height: Math.abs(dragCurrent.y - dragStart.y)
}}
/>
)
}
const SelectionBoxContainer = styled.div`
position: absolute;
border: 1px dashed var(--color-primary);
background-color: rgba(0, 114, 245, 0.1);
pointer-events: none;
z-index: 100;
`
export default SelectionBox