RBJin's picture
Upload 20 files
81cb6e0 verified
import { useState, useCallback } from 'react';
import { Paper, ViewType } from './types';
import { fetchPaperById, fetchPaperSections } from './utils/api';
import { getCachedSections, setCachedSections } from './utils/storage';
import SearchView from './components/SearchView';
import PaperDetail from './components/PaperDetail';
import FavoritesView from './components/FavoritesView';
import HighlightsView from './components/HighlightsView';
const NAV_ITEMS: { key: ViewType; label: string; icon: string }[] = [
{ key: 'search', label: '搜索 Search', icon: '🔍' },
{ key: 'favorites', label: '收藏 Favorites', icon: '⭐' },
{ key: 'highlights', label: '文本收藏 Highlights', icon: '📌' },
];
export function App() {
const [currentView, setCurrentView] = useState<ViewType>('search');
const [detailPaper, setDetailPaper] = useState<Paper | null>(null);
const [favRefreshKey, setFavRefreshKey] = useState(0);
const [hlRefreshKey, setHlRefreshKey] = useState(0);
const [navigationHistory, setNavigationHistory] = useState<Paper[]>([]);
const refreshFavorites = useCallback(() => {
setFavRefreshKey((k) => k + 1);
}, []);
const refreshHighlights = useCallback(() => {
setHlRefreshKey((k) => k + 1);
}, []);
const handleViewDetail = useCallback((paper: Paper) => {
setDetailPaper(paper);
setCurrentView('detail');
setNavigationHistory([]);
window.scrollTo(0, 0);
}, []);
const handleBack = useCallback(() => {
if (navigationHistory.length > 0) {
const prev = navigationHistory[navigationHistory.length - 1];
setNavigationHistory((h) => h.slice(0, -1));
setDetailPaper(prev);
} else {
setCurrentView('search');
setDetailPaper(null);
}
}, [navigationHistory]);
// Navigate to a referenced paper by ArXiv ID
const handleNavigateToPaper = useCallback(
async (arxivId: string) => {
// Save current paper to history
if (detailPaper) {
setNavigationHistory((h) => [...h, detailPaper]);
}
// Fetch the paper by ID
try {
const found = await fetchPaperById(arxivId);
if (found) {
let paper = found;
// Load sections
const cached = getCachedSections(paper.id);
if (cached) {
paper = { ...paper, ...cached, sectionsLoaded: true };
} else {
try {
const sections = await fetchPaperSections(paper.id);
setCachedSections(paper.id, sections);
paper = { ...paper, ...sections, sectionsLoaded: true };
} catch {
// Sections loading failed, but we can still show the paper
}
}
setDetailPaper(paper);
window.scrollTo(0, 0);
} else {
// If not found, open ArXiv page
window.open(`https://arxiv.org/abs/${arxivId}`, '_blank');
}
} catch {
window.open(`https://arxiv.org/abs/${arxivId}`, '_blank');
}
},
[detailPaper]
);
// View paper from favorites/highlights
const handleViewPaperById = useCallback(async (paperId: string) => {
try {
const found = await fetchPaperById(paperId);
if (found) {
let paper = found;
const cached = getCachedSections(paper.id);
if (cached) {
paper = { ...paper, ...cached, sectionsLoaded: true };
}
setDetailPaper(paper);
setCurrentView('detail');
setNavigationHistory([]);
window.scrollTo(0, 0);
}
} catch {
window.open(`https://arxiv.org/abs/${paperId}`, '_blank');
}
}, []);
return (
<div className="min-h-screen bg-gray-50/80">
{/* Top Navigation */}
<header className="sticky top-0 z-40 bg-white/90 backdrop-blur-md border-b border-gray-100 shadow-sm">
<div className="max-w-6xl mx-auto px-4 sm:px-6">
<div className="flex items-center h-14">
{/* Logo */}
<div className="flex items-center gap-2 mr-8">
<div className="w-8 h-8 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-lg flex items-center justify-center text-white text-sm font-bold">
Ax
</div>
<span className="font-bold text-gray-800 hidden sm:inline">
ArXiv Explorer
</span>
</div>
{/* Navigation */}
<nav className="flex gap-1">
{NAV_ITEMS.map((item) => (
<button
key={item.key}
onClick={() => {
setCurrentView(item.key);
if (item.key !== 'detail') {
setDetailPaper(null);
}
}}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-all ${
currentView === item.key ||
(item.key === 'search' && currentView === 'detail')
? 'bg-indigo-50 text-indigo-700'
: 'text-gray-500 hover:text-gray-700 hover:bg-gray-50'
}`}
>
<span className="mr-1.5">{item.icon}</span>
{item.label}
</button>
))}
</nav>
{/* Navigation history indicator */}
{currentView === 'detail' && navigationHistory.length > 0 && (
<div className="ml-auto flex items-center gap-1 text-xs text-gray-400">
<span>📚 导航深度 Depth: {navigationHistory.length}</span>
</div>
)}
</div>
</div>
</header>
{/* Main Content */}
<main className="px-4 sm:px-6 py-6">
{currentView === 'search' && (
<SearchView
onViewDetail={handleViewDetail}
onRefreshFavorites={refreshFavorites}
/>
)}
{currentView === 'detail' && detailPaper && (
<PaperDetail
paper={detailPaper}
onBack={handleBack}
onNavigateToPaper={handleNavigateToPaper}
onHighlightAdded={refreshHighlights}
/>
)}
{currentView === 'favorites' && (
<FavoritesView
refreshKey={favRefreshKey}
onViewPaper={handleViewPaperById}
/>
)}
{currentView === 'highlights' && (
<HighlightsView
refreshKey={hlRefreshKey}
onViewPaper={handleViewPaperById}
/>
)}
</main>
{/* Footer */}
<footer className="mt-12 py-6 border-t border-gray-100 text-center text-xs text-gray-400">
<p>
ArXiv Research Explorer — 使用 ArXiv API 和 ar5iv 构建
</p>
<p className="mt-1">
数据来源: <a href="https://arxiv.org" target="_blank" rel="noopener noreferrer" className="text-indigo-400 hover:underline">arxiv.org</a>
{' · '}
HTML渲染: <a href="https://ar5iv.labs.arxiv.org" target="_blank" rel="noopener noreferrer" className="text-indigo-400 hover:underline">ar5iv</a>
{' · '}
请遵守 ArXiv 的使用条款和请求频率限制
</p>
</footer>
</div>
);
}