Major Changes: - Add StreamingService with OpenAI Compatible format - Upgrade Chat component V2 with Ant Design X integration - Implement AIA module with 12 intelligent agents - Update API routes to unified /api/v1 prefix - Update system documentation Backend (~1300 lines): - common/streaming: OpenAI Compatible adapter - modules/aia: 12 agents, conversation service, streaming integration - Update route versions (RVW, PKB to v1) Frontend (~3500 lines): - modules/aia: AgentHub + ChatWorkspace (100% prototype restoration) - shared/Chat: AIStreamChat, ThinkingBlock, useAIStream Hook - Update API endpoints to v1 Documentation: - AIA module status guide - Universal capabilities catalog - System overview updates - All module documentation sync Tested: Stream response verified, authentication working Status: AIA V2.0 core completed (85%)
215 lines
7.7 KiB
TypeScript
215 lines
7.7 KiB
TypeScript
/**
|
||
* 逐篇精读模式组件 - ChatGPT风格全屏聊天
|
||
* 修复:参考文献格式、文档切换对话独立
|
||
*/
|
||
|
||
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';
|
||
import { getAccessToken } from '../../../../framework/auth/api';
|
||
|
||
interface DeepReadModeProps {
|
||
kbId: string;
|
||
kbInfo: KnowledgeBase;
|
||
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">
|
||
<div className="text-center">
|
||
<div className="w-16 h-16 mx-auto bg-purple-50 rounded-full flex items-center justify-center mb-4">
|
||
<FileText className="w-8 h-8 text-purple-400" />
|
||
</div>
|
||
<h3 className="text-lg font-semibold text-slate-700 mb-2">选择文档开始精读</h3>
|
||
<p className="text-sm text-slate-500 max-w-xs">
|
||
请在上方"逐篇精读"下拉框中选择一篇文档进行深度分析
|
||
</p>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const selectedDoc = selectedDocuments[0];
|
||
|
||
return (
|
||
<div className="h-full flex flex-col bg-white">
|
||
{/* 当前文档提示 */}
|
||
<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={conversationKey}
|
||
defaultMessages={[{
|
||
id: 'welcome',
|
||
role: 'assistant',
|
||
content: `您好!我已准备好深度解读文档《${selectedDoc.filename}》。\n\n我可以帮您:\n- 📖 逐段解读文献内容\n- 🎯 提炼核心观点和结论\n- 💡 分析研究方法和局限性\n\n请告诉我您想深入了解哪方面?`,
|
||
status: 'success',
|
||
timestamp: Date.now(),
|
||
}]}
|
||
providerConfig={{
|
||
apiEndpoint: '/api/v1/pkb/chat/stream',
|
||
requestFn: async (message: string) => {
|
||
// 🔑 关键:传递 fullTextDocumentIds 而不是 documentIds
|
||
// fullTextDocumentIds 会触发全文加载模式,AI可以看到完整文献
|
||
// documentIds 只是过滤RAG检索结果,AI只能看到片段
|
||
const token = getAccessToken();
|
||
const response = await fetch('/api/v1/pkb/chat/stream', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Accept': 'text/event-stream',
|
||
...(token ? { 'Authorization': `Bearer ${token}` } : {}),
|
||
},
|
||
body: JSON.stringify({
|
||
content: message,
|
||
modelType: 'qwen-long',
|
||
knowledgeBaseIds: [kbId],
|
||
fullTextDocumentIds: selectedDocIds, // ✅ 改用全文模式
|
||
}),
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`API请求失败: ${response.status}`);
|
||
}
|
||
|
||
const reader = response.body?.getReader();
|
||
const decoder = new TextDecoder();
|
||
let fullContent = '';
|
||
|
||
if (reader) {
|
||
while (true) {
|
||
const { done, value } = await reader.read();
|
||
if (done) break;
|
||
|
||
const chunk = decoder.decode(value);
|
||
const lines = chunk.split('\n');
|
||
|
||
for (const line of lines) {
|
||
if (line.startsWith('data: ')) {
|
||
const data = line.slice(6);
|
||
if (data === '[DONE]') break;
|
||
|
||
try {
|
||
const json = JSON.parse(data);
|
||
if (json.content) {
|
||
fullContent += json.content;
|
||
}
|
||
} catch (e) {
|
||
// 忽略解析错误
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return {
|
||
content: fullContent,
|
||
messageId: Date.now().toString(),
|
||
};
|
||
},
|
||
}}
|
||
customMessageRenderer={renderMessageContent}
|
||
/>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|