diff --git a/src/assets/images/ai_avatar.png b/src/assets/images/ai_avatar.png new file mode 100644 index 0000000..5c57716 Binary files /dev/null and b/src/assets/images/ai_avatar.png differ diff --git a/src/assets/images/me_avatar.png b/src/assets/images/me_avatar.png new file mode 100644 index 0000000..8802f94 Binary files /dev/null and b/src/assets/images/me_avatar.png differ diff --git a/src/components/chat/ChatMessageList.tsx b/src/components/chat/ChatMessageList.tsx index 157a3d1..a2fffd9 100644 --- a/src/components/chat/ChatMessageList.tsx +++ b/src/components/chat/ChatMessageList.tsx @@ -1,7 +1,10 @@ -import { memo, useEffect, useRef } from 'react'; +import { memo, useEffect, useState, useRef } from 'react'; +import { Check, ChevronRight, Copy, ImageIcon, Paperclip, Sparkles } from 'lucide-react'; import type { ChatMessageItem } from './types'; import { useI18n } from '../../i18n'; import ChatEmptyState from './ChatEmptyState'; +import aiAvatar from '../../assets/images/ai_avatar.png'; +import meAvatar from '../../assets/images/me_avatar.png'; type ChatMessageListProps = { messages: ChatMessageItem[]; @@ -9,6 +12,83 @@ type ChatMessageListProps = { showWelcomeState?: boolean; }; +function cn(...classes: Array) { + return classes.filter(Boolean).join(' '); +} + +function AttachmentPreview({ + messageRole, + attachment, +}: { + messageRole: ChatMessageItem['role']; + attachment: NonNullable[number]; +}) { + const isImage = attachment.mimeType.startsWith('image/'); + const imageSizeClass = messageRole === 'user' ? 'h-24 w-24' : 'h-28 w-28'; + + if (isImage && attachment.preview) { + return ( +
+ {attachment.fileName} +
+ ); + } + + return ( +
+ {isImage ? : } + {attachment.fileName} +
+ ); +} + +function AssistantMeta({ + content, + time, +}: { + content: string; + time: string; +}) { + const [copied, setCopied] = useState(false); + + const handleCopy = async () => { + if (!content.trim() || !navigator?.clipboard?.writeText) { + return; + } + + await navigator.clipboard.writeText(content); + setCopied(true); + window.setTimeout(() => setCopied(false), 1500); + }; + + return ( +
+ {time} + {content.trim() ? ( + + ) : null} +
+ ); +} + function ChatMessageList({ messages, loading, showWelcomeState }: ChatMessageListProps) { const containerRef = useRef(null); const { t } = useI18n(); @@ -18,13 +98,13 @@ function ChatMessageList({ messages, loading, showWelcomeState }: ChatMessageLis const container = containerRef.current; if (!container) return; container.scrollTop = container.scrollHeight; - }, [messages]); + }, [loading, messages]); return ( -
-
+
+
{loading ? ( -
+
{t('conversation.messageList.loading')}
) : null} @@ -32,61 +112,97 @@ function ChatMessageList({ messages, loading, showWelcomeState }: ChatMessageLis {messages.map((message) => (
-
- {message.role === 'assistant' - ? t('conversation.messageList.assistantBadge') - : t('conversation.messageList.userBadge')} -
- -
-
-
{message.name}
-
{message.time}
+ {message.role === 'assistant' ? ( +
+ aiAvatar
-

{message.content}

- {message.attachments && message.attachments.length > 0 ? ( -
- {message.attachments.map((attachment, index) => { - const attachmentKey = attachment.filePath || `${attachment.fileName}-${index}`; - const isImage = attachment.mimeType.startsWith('image/') && Boolean(attachment.preview); + ) : null} - if (isImage && attachment.preview) { - return ( -
- {attachment.fileName} -
- ); - } +
+ {message.role === 'assistant' && message.isStreaming ? ( + <> +
+ {t('conversation.messageList.streaming')} +
+ + + ) : null} - return ( -
- {attachment.fileName} -
- ); - })} + {message.role === 'user' && message.attachments && message.attachments.length > 0 ? ( +
+ {message.attachments.map((attachment, index) => ( + + ))}
) : null} - {message.isStreaming ? ( -
{t('conversation.messageList.streaming')}
+ + {message.content ? ( +
+

+ {message.content} +

+
) : null} + + {message.role === 'assistant' && message.attachments && message.attachments.length > 0 ? ( +
+ {message.attachments.map((attachment, index) => ( + + ))} +
+ ) : null} + + {message.role === 'assistant' ? ( + + ) : ( +
+ {message.time} +
+ )}
+ + {message.role === 'user' ? ( +
+ meAvatar +
+ ) : null}
))}