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:
@@ -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>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user