mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-04 20:00:00 +08:00
feat: add multi-select mode wrapper for message component (#8653)
* feat: add multi-select mode wrapper for message component * fix: update * update * update * chore: minor updates * fix: add drag threshold
This commit is contained in:
parent
10b7c70a59
commit
0113447481
@ -2,6 +2,7 @@ import { loggerService } from '@logger'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { useMessageEditing } from '@renderer/context/MessageEditingContext'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { useChatContext } from '@renderer/hooks/useChatContext'
|
||||
import { useMessageOperations } from '@renderer/hooks/useMessageOperations'
|
||||
import { useModel } from '@renderer/hooks/useModel'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
@ -38,6 +39,16 @@ interface Props {
|
||||
|
||||
const logger = loggerService.withContext('MessageItem')
|
||||
|
||||
const WrapperContainer = ({
|
||||
isMultiSelectMode,
|
||||
children
|
||||
}: {
|
||||
isMultiSelectMode: boolean
|
||||
children: React.ReactNode
|
||||
}) => {
|
||||
return isMultiSelectMode ? <label style={{ cursor: 'pointer' }}>{children}</label> : children
|
||||
}
|
||||
|
||||
const MessageItem: FC<Props> = ({
|
||||
message,
|
||||
topic,
|
||||
@ -49,6 +60,7 @@ const MessageItem: FC<Props> = ({
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { assistant, setModel } = useAssistant(message.assistantId)
|
||||
const { isMultiSelectMode } = useChatContext(topic)
|
||||
const model = useModel(getMessageModelId(message), message.model?.provider) || message.model
|
||||
const { messageFont, fontSize, messageStyle } = useSettings()
|
||||
const { editMessageBlocks, resendUserMessageWithEdit, editMessage } = useMessageOperations(topic)
|
||||
@ -122,7 +134,15 @@ const MessageItem: FC<Props> = ({
|
||||
|
||||
if (message.type === 'clear') {
|
||||
return (
|
||||
<NewContextMessage className="clear-context-divider" onClick={() => EventEmitter.emit(EVENT_NAMES.NEW_CONTEXT)}>
|
||||
<NewContextMessage
|
||||
isMultiSelectMode={isMultiSelectMode}
|
||||
className="clear-context-divider"
|
||||
onClick={() => {
|
||||
if (isMultiSelectMode) {
|
||||
return
|
||||
}
|
||||
EventEmitter.emit(EVENT_NAMES.NEW_CONTEXT)
|
||||
}}>
|
||||
<Divider dashed style={{ padding: '0 20px' }} plain>
|
||||
{t('chat.message.new.context')}
|
||||
</Divider>
|
||||
@ -131,56 +151,64 @@ const MessageItem: FC<Props> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<MessageContainer
|
||||
key={message.id}
|
||||
className={classNames({
|
||||
message: true,
|
||||
'message-assistant': isAssistantMessage,
|
||||
'message-user': !isAssistantMessage
|
||||
})}
|
||||
ref={messageContainerRef}>
|
||||
<MessageHeader message={message} assistant={assistant} model={model} key={getModelUniqId(model)} topic={topic} />
|
||||
{isEditing && (
|
||||
<MessageEditor
|
||||
<WrapperContainer isMultiSelectMode={isMultiSelectMode}>
|
||||
<MessageContainer
|
||||
key={message.id}
|
||||
className={classNames({
|
||||
message: true,
|
||||
'message-assistant': isAssistantMessage,
|
||||
'message-user': !isAssistantMessage
|
||||
})}
|
||||
ref={messageContainerRef}>
|
||||
<MessageHeader
|
||||
message={message}
|
||||
topicId={topic.id}
|
||||
onSave={handleEditSave}
|
||||
onResend={handleEditResend}
|
||||
onCancel={handleEditCancel}
|
||||
assistant={assistant}
|
||||
model={model}
|
||||
key={getModelUniqId(model)}
|
||||
topic={topic}
|
||||
/>
|
||||
)}
|
||||
{!isEditing && (
|
||||
<>
|
||||
<MessageContentContainer
|
||||
className="message-content-container"
|
||||
style={{
|
||||
fontFamily: messageFont === 'serif' ? 'var(--font-family-serif)' : 'var(--font-family)',
|
||||
fontSize,
|
||||
overflowY: 'visible'
|
||||
}}>
|
||||
<MessageErrorBoundary>
|
||||
<MessageContent message={message} />
|
||||
</MessageErrorBoundary>
|
||||
</MessageContentContainer>
|
||||
{showMenubar && (
|
||||
<MessageFooter className="MessageFooter" $isLastMessage={isLastMessage} $messageStyle={messageStyle}>
|
||||
<MessageMenubar
|
||||
message={message}
|
||||
assistant={assistant}
|
||||
model={model}
|
||||
index={index}
|
||||
topic={topic}
|
||||
isLastMessage={isLastMessage}
|
||||
isAssistantMessage={isAssistantMessage}
|
||||
isGrouped={isGrouped}
|
||||
messageContainerRef={messageContainerRef as React.RefObject<HTMLDivElement>}
|
||||
setModel={setModel}
|
||||
/>
|
||||
</MessageFooter>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</MessageContainer>
|
||||
{isEditing && (
|
||||
<MessageEditor
|
||||
message={message}
|
||||
topicId={topic.id}
|
||||
onSave={handleEditSave}
|
||||
onResend={handleEditResend}
|
||||
onCancel={handleEditCancel}
|
||||
/>
|
||||
)}
|
||||
{!isEditing && (
|
||||
<>
|
||||
<MessageContentContainer
|
||||
className="message-content-container"
|
||||
style={{
|
||||
fontFamily: messageFont === 'serif' ? 'var(--font-family-serif)' : 'var(--font-family)',
|
||||
fontSize,
|
||||
overflowY: 'visible'
|
||||
}}>
|
||||
<MessageErrorBoundary>
|
||||
<MessageContent message={message} />
|
||||
</MessageErrorBoundary>
|
||||
</MessageContentContainer>
|
||||
{showMenubar && (
|
||||
<MessageFooter className="MessageFooter" $isLastMessage={isLastMessage} $messageStyle={messageStyle}>
|
||||
<MessageMenubar
|
||||
message={message}
|
||||
assistant={assistant}
|
||||
model={model}
|
||||
index={index}
|
||||
topic={topic}
|
||||
isLastMessage={isLastMessage}
|
||||
isAssistantMessage={isAssistantMessage}
|
||||
isGrouped={isGrouped}
|
||||
messageContainerRef={messageContainerRef as React.RefObject<HTMLDivElement>}
|
||||
setModel={setModel}
|
||||
/>
|
||||
</MessageFooter>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</MessageContainer>
|
||||
</WrapperContainer>
|
||||
)
|
||||
}
|
||||
|
||||
@ -232,9 +260,11 @@ const MessageFooter = styled.div<{ $isLastMessage: boolean; $messageStyle: 'plai
|
||||
margin-top: 8px;
|
||||
`
|
||||
|
||||
const NewContextMessage = styled.div`
|
||||
const NewContextMessage = styled.div<{ isMultiSelectMode: boolean }>`
|
||||
cursor: pointer;
|
||||
flex: 1;
|
||||
|
||||
${({ isMultiSelectMode }) => isMultiSelectMode && 'cursor: default;'}
|
||||
`
|
||||
|
||||
export default memo(MessageItem)
|
||||
|
||||
@ -17,9 +17,14 @@ const SelectionBox: React.FC<SelectionBoxProps> = ({
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
const [dragStart, setDragStart] = useState({ x: 0, y: 0 })
|
||||
const [dragCurrent, setDragCurrent] = useState({ x: 0, y: 0 })
|
||||
const [isMouseDown, setIsMouseDown] = useState(false)
|
||||
|
||||
const dragSelectedIds = useRef<Set<string>>(new Set())
|
||||
|
||||
// 拖拽阈值,只有移动距离超过这个值才开始框选
|
||||
// 避免触控板点击触发拖拽
|
||||
const DRAG_THRESHOLD = 5
|
||||
|
||||
useEffect(() => {
|
||||
if (!isMultiSelectMode) return
|
||||
|
||||
@ -39,20 +44,30 @@ const SelectionBox: React.FC<SelectionBoxProps> = ({
|
||||
|
||||
e.preventDefault()
|
||||
|
||||
setIsDragging(true)
|
||||
setIsMouseDown(true)
|
||||
const pos = updateDragPos(e)
|
||||
setDragStart(pos)
|
||||
setDragCurrent(pos)
|
||||
dragSelectedIds.current.clear()
|
||||
document.body.classList.add('no-select')
|
||||
}
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (!isMouseDown) return
|
||||
|
||||
const pos = updateDragPos(e)
|
||||
|
||||
const deltaX = Math.abs(pos.x - dragStart.x)
|
||||
const deltaY = Math.abs(pos.y - dragStart.y)
|
||||
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY)
|
||||
|
||||
if (!isDragging && distance > DRAG_THRESHOLD) {
|
||||
setIsDragging(true)
|
||||
document.body.classList.add('no-select')
|
||||
}
|
||||
|
||||
if (!isDragging) return
|
||||
|
||||
e.preventDefault()
|
||||
|
||||
const pos = updateDragPos(e)
|
||||
setDragCurrent(pos)
|
||||
|
||||
// 计算当前框选矩形
|
||||
@ -69,6 +84,9 @@ const SelectionBox: React.FC<SelectionBoxProps> = ({
|
||||
const checkbox = el.querySelector('input[type="checkbox"]') as HTMLInputElement | null
|
||||
const isAlreadySelected = checkbox?.checked || false
|
||||
|
||||
// 清除上下文这类消息也会被选中,所以需要跳过
|
||||
if (!checkbox) return
|
||||
|
||||
// 如果已经被记录为拖动选中,跳过
|
||||
if (dragSelectedIds.current.has(id)) return
|
||||
|
||||
@ -94,9 +112,11 @@ const SelectionBox: React.FC<SelectionBoxProps> = ({
|
||||
}
|
||||
|
||||
const handleMouseUp = () => {
|
||||
if (!isDragging) return
|
||||
setIsDragging(false)
|
||||
document.body.classList.remove('no-select')
|
||||
setIsMouseDown(false)
|
||||
if (isDragging) {
|
||||
setIsDragging(false)
|
||||
document.body.classList.remove('no-select')
|
||||
}
|
||||
}
|
||||
|
||||
const container = scrollContainerRef.current!
|
||||
@ -110,7 +130,7 @@ const SelectionBox: React.FC<SelectionBoxProps> = ({
|
||||
window.removeEventListener('mouseup', handleMouseUp)
|
||||
document.body.classList.remove('no-select')
|
||||
}
|
||||
}, [isMultiSelectMode, isDragging, dragStart, scrollContainerRef, messageElements, handleSelectMessage])
|
||||
}, [isMultiSelectMode, isDragging, isMouseDown, dragStart, scrollContainerRef, messageElements, handleSelectMessage])
|
||||
|
||||
if (!isDragging || !isMultiSelectMode) return null
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user