mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-26 03:31:24 +08:00
refactor: improve ContextMenu and Message components layout
- Replaced the div in ContextMenu with a styled component for better styling control. - Enhanced Message component to handle editing state more cleanly, separating the editor from the message display. - Adjusted styling for the MessageEditor and FileBlocksContainer for improved layout and responsiveness.
This commit is contained in:
parent
0e29d2a136
commit
850f1bf181
@ -2,6 +2,7 @@ import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { Dropdown } from 'antd'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface ContextMenuProps {
|
||||
children: React.ReactNode
|
||||
@ -73,7 +74,7 @@ const ContextMenu: React.FC<ContextMenuProps> = ({ children, onContextMenu }) =>
|
||||
]
|
||||
|
||||
return (
|
||||
<div onContextMenu={handleContextMenu}>
|
||||
<ContextContainer onContextMenu={handleContextMenu}>
|
||||
{contextMenuPosition && (
|
||||
<Dropdown
|
||||
overlayStyle={{ position: 'fixed', left: contextMenuPosition.x, top: contextMenuPosition.y, zIndex: 1000 }}
|
||||
@ -84,8 +85,10 @@ const ContextMenu: React.FC<ContextMenuProps> = ({ children, onContextMenu }) =>
|
||||
</Dropdown>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
</ContextContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const ContextContainer = styled.div``
|
||||
|
||||
export default ContextMenu
|
||||
|
||||
@ -4,7 +4,6 @@ import { RootState } from '@renderer/store'
|
||||
import { messageBlocksSelectors } from '@renderer/store/messageBlock'
|
||||
import { selectMessagesForTopic } from '@renderer/store/newMessage'
|
||||
import { Topic } from '@renderer/types'
|
||||
import { Modal } from 'antd'
|
||||
import { createContext, FC, ReactNode, use, useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStore } from 'react-redux'
|
||||
@ -41,8 +40,6 @@ export const ChatProvider: FC<ChatProviderProps> = ({ children, activeTopic }) =
|
||||
const { deleteMessage } = useMessageOperations(activeTopic)
|
||||
const [isMultiSelectMode, setIsMultiSelectMode] = useState(false)
|
||||
const [selectedMessageIds, setSelectedMessageIds] = useState<string[]>([])
|
||||
const [confirmDeleteVisible, setConfirmDeleteVisible] = useState(false)
|
||||
const [messagesToDelete, setMessagesToDelete] = useState<string[]>([])
|
||||
const [messageRefs, setMessageRefs] = useState<Map<string, HTMLElement>>(new Map())
|
||||
|
||||
const store = useStore<RootState>()
|
||||
@ -118,8 +115,22 @@ export const ChatProvider: FC<ChatProviderProps> = ({ children, activeTopic }) =
|
||||
|
||||
switch (actionType) {
|
||||
case 'delete':
|
||||
setMessagesToDelete(messageIds)
|
||||
setConfirmDeleteVisible(true)
|
||||
window.modal.confirm({
|
||||
title: t('message.delete.confirm.title'),
|
||||
content: t('message.delete.confirm.content', { count: messageIds.length }),
|
||||
okButtonProps: { danger: true },
|
||||
centered: true,
|
||||
onOk: async () => {
|
||||
try {
|
||||
await Promise.all(messageIds.map((messageId) => deleteMessage(messageId)))
|
||||
window.message.success(t('message.delete.success'))
|
||||
toggleMultiSelectMode(false)
|
||||
} catch (error) {
|
||||
console.error('Failed to delete messages:', error)
|
||||
window.message.error(t('message.delete.failed'))
|
||||
}
|
||||
}
|
||||
})
|
||||
break
|
||||
case 'save': {
|
||||
const assistantMessages = messages.filter((msg) => messageIds.includes(msg.id))
|
||||
@ -173,25 +184,6 @@ export const ChatProvider: FC<ChatProviderProps> = ({ children, activeTopic }) =
|
||||
}
|
||||
}
|
||||
|
||||
const confirmDelete = async () => {
|
||||
try {
|
||||
await Promise.all(messagesToDelete.map((messageId) => deleteMessage(messageId)))
|
||||
window.message.success(t('message.delete.success'))
|
||||
setMessagesToDelete([])
|
||||
toggleMultiSelectMode(false)
|
||||
} catch (error) {
|
||||
console.error('Failed to delete messages:', error)
|
||||
window.message.error(t('message.delete.failed'))
|
||||
} finally {
|
||||
setConfirmDeleteVisible(false)
|
||||
}
|
||||
}
|
||||
|
||||
const cancelDelete = () => {
|
||||
setConfirmDeleteVisible(false)
|
||||
setMessagesToDelete([])
|
||||
}
|
||||
|
||||
const value = {
|
||||
isMultiSelectMode,
|
||||
selectedMessageIds,
|
||||
@ -204,20 +196,5 @@ export const ChatProvider: FC<ChatProviderProps> = ({ children, activeTopic }) =
|
||||
registerMessageElement
|
||||
}
|
||||
|
||||
return (
|
||||
<ChatContext value={value}>
|
||||
{children}
|
||||
<Modal
|
||||
title={t('message.delete.confirm.title')}
|
||||
open={confirmDeleteVisible}
|
||||
onOk={confirmDelete}
|
||||
onCancel={cancelDelete}
|
||||
okText={t('common.confirm')}
|
||||
cancelText={t('common.cancel')}
|
||||
okButtonProps={{ danger: true }}
|
||||
centered={true}>
|
||||
<p>{t('message.delete.confirm.content', { count: messagesToDelete.length })}</p>
|
||||
</Modal>
|
||||
</ChatContext>
|
||||
)
|
||||
return <ChatContext value={value}>{children}</ChatContext>
|
||||
}
|
||||
|
||||
@ -51,7 +51,7 @@ const MessageItem: FC<Props> = ({
|
||||
const { assistant, setModel } = useAssistant(message.assistantId)
|
||||
const model = useModel(getMessageModelId(message), message.model?.provider) || message.model
|
||||
const { isBubbleStyle } = useMessageStyle()
|
||||
const { showMessageDivider, messageFont, fontSize, narrowMode } = useSettings()
|
||||
const { showMessageDivider, messageFont, fontSize, narrowMode, messageStyle } = useSettings()
|
||||
const { editMessageBlocks, resendUserMessageWithEdit } = useMessageOperations(topic)
|
||||
const messageContainerRef = useRef<HTMLDivElement>(null)
|
||||
const { editingMessageId, stopEditing } = useMessageEditing()
|
||||
@ -134,6 +134,22 @@ const MessageItem: FC<Props> = ({
|
||||
)
|
||||
}
|
||||
|
||||
if (isEditing) {
|
||||
return (
|
||||
<MessageContainer style={{ paddingTop: 15 }}>
|
||||
<MessageHeader message={message} assistant={assistant} model={model} key={getModelUniqId(model)} />
|
||||
<div style={{ paddingLeft: messageStyle === 'plain' ? 46 : undefined }}>
|
||||
<MessageEditor
|
||||
message={message}
|
||||
onSave={handleEditSave}
|
||||
onResend={handleEditResend}
|
||||
onCancel={handleEditCancel}
|
||||
/>
|
||||
</div>
|
||||
</MessageContainer>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<MessageContainer
|
||||
key={message.id}
|
||||
@ -161,18 +177,9 @@ const MessageItem: FC<Props> = ({
|
||||
overflowY: 'visible',
|
||||
maxWidth: narrowMode ? 760 : undefined
|
||||
}}>
|
||||
{isEditing ? (
|
||||
<MessageEditor
|
||||
message={message}
|
||||
onSave={handleEditSave}
|
||||
onResend={handleEditResend}
|
||||
onCancel={handleEditCancel}
|
||||
/>
|
||||
) : (
|
||||
<MessageErrorBoundary>
|
||||
<MessageContent message={message} />
|
||||
</MessageErrorBoundary>
|
||||
)}
|
||||
<MessageErrorBoundary>
|
||||
<MessageContent message={message} />
|
||||
</MessageErrorBoundary>
|
||||
{showMenubar && (
|
||||
<MessageFooter
|
||||
className="MessageFooter"
|
||||
|
||||
@ -172,119 +172,108 @@ const MessageBlockEditor: FC<Props> = ({ message, onSave, onResend, onCancel })
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<EditorContainer onDragOver={(e) => e.preventDefault()} onDrop={handleDrop}>
|
||||
{editedBlocks
|
||||
.filter((block) => block.type === MessageBlockType.MAIN_TEXT)
|
||||
.map((block) => (
|
||||
<Textarea
|
||||
className={classNames('editing-message', isFileDragging && 'file-dragging')}
|
||||
key={block.id}
|
||||
ref={textareaRef}
|
||||
variant="borderless"
|
||||
value={block.content}
|
||||
onChange={(e) => {
|
||||
handleTextChange(block.id, e.target.value)
|
||||
autoResizeTextArea(e)
|
||||
}}
|
||||
autoFocus
|
||||
contextMenu="true"
|
||||
spellCheck={false}
|
||||
onPaste={(e) => onPaste(e.nativeEvent)}
|
||||
onFocus={() => {
|
||||
// 记录当前聚焦的组件
|
||||
PasteService.setLastFocusedComponent('messageEditor')
|
||||
}}
|
||||
style={{
|
||||
fontSize,
|
||||
padding: '0px 15px 8px 15px'
|
||||
}}>
|
||||
<TranslateButton onTranslated={onTranslated} />
|
||||
</Textarea>
|
||||
<EditorContainer onDragOver={(e) => e.preventDefault()} onDrop={handleDrop}>
|
||||
{editedBlocks
|
||||
.filter((block) => block.type === MessageBlockType.MAIN_TEXT)
|
||||
.map((block) => (
|
||||
<Textarea
|
||||
className={classNames('editing-message', isFileDragging && 'file-dragging')}
|
||||
key={block.id}
|
||||
ref={textareaRef}
|
||||
variant="borderless"
|
||||
value={block.content}
|
||||
onChange={(e) => {
|
||||
handleTextChange(block.id, e.target.value)
|
||||
autoResizeTextArea(e)
|
||||
}}
|
||||
autoFocus
|
||||
contextMenu="true"
|
||||
spellCheck={false}
|
||||
onPaste={(e) => onPaste(e.nativeEvent)}
|
||||
onFocus={() => {
|
||||
// 记录当前聚焦的组件
|
||||
PasteService.setLastFocusedComponent('messageEditor')
|
||||
}}
|
||||
style={{
|
||||
fontSize,
|
||||
padding: '0px 15px 8px 15px'
|
||||
}}>
|
||||
<TranslateButton onTranslated={onTranslated} />
|
||||
</Textarea>
|
||||
))}
|
||||
{(editedBlocks.some((block) => block.type === MessageBlockType.FILE || block.type === MessageBlockType.IMAGE) ||
|
||||
files.length > 0) && (
|
||||
<FileBlocksContainer>
|
||||
{editedBlocks
|
||||
.filter((block) => block.type === MessageBlockType.FILE || block.type === MessageBlockType.IMAGE)
|
||||
.map(
|
||||
(block) =>
|
||||
block.file && (
|
||||
<CustomTag
|
||||
key={block.id}
|
||||
icon={getFileIcon(block.file.ext)}
|
||||
color="#37a5aa"
|
||||
closable
|
||||
onClose={() => handleFileRemove(block.id)}>
|
||||
<FileNameRender file={block.file} />
|
||||
</CustomTag>
|
||||
)
|
||||
)}
|
||||
|
||||
{files.map((file) => (
|
||||
<CustomTag
|
||||
key={file.id}
|
||||
icon={getFileIcon(file.ext)}
|
||||
color="#37a5aa"
|
||||
closable
|
||||
onClose={() => setFiles((prevFiles) => prevFiles.filter((f) => f.id !== file.id))}>
|
||||
<FileNameRender file={file} />
|
||||
</CustomTag>
|
||||
))}
|
||||
{(editedBlocks.some((block) => block.type === MessageBlockType.FILE || block.type === MessageBlockType.IMAGE) ||
|
||||
files.length > 0) && (
|
||||
<FileBlocksContainer>
|
||||
{editedBlocks
|
||||
.filter((block) => block.type === MessageBlockType.FILE || block.type === MessageBlockType.IMAGE)
|
||||
.map(
|
||||
(block) =>
|
||||
block.file && (
|
||||
<CustomTag
|
||||
key={block.id}
|
||||
icon={getFileIcon(block.file.ext)}
|
||||
color="#37a5aa"
|
||||
closable
|
||||
onClose={() => handleFileRemove(block.id)}>
|
||||
<FileNameRender file={block.file} />
|
||||
</CustomTag>
|
||||
)
|
||||
)}
|
||||
</FileBlocksContainer>
|
||||
)}
|
||||
|
||||
{files.map((file) => (
|
||||
<CustomTag
|
||||
key={file.id}
|
||||
icon={getFileIcon(file.ext)}
|
||||
color="#37a5aa"
|
||||
closable
|
||||
onClose={() => setFiles((prevFiles) => prevFiles.filter((f) => f.id !== file.id))}>
|
||||
<FileNameRender file={file} />
|
||||
</CustomTag>
|
||||
))}
|
||||
</FileBlocksContainer>
|
||||
)}
|
||||
|
||||
<ActionBar>
|
||||
<ActionBarLeft>
|
||||
<AttachmentButton
|
||||
ref={attachmentButtonRef}
|
||||
model={model}
|
||||
files={files}
|
||||
setFiles={setFiles}
|
||||
ToolbarButton={ToolbarButton}
|
||||
/>
|
||||
</ActionBarLeft>
|
||||
<ActionBarMiddle />
|
||||
<ActionBarRight>
|
||||
<Tooltip title={t('common.cancel')}>
|
||||
<ToolbarButton type="text" onClick={onCancel}>
|
||||
<X size={16} />
|
||||
</ToolbarButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={t('common.save')}>
|
||||
<ToolbarButton type="text" onClick={() => handleClick()}>
|
||||
<Save size={16} />
|
||||
</ToolbarButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={t('chat.resend')}>
|
||||
<ToolbarButton type="text" onClick={() => handleClick(true)}>
|
||||
<Send size={16} />
|
||||
</ToolbarButton>
|
||||
</Tooltip>
|
||||
</ActionBarRight>
|
||||
</ActionBar>
|
||||
</EditorContainer>
|
||||
</>
|
||||
<ActionBar>
|
||||
<ActionBarLeft>
|
||||
<AttachmentButton
|
||||
ref={attachmentButtonRef}
|
||||
model={model}
|
||||
files={files}
|
||||
setFiles={setFiles}
|
||||
ToolbarButton={ToolbarButton}
|
||||
/>
|
||||
</ActionBarLeft>
|
||||
<ActionBarMiddle />
|
||||
<ActionBarRight>
|
||||
<Tooltip title={t('common.cancel')}>
|
||||
<ToolbarButton type="text" onClick={onCancel}>
|
||||
<X size={16} />
|
||||
</ToolbarButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={t('common.save')}>
|
||||
<ToolbarButton type="text" onClick={() => handleClick()}>
|
||||
<Save size={16} />
|
||||
</ToolbarButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={t('chat.resend')}>
|
||||
<ToolbarButton type="text" onClick={() => handleClick(true)}>
|
||||
<Send size={16} />
|
||||
</ToolbarButton>
|
||||
</Tooltip>
|
||||
</ActionBarRight>
|
||||
</ActionBar>
|
||||
</EditorContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const FileBlocksContainer = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
padding: 0 15px;
|
||||
margin: 8px 0;
|
||||
background: transparent;
|
||||
border-radius: 4px;
|
||||
`
|
||||
|
||||
const EditorContainer = styled.div`
|
||||
padding: 8px 0;
|
||||
border: 1px solid var(--color-border);
|
||||
transition: all 0.2s ease;
|
||||
border-radius: 15px;
|
||||
margin-top: 0;
|
||||
margin-top: 5px;
|
||||
background-color: var(--color-background-opacity);
|
||||
width: 100%;
|
||||
|
||||
&.file-dragging {
|
||||
border: 2px dashed #2ecc71;
|
||||
@ -304,6 +293,16 @@ const EditorContainer = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
const FileBlocksContainer = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
padding: 0 15px;
|
||||
margin: 8px 0;
|
||||
background: transparent;
|
||||
border-radius: 4px;
|
||||
`
|
||||
|
||||
const Textarea = styled(TextArea)`
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user