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
180 lines
5.0 KiB
TypeScript
180 lines
5.0 KiB
TypeScript
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;
|
||
|