Spaces:
Running
Running
| 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> | |
| ); | |
| } | |