- 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
388 lines
12 KiB
TypeScript
388 lines
12 KiB
TypeScript
/**
|
||
* 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
|
||
|
||
|
||
|
||
|
||
|
||
|