Files
AIclinicalresearch/frontend/src/pages/AgentChatPage.tsx
2025-10-11 17:56:19 +08:00

315 lines
9.5 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 { useParams } from 'react-router-dom'
import { useState, useEffect } from 'react'
import { Space, Alert, Spin, message } from 'antd'
import { RobotOutlined } from '@ant-design/icons'
import { agentApi, type AgentConfig } from '../api/agentApi'
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'
import { useKnowledgeBaseStore } from '../stores/useKnowledgeBaseStore'
const AgentChatPage = () => {
const { agentId } = useParams()
const { currentProject } = useProjectStore()
const { knowledgeBases, fetchKnowledgeBases } = useKnowledgeBaseStore()
// 智能体相关状态
const [agent, setAgent] = useState<AgentConfig | null>(null)
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('')
// 加载智能体配置
useEffect(() => {
const fetchAgent = async () => {
if (!agentId) return
try {
setAgentLoading(true)
const response = await agentApi.getById(agentId)
if (response.success && response.data) {
setAgent(response.data)
} else {
setError(response.message || '智能体不存在')
}
} catch (err) {
console.error('Failed to load agent:', err)
setError('加载智能体配置失败')
message.error('加载智能体配置失败')
} finally {
setAgentLoading(false)
}
}
fetchAgent()
}, [agentId])
// 加载知识库列表
useEffect(() => {
fetchKnowledgeBases()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
// 创建或加载对话
useEffect(() => {
const initConversation = async () => {
console.log('🔷 [initConversation] 开始', { agent, currentProject })
if (!agent || !currentProject) {
console.warn('⚠️ [initConversation] agent或currentProject为空跳过')
return
}
try {
console.log('📞 [initConversation] 调用API创建对话')
// 创建新对话
const response = await conversationApi.createConversation({
projectId: currentProject.id,
agentId: agent.id,
title: `${agent.name}的对话`,
})
console.log('✅ [initConversation] 对话创建成功', response)
console.log('📦 [initConversation] response:', response)
// response 本身就是 ApiResponse<Conversation>
// response.data 是 Conversation | undefined
if (response.data) {
console.log('💾 [initConversation] 设置conversation为:', response.data)
setConversation(response.data as any)
setMessages([])
} else {
console.error('❌ [initConversation] response.data为空')
message.error('创建对话失败:无数据返回')
}
} catch (err) {
console.error('❌ [initConversation] 创建对话失败:', err)
message.error('创建对话失败')
}
}
initConversation()
}, [agent, currentProject])
// 发送消息(流式)
const handleSendMessage = async (content: string, knowledgeBaseIds: string[]) => {
console.log('🔵 [handleSendMessage] 开始', { content, knowledgeBaseIds, conversation, sending })
if (!conversation) {
console.error('❌ [handleSendMessage] conversation为空')
message.error('对话未初始化,请刷新页面')
return
}
if (sending) {
console.warn('⚠️ [handleSendMessage] 正在发送中,忽略本次请求')
return
}
setSending(true)
setStreamingContent('')
// 添加用户消息到列表
const userMessage: Message = {
id: `temp-${Date.now()}`,
conversationId: conversation.id,
role: 'user',
content,
createdAt: new Date().toISOString(),
}
console.log('📝 [handleSendMessage] 添加用户消息', userMessage)
setMessages(prev => {
const newMessages = [...prev, userMessage]
console.log('📋 [handleSendMessage] 更新后的消息列表', newMessages)
return newMessages
})
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={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100vh'
}}>
<Spin size="large" tip="加载智能体配置中...">
<div style={{ width: 200, height: 100 }} />
</Spin>
</div>
)
}
if (!currentProject) {
return (
<Alert
message="请先选择项目"
description="请在侧边栏选择一个项目后再开始对话"
type="info"
showIcon
/>
)
}
if (error || !agent) {
return (
<Alert
message="智能体不存在"
description={error || "请从首页选择一个智能体"}
type="error"
showIcon
/>
)
}
if (!agent.enabled) {
return (
<Alert
message="该智能体正在开发中"
description="敬请期待后续版本..."
type="warning"
showIcon
/>
)
}
return (
<div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
{/* 顶部工具栏 - 紧凑设计 */}
<div style={{
padding: '12px 24px',
background: '#fff',
borderBottom: '1px solid #e8e8e8',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
flexShrink: 0,
}}>
<Space align="center" size="middle">
<span style={{ fontSize: 24 }}>{agent.icon}</span>
<div>
<div style={{ fontSize: 16, fontWeight: 600, lineHeight: '24px' }}>
{agent.name}
</div>
<div style={{ fontSize: 12, color: '#8c8c8c', lineHeight: '20px' }}>
{agent.category} · {currentProject?.name}
</div>
</div>
</Space>
{/* 模型选择器 */}
<ModelSelector
value={selectedModel}
onChange={setSelectedModel}
disabled={sending}
/>
</div>
{/* 聊天区域 - 去掉Card直接使用div */}
<div
style={{
flex: 1,
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
minHeight: 0,
background: '#fff',
}}
>
{/* 消息列表区域 */}
<div style={{
flex: 1,
display: 'flex',
flexDirection: 'column',
minHeight: 0, // 重要:确保可以滚动
overflow: 'hidden',
}}>
{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>
) : (
<MessageList
messages={messages}
loading={sending}
streamingContent={streamingContent}
/>
)}
</div>
{/* 消息输入 */}
<MessageInput
onSend={handleSendMessage}
loading={sending}
knowledgeBases={knowledgeBases}
placeholder={`${agent.name}提问...Shift+Enter换行Enter发送`}
/>
</div>
</div>
)
}
export default AgentChatPage