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
This commit is contained in:
387
frontend/src/pages/ChatPage.new.tsx
Normal file
387
frontend/src/pages/ChatPage.new.tsx
Normal file
@@ -0,0 +1,387 @@
|
||||
/**
|
||||
* 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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user