diff --git a/src/renderer/src/pages/home/Chat.tsx b/src/renderer/src/pages/home/Chat.tsx index 966b3c83b..1c82a46b0 100644 --- a/src/renderer/src/pages/home/Chat.tsx +++ b/src/renderer/src/pages/home/Chat.tsx @@ -24,7 +24,7 @@ interface Props { setActiveAssistant: (assistant: Assistant) => void } -const ChatContent: FC = (props) => { +const Chat: FC = (props) => { const { assistant } = useAssistant(props.assistant.id) const { topicPosition, messageStyle, showAssistants } = useSettings() const { showTopics } = useShowTopics() @@ -143,10 +143,6 @@ const ChatContent: FC = (props) => { ) } -const Chat: FC = (props) => { - return -} - const MessagesContainer = styled.div` display: flex; flex-direction: column; diff --git a/src/renderer/src/pages/home/Messages/MessageGroup.tsx b/src/renderer/src/pages/home/Messages/MessageGroup.tsx index e5631bf2a..c18c5f52b 100644 --- a/src/renderer/src/pages/home/Messages/MessageGroup.tsx +++ b/src/renderer/src/pages/home/Messages/MessageGroup.tsx @@ -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( diff --git a/src/renderer/src/pages/home/Messages/Messages.tsx b/src/renderer/src/pages/home/Messages/Messages.tsx index 402fb0b5f..8270c192c 100644 --- a/src/renderer/src/pages/home/Messages/Messages.tsx +++ b/src/renderer/src/pages/home/Messages/Messages.tsx @@ -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 = ({ 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([]) @@ -61,117 +59,17 @@ const Messages: React.FC = ({ 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>(new Map()) const messages = useTopicMessages(topic.id) const { displayCount, clearTopicMessages, deleteMessage, createTopicBranch } = useMessageOperations(topic) const messagesRef = useRef(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 = ({ assistant, topic, setActiveTopic, o {messageNavigation === 'anchor' && } {messageNavigation === 'buttons' && } - {isDragging && isMultiSelectMode && ( - - )} + {/* TODO: 多选功能实现有问题,需要重新改改 */} + {/* */} ) } @@ -492,12 +387,4 @@ const Container = styled(Scrollbar)` 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 diff --git a/src/renderer/src/pages/home/Messages/SelectionBox.tsx b/src/renderer/src/pages/home/Messages/SelectionBox.tsx new file mode 100644 index 000000000..b8ac6206d --- /dev/null +++ b/src/renderer/src/pages/home/Messages/SelectionBox.tsx @@ -0,0 +1,141 @@ +import { useEffect, useState } from 'react' +import styled from 'styled-components' + +interface SelectionBoxProps { + isMultiSelectMode: boolean + scrollContainerRef: React.RefObject + messageElements: Map + handleSelectMessage: (messageId: string, selected: boolean) => void +} + +const SelectionBox: React.FC = ({ + 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 ( + + ) +} + +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