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
187 lines
5.4 KiB
TypeScript
187 lines
5.4 KiB
TypeScript
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;
|
||
|