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:
2025-11-16 15:43:39 +08:00
parent 11325f88a7
commit 0fe6821a89
52 changed files with 7324 additions and 109 deletions

View 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