// dashboard/src/features/reader/AIAssistantPanel.tsx import { useState, useRef, useEffect } from 'react'; import ReactMarkdown from 'react-markdown'; import remarkMath from 'remark-math'; import remarkGfm from 'remark-gfm'; import rehypeRaw from 'rehype-raw'; import rehypeKatex from 'rehype-katex'; import rehypeSanitize, { defaultSchema } from 'rehype-sanitize'; import 'katex/dist/katex.min.css'; import { Send, Loader, Sparkles, X, BookOpen, AlertCircle, Compass, Brain, Settings, ChevronDown, ChevronUp, AlertTriangle, Square } from 'lucide-react'; import axios from 'axios'; interface RetrievalSource { bibcode: string; paragraph_index: number; content: string; distance: number; } interface AgentStep { step: number; type: 'thought' | 'tool_call' | 'tool_result' | 'error'; label: string; detail?: string; isError?: boolean; isFinished?: boolean; } interface Message { sender: 'user' | 'ai'; text: string; sources?: RetrievalSource[]; imageUrl?: string; steps?: AgentStep[]; } interface AIAssistantPanelProps { bibcode: string; onClose: () => void; onJumpToSource: (bibcode: string, paragraphIndex: number) => void; pendingFigure?: { path: string; url: string } | null; onClearPendingFigure?: () => void; } const safeSchema = { ...defaultSchema, attributes: { ...defaultSchema.attributes, '*': (defaultSchema.attributes?.['*'] || []).concat(['className', 'style', 'mathvariant', 'display']), }, tagNames: (defaultSchema.tagNames || []).concat([ 'math', 'mrow', 'mi', 'mo', 'mn', 'msup', 'msub', 'msubsup', 'mfrac', 'mover', 'munder', 'munderover', 'mspace', 'mtext', 'annotation' ]), }; const SUGGESTED_QUESTIONS = [ "对比我馆藏的文献中,针对热亚矮星双星在共同包层抛射过程中恒星风流失速率的各种主流观点差异。", "有哪些文献提及了脉动白矮星的非径向振动模?", "简述目前文献中关于 Gaia DR3 视差零点改正的处理方法。", "文献库中关于双星合并前奏(Precursor)观测特征 of 论述有哪些?" ]; function getToolDisplayName(name: string): string { switch (name) { case 'search_papers': return '检索 ADS/arXiv'; case 'get_paper_metadata': return '获取文献详细元数据'; case 'download_paper': return '下载文献全文资源'; case 'parse_paper': return '结构化解析文献内容'; case 'get_paper_content': return '获取文献全文内容'; case 'rag_search': return '语义库检索 (RAG)'; case 'query_target': return '查询天体物理参数'; default: return name; } } export function AIAssistantPanel({ bibcode, onClose, onJumpToSource, pendingFigure, onClearPendingFigure }: AIAssistantPanelProps) { const [messages, setMessages] = useState([]); const [input, setInput] = useState(''); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [sessionId, setSessionId] = useState(null); // 步骤展开状态 const [stepsExpanded, setStepsExpanded] = useState>({}); const chatEndRef = useRef(null); const scrollContainerRef = useRef(null); const [shouldAutoScroll, setShouldAutoScroll] = useState(true); const scrollToBottom = () => { chatEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }; useEffect(() => { if (shouldAutoScroll) { scrollToBottom(); } }, [messages, loading, shouldAutoScroll]); const handleScroll = () => { if (!scrollContainerRef.current) return; const { scrollTop, scrollHeight, clientHeight } = scrollContainerRef.current; // 距离底部 50 像素以内视为贴紧底部 const isAtBottom = scrollHeight - scrollTop - clientHeight < 50; setShouldAutoScroll(isAtBottom); }; // 重置会话(当 bibcode 切换时) useEffect(() => { setMessages([]); setSessionId(null); setError(null); setStepsExpanded({}); }, [bibcode]); // 手动停止智能体执行 const handleStop = async () => { if (!sessionId) return; try { await axios.post(`/api/chat/sessions/${sessionId}/stop`); } catch (e) { console.error('停止智能体执行失败:', e); } }; const handleSend = async (questionText: string) => { if (!questionText.trim() || loading) return; setError(null); const userMsg: Message = { sender: 'user', text: questionText, imageUrl: pendingFigure?.url }; setMessages(prev => [...prev, userMsg]); setInput(''); setLoading(true); setShouldAutoScroll(true); const isFigureQuery = !!pendingFigure; const currentFigurePath = pendingFigure?.path; if (onClearPendingFigure) { onClearPendingFigure(); } // AI 初始消息占位符 const aiMsgPlaceholder: Message = { sender: 'ai', text: '', steps: [], sources: [] }; setMessages(prev => [...prev, aiMsgPlaceholder]); try { if (isFigureQuery && currentFigurePath) { // 图表多模态分析依然调用特定接口 const res = await axios.post<{ answer: string }>('/api/chat/figure', { bibcode, image_path: currentFigurePath, question: questionText }); setMessages(prev => { const next = [...prev]; const last = next[next.length - 1]; if (last && last.sender === 'ai') { last.text = res.data.answer; } return next; }); setLoading(false); } else { // 调用智能体流式对话接口(带文献上下文提示,确保 Agent 优先分析当前阅读的文献) const contextPrefix = `[当前正在阅读文献: ${bibcode}] `; const requestQuery = questionText.includes(bibcode) ? questionText : `${contextPrefix}${questionText}`; try { const response = await fetch('/api/chat/agent', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ question: requestQuery, session_id: sessionId, }), }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const reader = response.body?.getReader(); if (!reader) { throw new Error('ReadableStream not supported'); } const decoder = new TextDecoder('utf-8'); let buffer = ''; while (true) { const { value, done } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop() || ''; for (const line of lines) { const trimmed = line.trim(); if (!trimmed) continue; if (trimmed.startsWith('data:')) { const dataStr = trimmed.slice(5).trim(); if (!dataStr) continue; try { const event = JSON.parse(dataStr); switch (event.type) { case 'session': if (!sessionId) { setSessionId(event.session_id); } break; case 'thought': setMessages(prev => { const next = [...prev]; const last = next[next.length - 1]; if (last && last.sender === 'ai') { if (!last.steps) last.steps = []; let stepObj = last.steps.find(s => s.step === event.step && s.type === 'thought'); if (!stepObj) { stepObj = { step: event.step, type: 'thought', label: '思考推理中' }; last.steps.push(stepObj); } stepObj.detail = event.content; } return next; }); break; case 'tool_call': setMessages(prev => { const next = [...prev]; const last = next[next.length - 1]; if (last && last.sender === 'ai') { if (!last.steps) last.steps = []; let stepObj = last.steps.find(s => s.step === event.step && s.type === 'tool_call'); if (!stepObj) { stepObj = { step: event.step, type: 'tool_call', label: `调用: ${getToolDisplayName(event.name)}`, isFinished: false }; last.steps.push(stepObj); } stepObj.detail = typeof event.arguments === 'string' ? event.arguments : JSON.stringify(event.arguments); } return next; }); break; case 'tool_result': setMessages(prev => { const next = [...prev]; const last = next[next.length - 1]; if (last && last.sender === 'ai') { if (!last.steps) last.steps = []; let stepObj = last.steps.find(s => s.step === event.step && s.type === 'tool_call'); if (stepObj) { stepObj.label = `调用: ${getToolDisplayName(event.name)} (已获取观测数据)`; stepObj.isError = event.is_error; stepObj.isFinished = true; } // 提取 RAG 检索到的参考来源文献,并在 sources 中进行去重和追加 if (event.name === 'rag_search' && event.metadata && event.metadata.sources) { const newSources: RetrievalSource[] = event.metadata.sources.map((s: any) => ({ bibcode: s.bibcode, paragraph_index: s.paragraph_index, content: s.preview || '', distance: s.distance, })); const existing = last.sources || []; const combined = [...existing]; for (const src of newSources) { if (!combined.some(c => c.bibcode === src.bibcode && c.paragraph_index === src.paragraph_index)) { combined.push(src); } } last.sources = combined; } } return next; }); break; case 'text_delta': setMessages(prev => { const next = [...prev]; const last = next[next.length - 1]; if (last && last.sender === 'ai') { last.text += event.content; } return next; }); break; case 'error': setMessages(prev => { const next = [...prev]; const last = next[next.length - 1]; if (last && last.sender === 'ai') { if (!last.steps) last.steps = []; last.steps.push({ step: 99, type: 'error', label: `错误返回`, detail: event.message }); } return next; }); break; } } catch (e) { console.error('解析流式 SSE 失败:', e); } } } } setLoading(false); } catch (streamErr) { console.warn('智能体流式对话失败,正在尝试回退至旧版本 RAG 单次问答...', streamErr); // 降级回退 (Deprecated Fallback) —— 调用原始单次 /api/chat/rag const res = await axios.post<{ answer: string; sources: RetrievalSource[] }>('/api/chat/rag', { question: questionText, top_k: 5 }); setMessages(prev => { const next = [...prev]; const last = next[next.length - 1]; if (last && last.sender === 'ai') { last.text = res.data.answer; last.sources = res.data.sources; } return next; }); setLoading(false); } } } catch (err: any) { console.error('问答请求失败:', err); setError(err.response?.data?.error || err.message || '网络请求错误,请稍后重试'); // 如果报错,清空多余的消息占位符 setMessages(prev => { const next = [...prev]; if (next.length > 0 && next[next.length - 1].sender === 'ai' && !next[next.length - 1].text) { next.pop(); } return next; }); setLoading(false); } }; const toggleSteps = (msgIdx: number) => { setStepsExpanded(prev => ({ ...prev, [msgIdx]: !prev[msgIdx] })); }; return (
{/* 头部面板 */}
科研智能问答助手
{/* 对话消息区 */}
{messages.length === 0 ? (

探索馆藏文献知识库

基于 ReAct 框架的智能科研体。支持自动读取已解析的 Markdown 文献,在向量库进行 RAG 查询以及天体参数检索。

{/* 推荐提示词 */}
推荐学术提问:
{SUGGESTED_QUESTIONS.map((q, idx) => ( ))}
) : ( messages.map((msg, index) => (
{msg.sender === 'user' ? '我' : '智能科研助手'}
{msg.sender === 'user' ? (
{msg.imageUrl && ( Attached Figure )}

{msg.text}

) : (
{/* Collapsible Steps list */} {msg.steps && msg.steps.length > 0 && (
{stepsExpanded[index] && (
{msg.steps.map((step, sIdx) => (
{step.type === 'thought' ? ( ) : step.type === 'error' ? ( ) : ( )} {step.label} {step.detail && ( {step.detail} )}
))}
)}
)} {/* Final output text */} {msg.text ? (
{msg.text}
) : ( !loading && 正在构建最终结论... )}
)}
{/* 关联来源展示 */} {msg.sender === 'ai' && msg.sources && msg.sources.length > 0 && (
参考来源文献(点击跳转):
{msg.sources.map((src, sIdx) => ( ))}
)}
)) )} {loading && messages.length > 0 && !messages[messages.length - 1].text && (
文献检索及推理结论构建中...
)} {error && (
查询发生错误
{error}
)}
{/* 待提问图片预览 */} {pendingFigure && (
Preview
已选中图表插图 {pendingFigure.path.split('/').pop()}
)} {/* 底栏输入区 */}
{ e.preventDefault(); handleSend(input); }} className="p-3 border-t border-slate-200 bg-white shrink-0" >
setInput(e.target.value)} disabled={loading} placeholder={pendingFigure ? "针对选中图表提问..." : "向 AI 馆藏助手提问..."} className="flex-1 bg-slate-50 border border-slate-200 rounded-xl text-xs text-slate-900 placeholder-slate-400 pl-3 pr-10 py-2.5 focus:outline-none focus:bg-white focus:border-sky-500 focus:ring-1 focus:ring-sky-500/10 leading-relaxed font-semibold transition-all disabled:opacity-60" /> {loading ? ( ) : ( )}
); }