'use client'; import { useCallback, useEffect, useRef, useState } from 'react'; import type { ChatMessage } from '@/lib/api'; import { isSelectableMessage } from '@/lib/chat-message-utils'; export function useChatMessageSelection(messages: ChatMessage[]) { const [selectionMode, setSelectionMode] = useState(false); const [selectedMessageIds, setSelectedMessageIds] = useState([]); const anchorIndexRef = useRef(null); const exitSelectionMode = useCallback(() => { setSelectionMode(false); setSelectedMessageIds([]); anchorIndexRef.current = null; }, []); useEffect(() => { if (!selectionMode) return; const onKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') { event.preventDefault(); exitSelectionMode(); } }; window.addEventListener('keydown', onKeyDown); return () => window.removeEventListener('keydown', onKeyDown); }, [exitSelectionMode, selectionMode]); const enterSelectionMode = useCallback((messageId?: string) => { if (messageId) { const message = messages.find((item) => item.id === messageId); if (!message || !isSelectableMessage(message)) return; } setSelectionMode(true); setSelectedMessageIds(messageId ? [messageId] : []); anchorIndexRef.current = messageId ? messages.findIndex((item) => item.id === messageId) : null; }, [messages]); const addSelectedMessage = useCallback((messageId: string) => { const message = messages.find((item) => item.id === messageId); if (!message || !isSelectableMessage(message)) return; setSelectionMode(true); setSelectedMessageIds((current) => (current.includes(messageId) ? current : [...current, messageId])); anchorIndexRef.current = messages.findIndex((item) => item.id === messageId); }, [messages]); const toggleSelectedMessage = useCallback((messageId: string) => { setSelectedMessageIds((current) => { const next = current.includes(messageId) ? current.filter((id) => id !== messageId) : [...current, messageId]; return next; }); anchorIndexRef.current = messages.findIndex((item) => item.id === messageId); }, [messages]); const handleSelectionPointer = useCallback( (messageId: string, event: { ctrlKey?: boolean; metaKey?: boolean; shiftKey?: boolean }) => { const index = messages.findIndex((item) => item.id === messageId); if (index < 0) return false; const multiKey = Boolean(event.ctrlKey || event.metaKey); if (multiKey) { if (!selectionMode) { enterSelectionMode(messageId); } else { toggleSelectedMessage(messageId); } return true; } if (event.shiftKey) { if (!selectionMode) { enterSelectionMode(messageId); } const anchor = anchorIndexRef.current ?? index; const start = Math.min(anchor, index); const end = Math.max(anchor, index); const rangeIds = messages .slice(start, end + 1) .filter((item) => isSelectableMessage(item)) .map((item) => item.id); setSelectedMessageIds(rangeIds); return true; } if (selectionMode) { toggleSelectedMessage(messageId); return true; } return false; }, [enterSelectionMode, messages, selectionMode, toggleSelectedMessage] ); return { selectionMode, selectedMessageIds, setSelectedMessageIds, enterSelectionMode, exitSelectionMode, toggleSelectedMessage, addSelectedMessage, handleSelectionPointer }; }