Files
AIclinicalresearch/frontend/src/components/chat/MessageList.tsx
AI Clinical Dev Team 84bf1c86ab 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
2025-10-10 20:52:30 +08:00

187 lines
5.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;