feat(pkb): implement complete batch processing workflow and frontend optimization

- Frontend V3 architecture migration to modules/pkb
- Implement three work modes: full-text reading, deep reading, batch processing
- Complete batch processing: template selection, progress display, result export (CSV)
- Integrate Ant Design X Chat component with streaming support
- Add document upload modal with drag-and-drop support
- Optimize UI: multi-line table display, citation formatting, auto-scroll
- Fix 10+ technical issues: API mapping, state sync, form clearing
- Update documentation: development records and module status

Performance: 3 docs batch processing ~17-28s
Status: PKB module now production-ready (90% complete)
This commit is contained in:
2026-01-07 18:23:43 +08:00
parent e59676342a
commit 06028c6952
195 changed files with 1405 additions and 272 deletions

View File

@@ -1,10 +1,10 @@
/**
* 逐篇精读模式组件 - ChatGPT风格全屏聊天
* 修复:参考文献格式、文档切换对话独立
*/
import React from 'react';
import { Empty } from 'antd';
import { FileText } from 'lucide-react';
import React, { useMemo } from 'react';
import { FileText, BookOpen, ExternalLink } from 'lucide-react';
import { ChatContainer } from '@/shared/components/Chat';
import type { KnowledgeBase, Document } from '../../api/knowledgeBaseApi';
@@ -14,10 +14,92 @@ interface DeepReadModeProps {
selectedDocuments: Document[];
}
// 消息渲染参数类型
interface MessageRenderParams {
id: string | number;
message: {
id: string | number;
role: string;
content: string;
status?: string;
[key: string]: unknown;
};
status: string;
}
// 自定义消息渲染器 - 解析并格式化参考文献
const renderMessageContent = (params: MessageRenderParams) => {
// 从params中提取消息内容
const textContent = params?.message?.content;
// 空内容处理
if (!textContent || typeof textContent !== 'string' || textContent.trim() === '') {
return <div className="text-slate-400 italic">...</div>;
}
// 处理参考文献格式
let formattedContent = textContent;
// 1. 移除HTML标签转换为可读格式
formattedContent = formattedContent.replace(
/<span[^>]*id="citation-detail-(\d+)"[^>]*>\[(\d+)\]<\/span>\s*\*?\*?([^*\n]+)\*?\*?/g,
(_, _num, num2, title) => `\n📄 [${num2}] ${title.trim()}`
);
// 2. 处理其他HTML span标签
formattedContent = formattedContent.replace(/<span[^>]*>[^<]*<\/span>/g, '');
// 3. 处理Markdown加粗中的下划线文件名
formattedContent = formattedContent.replace(
/\*\*([^*]+\.pdf)\*\*/gi,
'📄 **$1**'
);
return (
<div className="prose prose-sm max-w-none">
{formattedContent.split('\n').map((line, idx) => {
// 检测是否是参考文献行
if (line.startsWith('📄')) {
return (
<div key={idx} className="flex items-start my-2 p-2 bg-blue-50 rounded-lg border border-blue-100">
<BookOpen className="w-4 h-4 text-blue-500 mr-2 mt-0.5 flex-shrink-0" />
<span className="text-sm text-slate-700">{line.replace('📄 ', '')}</span>
</div>
);
}
// 检测是否是"参考文献"标题
if (line.includes('**参考文献**') || line.includes('📚 **参考文献**')) {
return (
<div key={idx} className="font-semibold text-slate-800 mt-4 mb-2 flex items-center">
<BookOpen className="w-4 h-4 mr-2 text-blue-600" />
</div>
);
}
// 普通文本
return line ? <p key={idx} className="mb-2 text-slate-700 leading-relaxed">{line}</p> : null;
})}
</div>
);
};
export const DeepReadMode: React.FC<DeepReadModeProps> = ({
kbId,
selectedDocuments
}) => {
// 使用useMemo确保文档切换时生成新的key
const conversationKey = useMemo(() => {
if (!selectedDocuments || selectedDocuments.length === 0) return '';
return `kb-deepread-${kbId}-${selectedDocuments[0].id}-${Date.now()}`;
}, [kbId, selectedDocuments]);
const selectedDocIds = useMemo(() =>
selectedDocuments.map(d => d.id),
[selectedDocuments]
);
if (!selectedDocuments || selectedDocuments.length === 0) {
return (
<div className="h-full flex items-center justify-center bg-slate-50">
@@ -35,15 +117,25 @@ export const DeepReadMode: React.FC<DeepReadModeProps> = ({
}
const selectedDoc = selectedDocuments[0];
const selectedDocIds = selectedDocuments.map(d => d.id);
return (
<div className="h-full flex flex-col bg-white">
{/* Chat组件 - 全屏展开 */}
<div className="flex-1 overflow-hidden">
{/* 当前文档提示 */}
<div className="flex-shrink-0 px-4 py-2 bg-purple-50 border-b border-purple-100">
<div className="flex items-center text-sm">
<FileText className="w-4 h-4 text-purple-500 mr-2" />
<span className="text-purple-700"></span>
<span className="font-medium text-purple-900 ml-1 truncate max-w-md" title={selectedDoc.filename}>
{selectedDoc.filename}
</span>
</div>
</div>
{/* Chat组件 - 使用key强制重新渲染 */}
<div className="flex-1 overflow-hidden" key={conversationKey}>
<ChatContainer
conversationType="pkb"
conversationKey={`kb-deepread-${kbId}-${selectedDoc.id}`}
conversationKey={conversationKey}
defaultMessages={[{
id: 'welcome',
role: 'assistant',
@@ -54,6 +146,9 @@ export const DeepReadMode: React.FC<DeepReadModeProps> = ({
providerConfig={{
apiEndpoint: '/api/v1/chat/stream',
requestFn: async (message: string) => {
// 🔑 关键:传递 fullTextDocumentIds 而不是 documentIds
// fullTextDocumentIds 会触发全文加载模式AI可以看到完整文献
// documentIds 只是过滤RAG检索结果AI只能看到片段
const response = await fetch('/api/v1/chat/stream', {
method: 'POST',
headers: {
@@ -64,7 +159,7 @@ export const DeepReadMode: React.FC<DeepReadModeProps> = ({
content: message,
modelType: 'qwen-long',
knowledgeBaseIds: [kbId],
documentIds: selectedDocIds, // 🌟 关键参数:限定文档范围
fullTextDocumentIds: selectedDocIds, // ✅ 改用全文模式
}),
});
@@ -72,7 +167,6 @@ export const DeepReadMode: React.FC<DeepReadModeProps> = ({
throw new Error(`API请求失败: ${response.status}`);
}
// 处理流式响应
const reader = response.body?.getReader();
const decoder = new TextDecoder();
let fullContent = '';
@@ -109,6 +203,7 @@ export const DeepReadMode: React.FC<DeepReadModeProps> = ({
};
},
}}
customMessageRenderer={renderMessageContent}
/>
</div>
</div>