// dashboard/src/App.tsx import { useState, useEffect, useCallback } from 'react'; import axios from 'axios'; import { Loader, Download, BookOpen, GitFork, RefreshCw, AlertTriangle } from 'lucide-react'; import { Sidebar } from './components/layout/Sidebar'; import { SearchPanel, getDoctypeBadge } from './features/search/SearchPanel'; import { LibraryPanel } from './features/library/LibraryPanel'; import { ReaderPanel } from './features/reader/ReaderPanel'; import { CitationPanel } from './features/citation/CitationPanel'; import { SyncPanel } from './features/sync/SyncPanel'; import type { StandardPaper, CitationNetwork, NoteRecord } from './types'; export default function App() { const [activeTab, setActiveTab] = useState<'search' | 'library' | 'reader' | 'citation' | 'sync'>(() => { const saved = localStorage.getItem('astro_active_tab'); return (saved as any) || 'search'; }); useEffect(() => { localStorage.setItem('astro_active_tab', activeTab); }, [activeTab]); // 全局对话框弹窗状态 const [dialog, setDialog] = useState<{ type: 'alert' | 'confirm'; title: string; message: string; onConfirm: () => void; onCancel?: () => void; } | null>(null); const showAlert = useCallback((message: string, title = '系统提示') => { setDialog({ type: 'alert', title, message, onConfirm: () => {}, }); }, []); const showConfirm = useCallback((message: string, onConfirm: () => void, title = '确认操作') => { setDialog({ type: 'confirm', title, message, onConfirm, }); }, []); // 共享数据状态 const [library, setLibrary] = useState([]); const [selectedPaper, setSelectedPaper] = useState(null); const [detailBibcode, setDetailBibcode] = useState(null); const [recentlySelected, setRecentlySelected] = useState(() => { try { const saved = localStorage.getItem('astro_recently_selected'); return saved ? JSON.parse(saved) : []; } catch (e) { console.error('Failed to parse recently selected papers', e); return []; } }); useEffect(() => { localStorage.setItem('astro_recently_selected', JSON.stringify(recentlySelected)); }, [recentlySelected]); const addPaperToRecent = (paper: StandardPaper) => { setRecentlySelected(prev => { const filtered = prev.filter(p => p.bibcode !== paper.bibcode); return [paper, ...filtered].slice(0, 5); }); }; const openCitation = (paper: StandardPaper) => { setSelectedPaper(paper); setActiveTab('citation'); loadCitations(paper.bibcode, true); addPaperToRecent(paper); }; // 检索页状态 const [searchQuery, setSearchQuery] = useState(''); const [searchSource, setSearchSource] = useState<'all' | 'ads' | 'arxiv'>('all'); const [searchResults, setSearchResults] = useState([]); // 衍生状态计算 const detailPaper = library.find(p => p.bibcode === detailBibcode) || searchResults.find(p => p.bibcode === detailBibcode); const [searching, setSearching] = useState(false); const [exportingList, setExportingList] = useState([]); const [bibtexContent, setBibtexContent] = useState(null); const [exporting, setExporting] = useState(false); const [searchRows, setSearchRows] = useState(15); const [searchStart, setSearchStart] = useState(0); const [searchSort, setSearchSort] = useState('relevance'); const [searchCache, setSearchCache] = useState>({}); // 读者页状态 const [englishText, setEnglishText] = useState(''); const [chineseText, setChineseText] = useState(''); const [parsing, setParsing] = useState(false); const [translating, setTranslating] = useState(false); const [vectorizing, setVectorizing] = useState(false); // 引用星系数据状态 const [citationNetwork, setCitationNetwork] = useState(null); const [loadingCitations, setLoadingCitations] = useState(false); const [citationHistory, setCitationHistory] = useState([]); // 多跳历史 const [uncachedBibcode, setUncachedBibcode] = useState(null); // 笔记系统状态 const [notes, setNotes] = useState([]); const [showNotesPanel, setShowNotesPanel] = useState(false); const [newNoteText, setNewNoteText] = useState(''); const [newNoteColor, setNewNoteColor] = useState('yellow'); const [selectedParagraphIdx, setSelectedParagraphIdx] = useState(null); const [selectedText, setSelectedText] = useState(''); // 下载进度状态 const [downloadingBibcodes, setDownloadingBibcodes] = useState>({}); const [uploadingBibcode, setUploadingBibcode] = useState(null); // 1. 初始化时加载本地文献 useEffect(() => { fetchLibrary(); }, []); const fetchLibrary = async () => { try { const res = await axios.get('/api/library'); setLibrary(res.data); const lastReadBibcode = localStorage.getItem('last_read_bibcode'); if (lastReadBibcode) { const lastReadPaper = res.data.find(p => p.bibcode === lastReadBibcode); if (lastReadPaper) { const initialTab = localStorage.getItem('astro_active_tab') || 'search'; openReader(lastReadPaper, initialTab !== 'reader'); } } } catch (e) { console.error('加载本地文献库失败', e); } }; // 2. 检索文献 (统一执行逻辑,包含前端缓存) const executeSearch = async (start: number, rows: number, sort: string) => { if (!searchQuery.trim()) return; // 构造缓存 Key const cacheKey = `${searchQuery.trim()}_${searchSource}_${start}_${rows}_${sort}`; if (searchCache[cacheKey]) { setSearchResults(searchCache[cacheKey]); return; } setSearching(true); setBibtexContent(null); try { const res = await axios.get('/api/search', { params: { q: searchQuery, source: searchSource, rows, start, sort } }); setSearchResults(res.data); // 写入缓存 setSearchCache(prev => ({ ...prev, [cacheKey]: res.data })); } catch (e) { console.error('检索文献失败', e); showAlert('检索失败,请确认后端连接及 API 密钥配置。', '检索出错'); } finally { setSearching(false); } }; const handleSearch = async (e: React.FormEvent) => { e.preventDefault(); setSearchStart(0); executeSearch(0, searchRows, searchSort); }; const handlePageChange = (newStart: number) => { setSearchStart(newStart); executeSearch(newStart, searchRows, searchSort); }; const handleSortChange = (newSort: string) => { setSearchSort(newSort); setSearchStart(0); executeSearch(0, searchRows, newSort); }; const handleRowsChange = (newRows: number) => { setSearchRows(newRows); setSearchStart(0); executeSearch(0, newRows, searchSort); }; // 3. 触发文献双格式下载 const handleDownload = async (bibcode: string, force = false) => { setDownloadingBibcodes(prev => ({ ...prev, [bibcode]: true })); try { const res = await axios.post('/api/download', { bibcode, force }); // 更新库及检索列表中的状态 setSearchResults(prev => prev.map(p => p.bibcode === bibcode ? res.data : p)); setLibrary(prev => { if (prev.some(p => p.bibcode === bibcode)) { return prev.map(p => p.bibcode === bibcode ? res.data : p); } else { return [res.data, ...prev]; } }); if (selectedPaper?.bibcode === bibcode) { setSelectedPaper(res.data); } } catch (e) { console.error('下载文献失败', e); showAlert('文献下载失败,请检查 ADS 网络限制与网络代理!', '下载失败'); } finally { setDownloadingBibcodes(prev => ({ ...prev, [bibcode]: false })); } }; // 3b. 手动上传文献文件以绕过反爬/人机验证 const handleManualUpload = async (bibcode: string, type: 'pdf' | 'html', file: File) => { setUploadingBibcode(bibcode); const formData = new FormData(); formData.append('bibcode', bibcode); formData.append('type', type); formData.append('file', file); try { const res = await axios.post('/api/upload', formData, { headers: { 'Content-Type': 'multipart/form-data', }, }); // 更新前端库及检索列表状态 setSearchResults(prev => prev.map(p => p.bibcode === bibcode ? res.data : p)); setLibrary(prev => { if (prev.some(p => p.bibcode === bibcode)) { return prev.map(p => p.bibcode === bibcode ? res.data : p); } else { return [res.data, ...prev]; } }); if (selectedPaper?.bibcode === bibcode) { setSelectedPaper(res.data); } showAlert('手动文献文件上传导入成功!', '上传成功'); } catch (e: any) { console.error('手动文件上传失败', e); const errMsg = e.response?.data || '请确保上传的是合法且完整的文件。'; showAlert(`文件上传失败: ${errMsg}`, '上传出错'); } finally { setUploadingBibcode(null); } }; // 3c. 手动标记文献为“无有效全文资源” const handleMarkNoResource = async (bibcode: string, clear = false) => { const performMark = async () => { try { const res = await axios.post('/api/no_resource', { bibcode, clear }); // 更新前端库及检索列表状态 setSearchResults(prev => prev.map(p => p.bibcode === bibcode ? res.data : p)); setLibrary(prev => { if (prev.some(p => p.bibcode === bibcode)) { return prev.map(p => p.bibcode === bibcode ? res.data : p); } else { return [res.data, ...prev]; } }); if (selectedPaper?.bibcode === bibcode) { setSelectedPaper(res.data); } showAlert( clear ? '已成功清除“无全文资源”标记,该文献已被重新允许重载!' : '已成功将文献标记为“无全文资源”,后续批量任务将自动跳过!', '标记更新' ); } catch (e: any) { console.error('标记更新失败', e); const errMsg = e.response?.data || '请稍后重试。'; showAlert(`标记失败: ${errMsg}`, '操作出错'); } }; if (clear) { performMark(); } else { showConfirm( '确认将此文献标记为“无有效全文资源”吗?\n标记后,未来的批量下载/解析重试任务将自动跳过此文献。', performMark, '标记无有效全文资源' ); } }; // 4. 文献解析成 Markdown (优先 HTML, 其次 PDF MinerU) const handleParse = async (bibcode: string, force = false) => { setParsing(true); try { const res = await axios.post<{ markdown: string }>('/api/parse', { bibcode, force }); setEnglishText(res.data.markdown); // 更新文献状态 setLibrary(prev => prev.map(p => p.bibcode === bibcode ? { ...p, has_markdown: true } : p)); if (selectedPaper?.bibcode === bibcode) { setSelectedPaper(prev => prev ? { ...prev, has_markdown: true } : null); } } catch (e) { console.error('文献解析失败', e); showAlert('文献排版解析失败,请检查是否已完成 HTML/PDF 下载,并配置了 MinerU API 节点。', '解析失败'); } finally { setParsing(false); } }; // 5. 对比翻译文本 (带天文学术语修正) const handleTranslate = async (bibcode: string, force = false) => { setTranslating(true); try { const res = await axios.post<{ translation: string }>('/api/translate', { bibcode, force }); setChineseText(res.data.translation); setLibrary(prev => prev.map(p => p.bibcode === bibcode ? { ...p, has_translation: true } : p)); if (selectedPaper?.bibcode === bibcode) { setSelectedPaper(prev => prev ? { ...prev, has_translation: true } : null); } } catch (e) { console.error('文献翻译失败', e); showAlert('翻译失败,请检查 .env 中的大模型 API 密钥与端点配置。', '翻译失败'); } finally { setTranslating(false); } }; // 5.5. 对文献进行向量化分块入库 (独立任务) const handleVectorize = async (bibcode: string) => { setVectorizing(true); try { const res = await axios.post<{ chunk_count: number }>('/api/embed', { bibcode }); setLibrary(prev => prev.map(p => p.bibcode === bibcode ? { ...p, has_vector: true } : p)); if (selectedPaper?.bibcode === bibcode) { setSelectedPaper(prev => prev ? { ...prev, has_vector: true } : null); } showAlert(`文献向量化分块入库成功,共切片并录入 ${res.data.chunk_count} 个向量块。`, '向量化成功'); } catch (e) { console.error('文献向量化失败', e); showAlert('向量化失败,请检查 .env 中的 Embedding API 配置。', '向量化失败'); } finally { setVectorizing(false); } }; // 6. 加载文献引用关系网络 const loadCitations = useCallback(async (bibcode: string, reset = false) => { setLoadingCitations(true); try { const res = await axios.get('/api/citations', { params: { bibcode } }); setCitationNetwork(res.data); setCitationHistory(prev => { if (reset) { return [res.data]; } if (prev.some(net => net.bibcode === res.data.bibcode)) { return prev; } return [...prev, res.data]; }); } catch (e) { console.error('加载引用拓扑失败', e); } finally { setLoadingCitations(false); } }, []); // 7. 进入阅读器 const openReader = async (paper: StandardPaper, skipTabSwitch = false) => { setSelectedPaper(paper); localStorage.setItem('last_read_bibcode', paper.bibcode); addPaperToRecent(paper); setEnglishText(''); setChineseText(''); setNotes([]); setShowNotesPanel(false); if (!skipTabSwitch) { setActiveTab('reader'); } // 获取详情 (包含已有原文及翻译) try { const res = await axios.get<{ paper: StandardPaper, english_content?: string, translation_content?: string }>('/api/paper', { params: { bibcode: paper.bibcode } }); if (res.data.english_content) { setEnglishText(res.data.english_content); } if (res.data.translation_content) { setChineseText(res.data.translation_content); } } catch (e) { console.error('加载文献详情失败', e); } // 加载该文献的所有笔记 try { const nRes = await axios.get('/api/notes', { params: { bibcode: paper.bibcode } }); setNotes(nRes.data); } catch (e) { console.error('加载笔记失败', e); } }; // 7.5. 从 RAG 问答结果跳转至目标文献段落并高亮 const handleJumpToSource = async (bibcode: string, paragraphIndex: number) => { setActiveTab('reader'); // 如果当前选中的不是目标文献,则先加载它 if (!selectedPaper || selectedPaper.bibcode !== bibcode) { setEnglishText(''); setChineseText(''); setNotes([]); setShowNotesPanel(false); try { // 获取 StandardPaper 基础信息与正文 const paperRes = await axios.get<{ paper: StandardPaper, english_content?: string, translation_content?: string }>('/api/paper', { params: { bibcode } }); setSelectedPaper(paperRes.data.paper); if (paperRes.data.english_content) { setEnglishText(paperRes.data.english_content); } if (paperRes.data.translation_content) { setChineseText(paperRes.data.translation_content); } // 加载该文献的所有笔记 const nRes = await axios.get('/api/notes', { params: { bibcode } }); setNotes(nRes.data); } catch (e) { console.error('加载跳转文献失败:', e); return; } } // 强制开启侧边栏 setShowNotesPanel(true); // 延迟等 DOM 渲染完成进行滚动高亮 setTimeout(() => { const element = document.getElementById(`paragraph-block-${paragraphIndex}`); if (element) { element.scrollIntoView({ behavior: 'smooth', block: 'center' }); element.classList.add('bg-amber-100', 'transition-all'); setTimeout(() => { element.classList.remove('bg-amber-100'); }, 2500); } }, 400); }; // 8. 批量导出 BibTeX const handleExportBibtex = async () => { if (exportingList.length === 0) return; setExporting(true); try { const res = await axios.post<{ bibtex: string }>('/api/export', { bibcodes: exportingList }); setBibtexContent(res.data.bibtex); } catch (e) { console.error('导出 BibTeX 失败', e); showAlert('导出 BibTeX 失败,请检查 ADS Token。', '导出失败'); } finally { setExporting(false); } }; // 批量选择引文 const toggleExportItem = (bibcode: string) => { setExportingList(prev => prev.includes(bibcode) ? prev.filter(b => b !== bibcode) : [...prev, bibcode] ); }; // 笔记相关操作 const handleCreateNote = async () => { if (!selectedPaper || selectedParagraphIdx === null || !newNoteText.trim()) return; try { const res = await axios.post('/api/notes', { bibcode: selectedPaper.bibcode, paragraph_index: selectedParagraphIdx, note_text: newNoteText.trim(), highlight_color: newNoteColor, selected_text: selectedText, }); setNotes(prev => [...prev, res.data]); setNewNoteText(''); setSelectedParagraphIdx(null); setSelectedText(''); } catch (e) { console.error('保存笔记失败', e); } }; const handleDeleteNote = async (id: number) => { try { await axios.delete('/api/notes', { params: { id } }); setNotes(prev => prev.filter(n => n.id !== id)); } catch (e) { console.error('删除笔记失败', e); } }; // 选中文本时弹出笔记添加面板 const handleTextSelection = (paragraphIdx: number) => { const sel = window.getSelection(); if (sel && sel.toString().trim().length > 3) { setSelectedText(sel.toString().trim()); setSelectedParagraphIdx(paragraphIdx); setShowNotesPanel(true); } }; return (
{/* 导航左侧栏 */} {/* 主工作区 */}
{/* 选项卡容器 */}
{activeTab === 'search' && ( setDetailBibcode(paper.bibcode)} /> )} {activeTab === 'library' && ( setDetailBibcode(paper.bibcode)} onOpenReader={openReader} onOpenCitation={openCitation} /> )} {activeTab === 'reader' && ( selectedPaper ? ( ) : (

未选定阅读文献

双语对比阅读功能需要基于已下载的馆藏文献。请前往“馆藏管理”选择或上传一篇文献,然后开启沉浸式双语精读。

) )} {activeTab === 'citation' && ( selectedPaper ? ( ) : (

未选定中心文献

引用星系拓扑图谱用于展示单篇文献的“引用 - 被引”星系脉络。请前往“馆藏管理”选择一篇文献并生成其引用图谱。

) )} {activeTab === 'sync' && ( )}
{/* 全局统一样式弹窗 */} {dialog && (
{ if (dialog.onCancel) dialog.onCancel(); setDialog(null); }} className="fixed inset-0 z-[60] flex items-center justify-center bg-slate-900/40 backdrop-blur-xs transition-all" >
e.stopPropagation()} className="bg-white rounded-xl border border-slate-200 shadow-xl max-w-sm w-full p-6 space-y-4" >

{dialog.title}

{dialog.message}

{dialog.type === 'confirm' && ( )}
)} {/* 未入库文献操作弹窗 */} {uncachedBibcode && (
setUncachedBibcode(null)} className="fixed inset-0 z-50 flex items-center justify-center bg-slate-900/40 backdrop-blur-xs transition-all animate-fade-in" >
e.stopPropagation()} className="bg-white rounded-xl border border-slate-200 shadow-xl max-w-sm w-full p-6 space-y-4" >

文献尚未入库

文献 {uncachedBibcode} 尚未收录在本地数据库中。

您可以选择在线拉取该文献元数据并入库,或是直接跳转至 NASA ADS 平台查看其原始页面。

)} {/* 文献详情弹窗 */} {detailPaper && (
setDetailBibcode(null)} className="fixed inset-0 z-50 flex items-center justify-center bg-slate-900/40 backdrop-blur-xs transition-all animate-fade-in cursor-pointer" >
e.stopPropagation()} className="bg-white rounded-xl border border-slate-200 shadow-xl max-w-lg w-full p-6 space-y-4 cursor-default animate-fade-in" > {/* 标题 & 关闭 */}
文献详情元数据

{getDoctypeBadge(detailPaper.doctype)} {detailPaper.title}

{/* 详情内容 */}
{/* 作者 */}
作者列表

{detailPaper.authors.join(', ')}

{/* 期刊 & 年份 */}
发表期刊 {detailPaper.pub_journal || '未标注'}
发表年份 {detailPaper.year}
{/* 摘要 */}
摘要

{detailPaper.abstract_text || '该文献暂无摘要数据。'}

{/* 关键字 */}
关键词 {detailPaper.keywords && detailPaper.keywords.length > 0 ? (
{detailPaper.keywords.map(kw => ( {kw} ))}
) : (
暂无关键词
)}
{/* 标识符 */} {/* 手动上传文件(应对防爬阻断) */}
手动离线上传文献 防爬/人机验证备用

若自动下载受阻,可在浏览器中打开上方链接,手动保存 PDF 或 HTML 后在此处上传覆盖。

{ const file = e.target.files?.[0]; if (file) handleManualUpload(detailPaper.bibcode, 'pdf', file); }} className="absolute inset-0 opacity-0 cursor-pointer w-full h-full" disabled={uploadingBibcode === detailPaper.bibcode} /> {detailPaper.has_pdf && ( 已下载 )} {uploadingBibcode === detailPaper.bibcode ? '上传中...' : '上传 PDF 文献'} 支持 .pdf 格式
{ const file = e.target.files?.[0]; if (file) handleManualUpload(detailPaper.bibcode, 'html', file); }} className="absolute inset-0 opacity-0 cursor-pointer w-full h-full" disabled={uploadingBibcode === detailPaper.bibcode} /> {detailPaper.has_html && ( 已下载 )} {uploadingBibcode === detailPaper.bibcode ? '上传中...' : '上传 HTML 文献'} 支持 .html 格式
{!detailPaper.is_downloaded && (
{detailPaper.pdf_error === 'no_resource' && detailPaper.html_error === 'no_resource' ? ( ) : ( )}
)}
{/* 自动下载失败诊断 */} {!detailPaper.is_downloaded && (detailPaper.pdf_error || detailPaper.html_error) && (
{detailPaper.pdf_error === 'no_resource' && detailPaper.html_error === 'no_resource' ? (
已手动标记为【无有效全文资源】
该文献已从物理文献下载重试队列中排除,后续批量下载时系统将自动跳过此文献。
) : ( <>
自动下载失败诊断信息:
{detailPaper.pdf_error && (
PDF 错误: {detailPaper.pdf_error}
)} {detailPaper.html_error && (
HTML 错误: {detailPaper.html_error}
)} )}
)}
{/* 底部操作:整合所有动作(阅读、图谱、下载) */}
{detailPaper.is_downloaded ? ( <> ) : ( <> )}
)}
); }