Files
AIclinicalresearch/frontend/src/components/chat/MessageInput.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

180 lines
5.0 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, { 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;