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:
179
frontend/src/components/chat/MessageInput.tsx
Normal file
179
frontend/src/components/chat/MessageInput.tsx
Normal 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;
|
||||
|
||||
Reference in New Issue
Block a user