Files
AIclinicalresearch/frontend/src/pages/ChatPage.new.tsx
HaHafeng 0fe6821a89 feat(frontend): add batch processing and review features
- Add batch processing API and mode
- Add deep read mode for full-text analysis
- Add document selector and switcher components
- Add review page with editorial and methodology assessment
- Add capacity indicator and usage info modal
- Add custom hooks for batch tasks and chat modes
- Update layouts and routing
- Add TypeScript types for chat features
2025-11-16 15:43:39 +08:00

388 lines
12 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.
/**
* Phase 2: 重构后的智能问答页面
* 支持:通用对话、全文阅读、逐篇精读三种模式
*/
import { useState, useEffect } from 'react'
import { message as antdMessage } from 'antd'
import chatApi, { type GeneralMessage } from '../api/chatApi'
import { documentSelectionApi, documentApi } from '../api/knowledgeBaseApi'
import { useChatMode } from '../hooks/useChatMode'
import { useDeepReadState } from '../hooks/useDeepReadState'
import { ModeSelector } from '../components/chat/ModeSelector'
import { FullTextMode } from '../components/chat/FullTextMode'
import { DeepReadMode } from '../components/chat/DeepReadMode'
import { DocumentSelector } from '../components/chat/DocumentSelector'
import MessageList from '../components/chat/MessageList'
import MessageInput from '../components/chat/MessageInput'
import ModelSelector, { type ModelType } from '../components/chat/ModelSelector'
import { useKnowledgeBaseStore } from '../stores/useKnowledgeBaseStore'
import { ChatMessage } from '../types/chat'
import type { Document } from '../types/index'
const ChatPage = () => {
const { knowledgeBases, fetchKnowledgeBases } = useKnowledgeBaseStore()
// 聊天模式状态
const {
state: modeState,
setBaseMode,
selectKnowledgeBase,
setKnowledgeBaseMode,
loadFullTextMode,
loadDeepReadMode,
} = useChatMode()
// 通用对话状态
const [generalMessages, setGeneralMessages] = useState<GeneralMessage[]>([])
const [selectedModel, setSelectedModel] = useState<ModelType>('deepseek-v3')
const [sending, setSending] = useState(false)
const [streamingContent, setStreamingContent] = useState('')
const [currentConversationId, setCurrentConversationId] = useState<string>()
// 逐篇精读状态
const [deepReadState, setDeepReadState] = useState<ReturnType<typeof useDeepReadState> | null>(null)
const [showDocSelector, setShowDocSelector] = useState(false)
const [allDocuments, setAllDocuments] = useState<Document[]>([])
// 加载知识库列表
useEffect(() => {
fetchKnowledgeBases()
}, [fetchKnowledgeBases])
// 当选择知识库时,加载文档选择结果(全文阅读模式)
useEffect(() => {
if (modeState.selectedKbId && modeState.kbMode === 'full_text') {
loadFullTextData()
}
}, [modeState.selectedKbId, modeState.kbMode])
// 当切换到逐篇精读模式时,显示文献选择器
useEffect(() => {
if (modeState.selectedKbId && modeState.kbMode === 'deep_read' && !modeState.deepReadState) {
loadDocumentsForSelection()
}
}, [modeState.selectedKbId, modeState.kbMode])
// 加载全文阅读模式数据
const loadFullTextData = async () => {
if (!modeState.selectedKbId) return
try {
const result = await documentSelectionApi.getSelection(modeState.selectedKbId)
loadFullTextMode({
loadedDocs: result.selectedDocuments || [],
totalFiles: result.limits.maxFiles,
selectedFiles: result.selection.selectedCount,
totalTokens: result.limits.maxTokens,
usedTokens: result.selection.selectedTokens,
availableTokens: result.selection.availableTokens,
reason: result.selection.reason,
})
} catch (error) {
console.error('Failed to load document selection:', error)
antdMessage.error('加载文档选择失败')
}
}
// 加载文献列表供选择
const loadDocumentsForSelection = async () => {
if (!modeState.selectedKbId) return
try {
const result = await documentSelectionApi.getSelection(modeState.selectedKbId)
setAllDocuments(result.selectedDocuments || [])
setShowDocSelector(true)
} catch (error) {
console.error('Failed to load documents:', error)
antdMessage.error('加载文献列表失败')
}
}
// 确认文献选择(逐篇精读模式)
const handleConfirmDocSelection = (selectedDocs: Document[]) => {
const deepRead = useDeepReadState(selectedDocs)
setDeepReadState(deepRead as any)
loadDeepReadMode({
selectedDocs,
currentDocId: selectedDocs[0].id,
conversationPerDoc: new Map(selectedDocs.map(doc => [doc.id, []])),
totalTokens: selectedDocs.reduce((sum, doc) => sum + (doc.tokensCount || 0), 0),
})
setShowDocSelector(false)
}
// 发送消息(通用对话)
const handleSendGeneralMessage = async (content: string, knowledgeBaseIds: string[]) => {
if (sending) return
setSending(true)
setStreamingContent('')
const userMessage: GeneralMessage = {
id: `temp-${Date.now()}`,
conversationId: currentConversationId || 'temp',
role: 'user',
content,
createdAt: new Date().toISOString(),
}
setGeneralMessages(prev => [...prev, userMessage])
try {
let fullContent = ''
await chatApi.sendMessageStream(
{
content,
modelType: selectedModel,
knowledgeBaseIds,
conversationId: currentConversationId,
},
(chunk) => {
fullContent += chunk
setStreamingContent(fullContent)
},
(conversationId) => {
if (!currentConversationId && conversationId) {
setCurrentConversationId(conversationId)
}
const assistantMessage: GeneralMessage = {
id: `temp-assistant-${Date.now()}`,
conversationId: conversationId || currentConversationId || 'temp',
role: 'assistant',
content: fullContent,
model: selectedModel,
createdAt: new Date().toISOString(),
}
setGeneralMessages(prev => [...prev, assistantMessage])
setStreamingContent('')
setSending(false)
},
(error) => {
console.error('Send failed:', error)
antdMessage.error('发送消息失败:' + error.message)
setStreamingContent('')
setSending(false)
}
)
} catch (err) {
console.error('Send message exception:', err)
antdMessage.error('发送消息失败')
setStreamingContent('')
setSending(false)
}
}
// 发送消息(逐篇精读模式)
const handleSendDeepReadMessage = async (content: string) => {
if (!deepReadState || !deepReadState.currentDoc) return
setSending(true)
// 添加用户消息
const userMsg: ChatMessage = {
id: `temp-${Date.now()}`,
role: 'user',
content,
timestamp: new Date(),
}
deepReadState.addMessage(userMsg)
try {
// TODO: 调用实际的API支持documentIds参数
// 当前先使用通用API
let fullContent = ''
await chatApi.sendMessageStream(
{
content: `[当前文献: ${deepReadState.currentDoc.filename}]\n\n${content}`,
modelType: selectedModel,
knowledgeBaseIds: modeState.selectedKbId ? [modeState.selectedKbId] : [],
},
(chunk) => {
fullContent += chunk
setStreamingContent(fullContent)
},
() => {
const assistantMsg: ChatMessage = {
id: `temp-assistant-${Date.now()}`,
role: 'assistant',
content: fullContent,
timestamp: new Date(),
}
deepReadState.addMessage(assistantMsg)
setStreamingContent('')
setSending(false)
},
(error) => {
console.error('Send failed:', error)
antdMessage.error('发送消息失败')
setStreamingContent('')
setSending(false)
}
)
} catch (error) {
console.error('Send error:', error)
antdMessage.error('发送消息失败')
setSending(false)
}
}
// 渲染内容区域
const renderContent = () => {
const kb = knowledgeBases.find(k => k.id === modeState.selectedKbId)
const kbName = kb?.name || '未选择'
// 通用对话模式
if (modeState.baseMode === 'general') {
return (
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0, overflow: 'hidden' }}>
{generalMessages.length === 0 && !sending ? (
<div style={{
flex: 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: '#fafafa'
}}>
<div style={{ textAlign: 'center', color: '#999' }}>
<div style={{ fontSize: 48, marginBottom: 16 }}>💬</div>
<div style={{ fontSize: 18, fontWeight: 500, marginBottom: 8 }}>
AI自由对话
</div>
<div style={{ fontSize: 14 }}>
使@知识库引用文献
</div>
</div>
</div>
) : (
<MessageList
messages={generalMessages}
loading={sending}
streamingContent={streamingContent}
/>
)}
</div>
<MessageInput
onSend={handleSendGeneralMessage}
loading={sending}
knowledgeBases={knowledgeBases}
placeholder="输入你的问题...Shift+Enter换行Enter发送"
/>
</div>
)
}
// 全文阅读模式
if (modeState.baseMode === 'knowledge_base' && modeState.kbMode === 'full_text') {
if (!modeState.fullTextState) {
return (
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<div>...</div>
</div>
)
}
return (
<FullTextMode
state={modeState.fullTextState}
kbName={kbName}
kbId={modeState.selectedKbId!}
onSendMessage={(content) => handleSendGeneralMessage(content, [modeState.selectedKbId!])}
messages={generalMessages}
loading={sending}
streamingContent={streamingContent}
/>
)
}
// 逐篇精读模式
if (modeState.baseMode === 'knowledge_base' && modeState.kbMode === 'deep_read') {
if (!modeState.deepReadState || !deepReadState) {
return (
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<div>...</div>
</div>
)
}
return (
<DeepReadMode
state={{
selectedDocs: deepReadState.selectedDocs,
currentDoc: deepReadState.currentDoc,
currentConversation: deepReadState.currentConversation,
} as any}
kbName={kbName}
onSwitch={deepReadState.switchDocument}
onSendMessage={handleSendDeepReadMessage}
loading={sending}
streamingContent={streamingContent}
/>
)
}
return null
}
return (
<div style={{ height: '100%', display: 'flex' }}>
{/* 左侧模式选择器 */}
<ModeSelector
state={modeState}
onModeChange={setBaseMode}
onKbSelect={selectKnowledgeBase}
onKbModeChange={setKnowledgeBaseMode}
/>
{/* 主内容区 */}
<div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
{/* 顶部工具栏 */}
<div style={{
padding: '12px 24px',
background: '#fff',
borderBottom: '1px solid #e8e8e8',
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
flexShrink: 0,
}}>
{/* 模型选择器 */}
<ModelSelector
value={selectedModel}
onChange={setSelectedModel}
disabled={sending}
/>
</div>
{/* 内容区域 */}
{renderContent()}
</div>
{/* 文献选择器弹窗 */}
{showDocSelector && (
<DocumentSelector
documents={allDocuments}
maxSelection={5}
onConfirm={handleConfirmDocSelection}
onCancel={() => setShowDocSelector(false)}
/>
)}
</div>
)
}
export default ChatPage