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:
kangfenmao 2025-05-21 18:46:50 +08:00
parent 0e29d2a136
commit 850f1bf181
4 changed files with 143 additions and 157 deletions

View File

@ -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

View File

@ -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>
}

View File

@ -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"

View File

@ -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;