Files
AIclinicalresearch/frontend-v2/src/modules/pkb/components/Workspace/DeepReadMode.tsx
HaHafeng 1b53ab9d52 feat(aia): Complete AIA V2.0 with universal streaming capabilities
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%)
2026-01-14 19:15:01 +08:00

215 lines
7.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 逐篇精读模式组件 - 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>
);
};