mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-19 22:52:08 +08:00
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:
parent
a5738fdae5
commit
fd1cf1331f
@ -24,7 +24,7 @@ interface Props {
|
|||||||
setActiveAssistant: (assistant: Assistant) => void
|
setActiveAssistant: (assistant: Assistant) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const ChatContent: FC<Props> = (props) => {
|
const Chat: FC<Props> = (props) => {
|
||||||
const { assistant } = useAssistant(props.assistant.id)
|
const { assistant } = useAssistant(props.assistant.id)
|
||||||
const { topicPosition, messageStyle, showAssistants } = useSettings()
|
const { topicPosition, messageStyle, showAssistants } = useSettings()
|
||||||
const { showTopics } = useShowTopics()
|
const { showTopics } = useShowTopics()
|
||||||
@ -143,10 +143,6 @@ const ChatContent: FC<Props> = (props) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const Chat: FC<Props> = (props) => {
|
|
||||||
return <ChatContent {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
const MessagesContainer = styled.div`
|
const MessagesContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { MessageEditingProvider } from '@renderer/context/MessageEditingContext'
|
|||||||
import { useChatContext } from '@renderer/hooks/useChatContext'
|
import { useChatContext } from '@renderer/hooks/useChatContext'
|
||||||
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'
|
||||||
@ -115,19 +116,48 @@ const MessageGroup = ({ messages, topic, hidePresetMessages, registerMessageElem
|
|||||||
// 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(() => {
|
||||||
|
// 为每个消息注册一个定位事件监听器
|
||||||
|
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(() => {
|
useEffect(() => {
|
||||||
messages.forEach((message) => {
|
messages.forEach((message) => {
|
||||||
const element = document.getElementById(`message-${message.id}`)
|
const element = document.getElementById(`message-${message.id}`)
|
||||||
if (element) {
|
element && registerMessageElement?.(message.id, element)
|
||||||
registerMessageElement?.(message.id, element)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
return () => messages.forEach((message) => registerMessageElement?.(message.id, null))
|
||||||
return () => {
|
|
||||||
messages.forEach((message) => {
|
|
||||||
registerMessageElement?.(message.id, null)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [messages, registerMessageElement])
|
}, [messages, registerMessageElement])
|
||||||
|
|
||||||
const renderMessage = useCallback(
|
const renderMessage = useCallback(
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import SvgSpinners180Ring from '@renderer/components/Icons/SvgSpinners180Ring'
|
|||||||
import Scrollbar from '@renderer/components/Scrollbar'
|
import Scrollbar from '@renderer/components/Scrollbar'
|
||||||
import { LOAD_MORE_COUNT } from '@renderer/config/constant'
|
import { LOAD_MORE_COUNT } from '@renderer/config/constant'
|
||||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||||
import { useChatContext } from '@renderer/hooks/useChatContext'
|
|
||||||
import { useMessageOperations, useTopicMessages } from '@renderer/hooks/useMessageOperations'
|
import { useMessageOperations, useTopicMessages } from '@renderer/hooks/useMessageOperations'
|
||||||
import useScrollPosition from '@renderer/hooks/useScrollPosition'
|
import useScrollPosition from '@renderer/hooks/useScrollPosition'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
@ -53,7 +52,6 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic, o
|
|||||||
)
|
)
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { showPrompt, showTopics, topicPosition, showAssistants, messageNavigation } = useSettings()
|
const { showPrompt, showTopics, topicPosition, showAssistants, messageNavigation } = useSettings()
|
||||||
const { isMultiSelectMode, handleSelectMessage } = useChatContext(topic)
|
|
||||||
const { updateTopic, addTopic } = useAssistant(assistant.id)
|
const { updateTopic, addTopic } = useAssistant(assistant.id)
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
const [displayMessages, setDisplayMessages] = useState<Message[]>([])
|
const [displayMessages, setDisplayMessages] = useState<Message[]>([])
|
||||||
@ -61,117 +59,17 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic, o
|
|||||||
const [isLoadingMore, setIsLoadingMore] = useState(false)
|
const [isLoadingMore, setIsLoadingMore] = useState(false)
|
||||||
const [isProcessingContext, setIsProcessingContext] = 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 messageElements = useRef<Map<string, HTMLElement>>(new Map())
|
||||||
const messages = useTopicMessages(topic.id)
|
const messages = useTopicMessages(topic.id)
|
||||||
const { displayCount, clearTopicMessages, deleteMessage, createTopicBranch } = useMessageOperations(topic)
|
const { displayCount, clearTopicMessages, deleteMessage, createTopicBranch } = useMessageOperations(topic)
|
||||||
const messagesRef = useRef<Message[]>(messages)
|
const messagesRef = useRef<Message[]>(messages)
|
||||||
|
|
||||||
|
// const { isMultiSelectMode, handleSelectMessage } = useChatContext(topic)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
messagesRef.current = messages
|
messagesRef.current = messages
|
||||||
}, [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) => {
|
const registerMessageElement = useCallback((id: string, element: HTMLElement | null) => {
|
||||||
if (element) {
|
if (element) {
|
||||||
messageElements.current.set(id, element)
|
messageElements.current.set(id, element)
|
||||||
@ -415,16 +313,13 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic, o
|
|||||||
</NarrowLayout>
|
</NarrowLayout>
|
||||||
{messageNavigation === 'anchor' && <MessageAnchorLine messages={displayMessages} />}
|
{messageNavigation === 'anchor' && <MessageAnchorLine messages={displayMessages} />}
|
||||||
{messageNavigation === 'buttons' && <ChatNavigation containerId="messages" />}
|
{messageNavigation === 'buttons' && <ChatNavigation containerId="messages" />}
|
||||||
{isDragging && isMultiSelectMode && (
|
{/* TODO: 多选功能实现有问题,需要重新改改 */}
|
||||||
<SelectionBox
|
{/* <SelectionBox
|
||||||
style={{
|
isMultiSelectMode={isMultiSelectMode}
|
||||||
left: Math.min(dragStart.x, dragCurrent.x),
|
scrollContainerRef={scrollContainerRef}
|
||||||
top: Math.min(dragStart.y, dragCurrent.y),
|
messageElements={messageElements.current}
|
||||||
width: Math.abs(dragCurrent.x - dragStart.x),
|
handleSelectMessage={handleSelectMessage}
|
||||||
height: Math.abs(dragCurrent.y - dragStart.y)
|
/> */}
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -492,12 +387,4 @@ const Container = styled(Scrollbar)<ContainerProps>`
|
|||||||
z-index: 1;
|
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
|
export default Messages
|
||||||
|
|||||||
141
src/renderer/src/pages/home/Messages/SelectionBox.tsx
Normal file
141
src/renderer/src/pages/home/Messages/SelectionBox.tsx
Normal 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
|
||||||
Loading…
Reference in New Issue
Block a user