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

@@ -1,24 +1,36 @@
import { useParams } from 'react-router-dom'
import { useState, useEffect } from 'react'
import { Card, Typography, Input, Button, Space, Select, Upload, Tag, Alert, Divider, Spin } from 'antd'
import {
SendOutlined,
PaperClipOutlined,
RobotOutlined,
FolderOpenOutlined,
SyncOutlined,
} from '@ant-design/icons'
import { Card, Typography, Space, Alert, Spin, message } from 'antd'
import { RobotOutlined } from '@ant-design/icons'
import { agentApi, type AgentConfig } from '../api/agentApi'
import { message } from 'antd'
import conversationApi, { type Conversation, type Message } from '../api/conversationApi'
import MessageList from '../components/chat/MessageList'
import MessageInput from '../components/chat/MessageInput'
import ModelSelector, { type ModelType } from '../components/chat/ModelSelector'
import { useProjectStore } from '../stores/useProjectStore'
const { Title, Paragraph } = Typography
const { TextArea } = Input
const AgentChatPage = () => {
const { agentId } = useParams()
const { currentProject } = useProjectStore()
// 智能体相关状态
const [agent, setAgent] = useState<AgentConfig | null>(null)
const [loading, setLoading] = useState(true)
const [agentLoading, setAgentLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
// 对话相关状态
const [conversation, setConversation] = useState<Conversation | null>(null)
const [messages, setMessages] = useState<Message[]>([])
const [selectedModel, setSelectedModel] = useState<ModelType>('deepseek-v3')
// 消息发送状态
const [sending, setSending] = useState(false)
const [streamingContent, setStreamingContent] = useState('')
// 知识库(预留)
const [knowledgeBases] = useState([])
// 加载智能体配置
useEffect(() => {
@@ -26,7 +38,7 @@ const AgentChatPage = () => {
if (!agentId) return
try {
setLoading(true)
setAgentLoading(true)
const response = await agentApi.getById(agentId)
if (response.success && response.data) {
@@ -39,14 +51,103 @@ const AgentChatPage = () => {
setError('加载智能体配置失败')
message.error('加载智能体配置失败')
} finally {
setLoading(false)
setAgentLoading(false)
}
}
fetchAgent()
}, [agentId])
if (loading) {
// 创建或加载对话
useEffect(() => {
const initConversation = async () => {
if (!agent || !currentProject) return
try {
// 创建新对话
const response = await conversationApi.createConversation({
projectId: currentProject.id,
agentId: agent.id,
title: `${agent.name}的对话`,
})
if (response.data.success && response.data.data) {
setConversation(response.data.data)
setMessages([])
}
} catch (err) {
console.error('Failed to create conversation:', err)
message.error('创建对话失败')
}
}
initConversation()
}, [agent, currentProject])
// 发送消息(流式)
const handleSendMessage = async (content: string, knowledgeBaseIds: string[]) => {
if (!conversation || sending) return
setSending(true)
setStreamingContent('')
// 添加用户消息到列表
const userMessage: Message = {
id: `temp-${Date.now()}`,
conversationId: conversation.id,
role: 'user',
content,
createdAt: new Date().toISOString(),
}
setMessages(prev => [...prev, userMessage])
try {
let fullContent = ''
await conversationApi.sendMessageStream(
{
conversationId: conversation.id,
content,
modelType: selectedModel,
knowledgeBaseIds,
},
// onChunk
(chunk) => {
fullContent += chunk
setStreamingContent(fullContent)
},
// onComplete
() => {
// 流式完成后,添加完整的助手消息
const assistantMessage: Message = {
id: `temp-assistant-${Date.now()}`,
conversationId: conversation.id,
role: 'assistant',
content: fullContent,
model: selectedModel,
createdAt: new Date().toISOString(),
}
setMessages(prev => [...prev, assistantMessage])
setStreamingContent('')
setSending(false)
},
// onError
(error) => {
console.error('Stream error:', error)
message.error('发送消息失败:' + error.message)
setStreamingContent('')
setSending(false)
}
)
} catch (err) {
console.error('Failed to send message:', err)
message.error('发送消息失败')
setStreamingContent('')
setSending(false)
}
}
if (agentLoading) {
return (
<div style={{ textAlign: 'center', padding: '100px 0' }}>
<Spin size="large" tip="加载智能体配置中..." />
@@ -54,6 +155,17 @@ const AgentChatPage = () => {
)
}
if (!currentProject) {
return (
<Alert
message="请先选择项目"
description="请在侧边栏选择一个项目后再开始对话"
type="info"
showIcon
/>
)
}
if (error || !agent) {
return (
<Alert
@@ -80,19 +192,28 @@ const AgentChatPage = () => {
<div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
{/* 标题栏 */}
<Card style={{ marginBottom: 16 }}>
<Space align="center">
<span style={{ fontSize: 32 }}>{agent.icon}</span>
<div>
<Title level={3} style={{ marginBottom: 4 }}>
{agent.name}
</Title>
<Paragraph type="secondary" style={{ marginBottom: 0 }}>
{agent.description}
</Paragraph>
<Paragraph type="secondary" style={{ marginBottom: 0, fontSize: 12 }}>
DeepSeek-V3 | {agent.category}
</Paragraph>
</div>
<Space align="center" style={{ width: '100%', justifyContent: 'space-between' }}>
<Space align="center">
<span style={{ fontSize: 32 }}>{agent.icon}</span>
<div>
<Title level={3} style={{ marginBottom: 4 }}>
{agent.name}
</Title>
<Paragraph type="secondary" style={{ marginBottom: 0 }}>
{agent.description}
</Paragraph>
<Paragraph type="secondary" style={{ marginBottom: 0, fontSize: 12 }}>
{agent.category} | {currentProject?.name}
</Paragraph>
</div>
</Space>
{/* 模型选择器 */}
<ModelSelector
value={selectedModel}
onChange={setSelectedModel}
disabled={sending}
/>
</Space>
</Card>
@@ -111,94 +232,32 @@ const AgentChatPage = () => {
padding: 0,
}}
>
{/* 消息列表区域(占位符) */}
<div
style={{
flex: 1,
padding: 24,
overflowY: 'auto',
background: '#fafafa',
}}
>
<div style={{ textAlign: 'center', padding: '60px 0', color: '#999' }}>
<RobotOutlined style={{ fontSize: 64, marginBottom: 16 }} />
<div></div>
</div>
{/* 消息示例(占位符) */}
<div style={{ maxWidth: 800, margin: '0 auto' }}>
<div style={{ marginBottom: 24 }}>
<Tag color="blue" style={{ marginBottom: 8 }}>
</Tag>
<Card size="small">
<Paragraph style={{ marginBottom: 0 }}>
...
</Paragraph>
</Card>
</div>
<div style={{ marginBottom: 24 }}>
<Tag color="green" style={{ marginBottom: 8 }}>
AI助手
</Tag>
<Card size="small">
<Paragraph style={{ marginBottom: 0 }}>
AI的回复示例...
</Paragraph>
</Card>
{/* 消息列表 */}
{messages.length === 0 && !sending ? (
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', background: '#fafafa' }}>
<div style={{ textAlign: 'center', color: '#999' }}>
<RobotOutlined style={{ fontSize: 64, marginBottom: 16 }} />
<div style={{ fontSize: 16 }}></div>
<div style={{ fontSize: 14, marginTop: 8 }}>
使@知识库功能引用文献
</div>
</div>
</div>
</div>
) : (
<MessageList
messages={messages}
loading={sending}
streamingContent={streamingContent}
/>
)}
<Divider style={{ margin: 0 }} />
{/* 输入区域 */}
<div style={{ padding: 16, background: '#fff' }}>
{/* 工具栏 */}
<Space style={{ marginBottom: 12, width: '100%', justifyContent: 'space-between' }}>
<Space>
<Upload showUploadList={false}>
<Button icon={<PaperClipOutlined />}></Button>
</Upload>
<Button icon={<FolderOpenOutlined />}>
@
</Button>
<Select
defaultValue="deepseek-v3"
style={{ width: 180 }}
options={[
{ label: 'DeepSeek-V3', value: 'deepseek-v3' },
{ label: 'Qwen3-72B', value: 'qwen3-72b' },
{ label: 'Gemini Pro', value: 'gemini-pro' },
]}
/>
</Space>
<Tag color="orange" icon={<SyncOutlined spin />}>
...
</Tag>
</Space>
{/* 输入框 */}
<Space.Compact style={{ width: '100%' }}>
<TextArea
placeholder="输入您的问题... (功能开发中)"
autoSize={{ minRows: 2, maxRows: 6 }}
disabled
/>
<Button
type="primary"
icon={<SendOutlined />}
style={{ height: 'auto' }}
disabled
>
</Button>
</Space.Compact>
</div>
{/* 消息输入 */}
<MessageInput
onSend={handleSendMessage}
loading={sending}
knowledgeBases={knowledgeBases}
placeholder={`${agent.name}提问...Shift+Enter换行Enter发送`}
/>
</Card>
</div>
)