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:
AI Clinical Dev Team
2025-10-10 20:52:30 +08:00
parent 8bd2b4fc54
commit 84bf1c86ab
11 changed files with 2939 additions and 119 deletions

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