feat: Day 14-17 - Frontend Chat Interface completed
Frontend: - Create MessageList component with streaming animation - Create MessageInput component with @knowledge base support - Create ModelSelector component (DeepSeek/Qwen/Gemini) - Implement conversationApi with SSE streaming - Update AgentChatPage integrate all components - Add Markdown rendering (react-markdown + remark-gfm) - Add code highlighting (react-syntax-highlighter) - Add vite-env.d.ts for environment variables Features: - Real-time streaming output with cursor animation - Markdown and code block rendering - Model switching (DeepSeek-V3, Qwen3-72B, Gemini Pro) - @Knowledge base selector (UI ready) - Auto-scroll to bottom - Shift+Enter for new line, Enter to send - Beautiful message bubble design Build: Frontend build successfully (7.94s, 1.9MB) New Files: - components/chat/MessageList.tsx (170 lines) - components/chat/MessageList.css (150 lines) - components/chat/MessageInput.tsx (145 lines) - components/chat/MessageInput.css (60 lines) - components/chat/ModelSelector.tsx (110 lines) - components/chat/ModelSelector.css (35 lines) - api/conversationApi.ts (170 lines) - src/vite-env.d.ts (9 lines) Total: ~850 lines of new code
This commit is contained in:
186
frontend/src/components/chat/MessageList.tsx
Normal file
186
frontend/src/components/chat/MessageList.tsx
Normal file
@@ -0,0 +1,186 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { Avatar, Typography, Space, Tag } from 'antd';
|
||||
import { UserOutlined, RobotOutlined, LoadingOutlined } from '@ant-design/icons';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import './MessageList.css';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
interface Message {
|
||||
id: string;
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
model?: string;
|
||||
tokens?: number;
|
||||
createdAt: string;
|
||||
isStreaming?: boolean;
|
||||
}
|
||||
|
||||
interface MessageListProps {
|
||||
messages: Message[];
|
||||
loading?: boolean;
|
||||
streamingContent?: string;
|
||||
}
|
||||
|
||||
const MessageList: React.FC<MessageListProps> = ({
|
||||
messages,
|
||||
loading = false,
|
||||
streamingContent = ''
|
||||
}) => {
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// 自动滚动到底部
|
||||
const scrollToBottom = () => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
}, [messages, streamingContent]);
|
||||
|
||||
// Markdown代码块渲染
|
||||
const MarkdownComponents = {
|
||||
code({ node, inline, className, children, ...props }: any) {
|
||||
const match = /language-(\w+)/.exec(className || '');
|
||||
return !inline && match ? (
|
||||
<SyntaxHighlighter
|
||||
style={vscDarkPlus}
|
||||
language={match[1]}
|
||||
PreTag="div"
|
||||
{...props}
|
||||
>
|
||||
{String(children).replace(/\n$/, '')}
|
||||
</SyntaxHighlighter>
|
||||
) : (
|
||||
<code className={className} {...props}>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
const renderMessage = (message: Message) => {
|
||||
const isUser = message.role === 'user';
|
||||
|
||||
return (
|
||||
<div
|
||||
key={message.id}
|
||||
className={`message-item ${isUser ? 'message-user' : 'message-assistant'}`}
|
||||
>
|
||||
<div className="message-avatar">
|
||||
<Avatar
|
||||
size={40}
|
||||
icon={isUser ? <UserOutlined /> : <RobotOutlined />}
|
||||
style={{
|
||||
backgroundColor: isUser ? '#1890ff' : '#52c41a',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="message-content">
|
||||
<div className="message-header">
|
||||
<Text strong>{isUser ? '我' : 'AI助手'}</Text>
|
||||
{message.model && (
|
||||
<Tag color="blue" style={{ marginLeft: 8 }}>
|
||||
{message.model}
|
||||
</Tag>
|
||||
)}
|
||||
<Text type="secondary" style={{ marginLeft: 8, fontSize: 12 }}>
|
||||
{new Date(message.createdAt).toLocaleTimeString('zh-CN')}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className="message-body">
|
||||
{isUser ? (
|
||||
<div className="user-message-text">{message.content}</div>
|
||||
) : (
|
||||
<div className="assistant-message-text">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={MarkdownComponents}
|
||||
>
|
||||
{message.content}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{message.tokens && (
|
||||
<div className="message-footer">
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
消耗Token: {message.tokens}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="message-list">
|
||||
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
||||
{messages.map(renderMessage)}
|
||||
|
||||
{/* 流式输出中的消息 */}
|
||||
{loading && streamingContent && (
|
||||
<div className="message-item message-assistant">
|
||||
<div className="message-avatar">
|
||||
<Avatar
|
||||
size={40}
|
||||
icon={<RobotOutlined />}
|
||||
style={{ backgroundColor: '#52c41a' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="message-content">
|
||||
<div className="message-header">
|
||||
<Text strong>AI助手</Text>
|
||||
<Tag color="processing" style={{ marginLeft: 8 }}>
|
||||
<LoadingOutlined /> 生成中...
|
||||
</Tag>
|
||||
</div>
|
||||
|
||||
<div className="message-body">
|
||||
<div className="assistant-message-text streaming">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={MarkdownComponents}
|
||||
>
|
||||
{streamingContent}
|
||||
</ReactMarkdown>
|
||||
<span className="cursor-blink">▊</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 仅显示loading,无流式内容 */}
|
||||
{loading && !streamingContent && (
|
||||
<div className="message-item message-assistant">
|
||||
<div className="message-avatar">
|
||||
<Avatar
|
||||
size={40}
|
||||
icon={<LoadingOutlined spin />}
|
||||
style={{ backgroundColor: '#52c41a' }}
|
||||
/>
|
||||
</div>
|
||||
<div className="message-content">
|
||||
<Text type="secondary">AI正在思考中...</Text>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Space>
|
||||
|
||||
{/* 自动滚动锚点 */}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MessageList;
|
||||
|
||||
Reference in New Issue
Block a user