AstroResearch/dashboard/src/App.tsx

378 lines
14 KiB
TypeScript

// dashboard/src/App.tsx
import { useState, useEffect, useCallback } from 'react';
import axios from 'axios';
import { Sidebar } from './components/layout/Sidebar';
import { SearchPanel } from './features/search/SearchPanel';
import { LibraryPanel } from './features/library/LibraryPanel';
import { ReaderPanel } from './features/reader/ReaderPanel';
import { CitationPanel } from './features/citation/CitationPanel';
import { SettingsPanel } from './features/settings/SettingsPanel';
import type { StandardPaper, CitationNetwork, NoteRecord } from './types';
export default function App() {
const [activeTab, setActiveTab] = useState<'search' | 'library' | 'reader' | 'citation' | 'settings'>('search');
// 共享数据状态
const [library, setLibrary] = useState<StandardPaper[]>([]);
const [selectedPaper, setSelectedPaper] = useState<StandardPaper | null>(null);
// 检索页状态
const [searchQuery, setSearchQuery] = useState('');
const [searchSource, setSearchSource] = useState<'all' | 'ads' | 'arxiv'>('all');
const [searchResults, setSearchResults] = useState<StandardPaper[]>([]);
const [searching, setSearching] = useState(false);
const [exportingList, setExportingList] = useState<string[]>([]);
const [bibtexContent, setBibtexContent] = useState<string | null>(null);
const [exporting, setExporting] = useState(false);
// 读者页状态
const [englishText, setEnglishText] = useState('');
const [chineseText, setChineseText] = useState('');
const [parsing, setParsing] = useState(false);
const [translating, setTranslating] = useState(false);
// 引用星系数据状态
const [citationNetwork, setCitationNetwork] = useState<CitationNetwork | null>(null);
const [loadingCitations, setLoadingCitations] = useState(false);
const [citationHistory, setCitationHistory] = useState<CitationNetwork[]>([]); // 多跳历史
// 笔记系统状态
const [notes, setNotes] = useState<NoteRecord[]>([]);
const [showNotesPanel, setShowNotesPanel] = useState(false);
const [newNoteText, setNewNoteText] = useState('');
const [newNoteColor, setNewNoteColor] = useState('yellow');
const [selectedParagraphIdx, setSelectedParagraphIdx] = useState<number | null>(null);
const [selectedText, setSelectedText] = useState('');
// 下载进度状态
const [downloadingBibcodes, setDownloadingBibcodes] = useState<Record<string, boolean>>({});
// 1. 初始化时加载本地文献
useEffect(() => {
fetchLibrary();
}, []);
const fetchLibrary = async () => {
try {
const res = await axios.get<StandardPaper[]>('/api/library');
setLibrary(res.data);
} catch (e) {
console.error('加载本地文献库失败', e);
}
};
// 2. 检索文献
const handleSearch = async (e: React.FormEvent) => {
e.preventDefault();
if (!searchQuery.trim()) return;
setSearching(true);
setBibtexContent(null);
try {
const res = await axios.get<StandardPaper[]>('/api/search', {
params: { q: searchQuery, source: searchSource, rows: 15 }
});
setSearchResults(res.data);
} catch (e) {
console.error('检索文献失败', e);
alert('检索失败,请确认后端连接及 API 密钥配置。');
} finally {
setSearching(false);
}
};
// 3. 触发文献双格式下载
const handleDownload = async (bibcode: string, force = false) => {
setDownloadingBibcodes(prev => ({ ...prev, [bibcode]: true }));
try {
const res = await axios.post<StandardPaper>('/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);
alert('文献下载失败,请检查 ADS 网络限制与网络代理!');
} finally {
setDownloadingBibcodes(prev => ({ ...prev, [bibcode]: false }));
}
};
// 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);
alert('文献排版解析失败,请检查是否已完成 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);
alert('翻译失败,请检查 .env 中的大模型 API 密钥与端点配置。');
} finally {
setTranslating(false);
}
};
// 6. 加载文献引用关系网络
const loadCitations = useCallback(async (bibcode: string, reset = false) => {
setLoadingCitations(true);
try {
const res = await axios.get<CitationNetwork>('/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) => {
setSelectedPaper(paper);
setEnglishText('');
setChineseText('');
setNotes([]);
setShowNotesPanel(false);
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<NoteRecord[]>('/api/notes', { params: { bibcode: paper.bibcode } });
setNotes(nRes.data);
} catch (e) {
console.error('加载笔记失败', e);
}
};
// 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);
alert('导出 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<NoteRecord>('/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 (
<div className="flex h-screen overflow-hidden text-slate-800">
{/* 炫酷淡雅背景装饰 */}
<div className="absolute inset-0 bg-[#f8fafc] z-[-10]">
<div className="absolute top-[10%] left-[20%] w-[300px] h-[300px] bg-purple-500/5 rounded-full blur-[120px] animate-pulse-slow" />
<div className="absolute bottom-[10%] right-[20%] w-[400px] h-[400px] bg-blue-500/5 rounded-full blur-[150px] animate-pulse-slow" style={{ animationDelay: '1.5s' }} />
</div>
{/* 导航左侧栏 */}
<Sidebar
activeTab={activeTab}
setActiveTab={setActiveTab}
selectedPaper={selectedPaper}
loadCitations={loadCitations}
/>
{/* 主工作区 */}
<main className="flex-1 flex flex-col overflow-hidden">
{/* 顶部状态条 */}
<header className="h-16 border-b border-slate-200/60 px-8 flex items-center justify-between bg-white/40 backdrop-blur-md">
<div className="flex items-center gap-2">
<span className="h-2 w-2 rounded-full bg-emerald-500 animate-pulse" />
<span className="text-xs text-slate-600 font-medium"></span>
</div>
<div className="text-xs text-slate-600">
: {library.length}
</div>
</header>
{/* 选项卡容器 */}
<div className="flex-1 overflow-y-auto p-8">
{activeTab === 'search' && (
<SearchPanel
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
searchSource={searchSource}
setSearchSource={setSearchSource}
searching={searching}
handleSearch={handleSearch}
searchResults={searchResults}
exportingList={exportingList}
toggleExportItem={toggleExportItem}
handleExportBibtex={handleExportBibtex}
exporting={exporting}
bibtexContent={bibtexContent}
downloadingBibcodes={downloadingBibcodes}
handleDownload={handleDownload}
selectedPaper={selectedPaper}
setSelectedPaper={setSelectedPaper}
openReader={openReader}
setActiveTab={setActiveTab}
loadCitations={loadCitations}
/>
)}
{activeTab === 'library' && (
<LibraryPanel
library={library}
fetchLibrary={fetchLibrary}
openReader={openReader}
setSelectedPaper={setSelectedPaper}
setActiveTab={setActiveTab}
loadCitations={loadCitations}
downloadingBibcodes={downloadingBibcodes}
handleDownload={handleDownload}
/>
)}
{activeTab === 'reader' && selectedPaper && (
<ReaderPanel
selectedPaper={selectedPaper}
parsing={parsing}
handleParse={handleParse}
translating={translating}
handleTranslate={handleTranslate}
showNotesPanel={showNotesPanel}
setShowNotesPanel={setShowNotesPanel}
notes={notes}
englishText={englishText}
chineseText={chineseText}
handleTextSelection={handleTextSelection}
selectedParagraphIdx={selectedParagraphIdx}
setSelectedParagraphIdx={setSelectedParagraphIdx}
selectedText={selectedText}
setSelectedText={setSelectedText}
newNoteColor={newNoteColor}
setNewNoteColor={setNewNoteColor}
newNoteText={newNoteText}
setNewNoteText={setNewNoteText}
handleCreateNote={handleCreateNote}
handleDeleteNote={handleDeleteNote}
/>
)}
{activeTab === 'citation' && (
<CitationPanel
selectedPaper={selectedPaper}
loadingCitations={loadingCitations}
citationNetwork={citationNetwork}
citationHistory={citationHistory}
loadCitations={loadCitations}
/>
)}
{activeTab === 'settings' && (
<SettingsPanel />
)}
</div>
</main>
</div>
);
}