/** * UserMessage — 用户消息卡片 * * 全宽卡片流布局,左侧头像。 * 支持附件可视化:图片缩略图、文件卡片。 */ import { useState, useCallback, useRef, useEffect } from 'react'; import { Box, Typography, Avatar, Stack, TextareaAutosize } from '@mui/material'; import { User, FileText, Image as ImageIcon, X } from 'lucide-react'; import type { ChatMessage, AttachmentInfo } from '@renderer/stores/agent-store'; import { useAgentStore } from '@renderer/stores/agent-store'; import { formatTime, formatFileSize } from '@renderer/lib/formatters'; import { ContextMenu, createContextMenuItems } from '@renderer/components/ContextMenu'; interface UserMessageProps { message: ChatMessage; } export function UserMessage({ message }: UserMessageProps): React.JSX.Element { const [editing, setEditing] = useState(false); const [editContent, setEditContent] = useState(message.content); const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null); const textareaRef = useRef(null); const updateMessage = useAgentStore((s) => s.updateMessage); const handleDoubleClick = useCallback(() => { setEditing(true); setEditContent(message.content); }, [message.content]); const handleEditSave = useCallback(() => { if (editContent.trim() && editContent !== message.content) updateMessage(message.id, { content: editContent.trim() }); setEditing(false); }, [editContent, message.content, message.id, updateMessage]); const handleEditKeyDown = useCallback((e: React.KeyboardEvent) => { if (e.key === 'Escape') { setEditing(false); setEditContent(message.content); } if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) handleEditSave(); }, [handleEditSave, message.content]); useEffect(() => { if (editing) textareaRef.current?.focus(); }, [editing]); const hasAttachments = message.attachments && message.attachments.length > 0; return ( { e.preventDefault(); setContextMenu({ x: e.clientX, y: e.clientY }); }} > {/* 附件预览区 */} {hasAttachments && ( {message.attachments!.map((att) => ( ))} )} {/* 文本内容 */} {editing ? ( setEditContent(e.target.value)} onKeyDown={handleEditKeyDown} onBlur={handleEditSave} minRows={3} style={{ width: '100%', background: 'transparent', border: 'none', outline: 'none', color: 'inherit', fontSize: 13, lineHeight: 1.7, resize: 'none', fontFamily: 'inherit' }} /> ) : message.content ? ( {message.content} ) : null} {formatTime(message.timestamp)} {contextMenu && setContextMenu(null)} />} ); } // ===== 附件预览组件 ===== function AttachmentPreview({ attachment }: { attachment: AttachmentInfo }) { const { type, name, size, preview } = attachment; if (type === 'image' && preview) { return ( {name} ); } // 文本文件 if (type === 'text') { return ( {name} {formatFileSize(size)} ); } // 其他文件 return ( {name} {formatFileSize(size)} ); }