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,179 @@
import React, { useState, useRef, KeyboardEvent } from 'react';
import { Input, Button, Space, Tooltip, Dropdown, Tag } from 'antd';
import {
SendOutlined,
PaperClipOutlined,
FileTextOutlined,
CloseCircleOutlined
} from '@ant-design/icons';
import type { MenuProps } from 'antd';
import './MessageInput.css';
const { TextArea } = Input;
interface KnowledgeBase {
id: string;
name: string;
}
interface MessageInputProps {
onSend: (content: string, knowledgeBaseIds: string[]) => void;
loading?: boolean;
knowledgeBases?: KnowledgeBase[];
placeholder?: string;
}
const MessageInput: React.FC<MessageInputProps> = ({
onSend,
loading = false,
knowledgeBases = [],
placeholder = '输入你的问题...Shift+Enter换行Enter发送',
}) => {
const [content, setContent] = useState('');
const [selectedKnowledgeBases, setSelectedKnowledgeBases] = useState<string[]>([]);
const textAreaRef = useRef<any>(null);
// 处理发送消息
const handleSend = () => {
const trimmedContent = content.trim();
if (!trimmedContent || loading) return;
onSend(trimmedContent, selectedKnowledgeBases);
setContent('');
setSelectedKnowledgeBases([]);
// 重置输入框高度
if (textAreaRef.current) {
textAreaRef.current.resizableTextArea.textArea.style.height = 'auto';
}
};
// 处理键盘事件
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
// Enter发送Shift+Enter换行
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSend();
}
};
// 添加知识库
const handleSelectKnowledgeBase = (kbId: string) => {
if (!selectedKnowledgeBases.includes(kbId)) {
setSelectedKnowledgeBases([...selectedKnowledgeBases, kbId]);
}
};
// 移除知识库
const handleRemoveKnowledgeBase = (kbId: string) => {
setSelectedKnowledgeBases(selectedKnowledgeBases.filter(id => id !== kbId));
};
// 知识库下拉菜单
const knowledgeBaseMenuItems: MenuProps['items'] = knowledgeBases.map(kb => ({
key: kb.id,
label: kb.name,
icon: <FileTextOutlined />,
onClick: () => handleSelectKnowledgeBase(kb.id),
disabled: selectedKnowledgeBases.includes(kb.id),
}));
// 获取选中的知识库名称
const getKnowledgeBaseName = (kbId: string) => {
const kb = knowledgeBases.find(k => k.id === kbId);
return kb ? kb.name : kbId;
};
return (
<div className="message-input-container">
{/* 已选择的知识库标签 */}
{selectedKnowledgeBases.length > 0 && (
<div className="selected-knowledge-bases">
<Space wrap>
{selectedKnowledgeBases.map(kbId => (
<Tag
key={kbId}
closable
color="blue"
onClose={() => handleRemoveKnowledgeBase(kbId)}
closeIcon={<CloseCircleOutlined />}
>
<FileTextOutlined /> {getKnowledgeBaseName(kbId)}
</Tag>
))}
</Space>
</div>
)}
{/* 输入框和工具栏 */}
<div className="message-input-wrapper">
<TextArea
ref={textAreaRef}
value={content}
onChange={(e) => setContent(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={placeholder}
autoSize={{ minRows: 2, maxRows: 8 }}
disabled={loading}
className="message-textarea"
/>
<div className="message-input-toolbar">
<Space>
{/* @知识库按钮 */}
{knowledgeBases.length > 0 && (
<Dropdown
menu={{ items: knowledgeBaseMenuItems }}
placement="topLeft"
disabled={loading}
>
<Tooltip title="@知识库 - 基于知识库内容回答">
<Button
icon={<FileTextOutlined />}
type="text"
disabled={loading}
>
@知识库
</Button>
</Tooltip>
</Dropdown>
)}
{/* 上传附件按钮(预留) */}
<Tooltip title="上传文档(即将上线)">
<Button
icon={<PaperClipOutlined />}
type="text"
disabled
/>
</Tooltip>
</Space>
{/* 发送按钮 */}
<Button
type="primary"
icon={<SendOutlined />}
onClick={handleSend}
loading={loading}
disabled={!content.trim() || loading}
>
</Button>
</div>
</div>
{/* 提示信息 */}
<div className="message-input-hint">
<span>Shift + Enter Enter </span>
{selectedKnowledgeBases.length > 0 && (
<span className="kb-hint">
· {selectedKnowledgeBases.length}
</span>
)}
</div>
</div>
);
};
export default MessageInput;