378 lines
14 KiB
TypeScript
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>
|
|
);
|
|
}
|