import { useState, useCallback } from 'react'; import { Paper, SectionType } from '../types'; import { searchArxiv, fetchPaperSections } from '../utils/api'; import { isFavorited, addFavorite, removeFavorite, getCachedSections, setCachedSections } from '../utils/storage'; interface Props { onViewDetail: (paper: Paper) => void; onRefreshFavorites: () => void; } const SECTION_OPTIONS: { key: SectionType; label: string; labelZh: string }[] = [ { key: 'abstract', label: 'Abstract', labelZh: '摘要' }, { key: 'introduction', label: 'Introduction', labelZh: '引言' }, { key: 'relatedWork', label: 'Related Work', labelZh: '相关工作' }, { key: 'methods', label: 'Methods', labelZh: '方法' }, ]; export default function SearchView({ onViewDetail, onRefreshFavorites }: Props) { const [query, setQuery] = useState(''); const [papers, setPapers] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const [total, setTotal] = useState(0); const [page, setPage] = useState(0); const [selectedSections, setSelectedSections] = useState(['abstract']); const [batchLoading, setBatchLoading] = useState(false); const [batchProgress, setBatchProgress] = useState({ done: 0, total: 0 }); const pageSize = 10; const doSearch = useCallback( async (startPage: number = 0) => { if (!query.trim()) return; setLoading(true); setError(''); try { const { papers: results, total: t } = await searchArxiv( query.trim(), startPage * pageSize, pageSize ); if (startPage === 0) { setPapers(results); } else { setPapers((prev) => [...prev, ...results]); } setTotal(t); setPage(startPage); } catch (e) { setError(e instanceof Error ? e.message : 'Search failed'); } finally { setLoading(false); } }, [query] ); const loadMore = () => { doSearch(page + 1); }; const loadSectionsForPaper = async (paper: Paper): Promise => { const needsFullText = selectedSections.some((s) => s !== 'abstract'); if (!needsFullText) return paper; if (paper.sectionsLoaded) return paper; // Check cache const cached = getCachedSections(paper.id); if (cached) { return { ...paper, introduction: cached.introduction, relatedWork: cached.relatedWork, methods: cached.methods, references: cached.references, sectionsLoaded: true, }; } try { const sections = await fetchPaperSections(paper.id); setCachedSections(paper.id, sections); return { ...paper, ...sections, sectionsLoaded: true, }; } catch (e) { return { ...paper, sectionsError: e instanceof Error ? e.message : 'Failed to load', sectionsLoaded: false, }; } }; const batchLoadSections = async () => { const needsFullText = selectedSections.some((s) => s !== 'abstract'); if (!needsFullText) return; setBatchLoading(true); const toLoad = papers.filter((p) => !p.sectionsLoaded); setBatchProgress({ done: 0, total: toLoad.length }); for (let i = 0; i < toLoad.length; i++) { const updated = await loadSectionsForPaper(toLoad[i]); setPapers((prev) => prev.map((p) => (p.id === updated.id ? updated : p))); setBatchProgress({ done: i + 1, total: toLoad.length }); } setBatchLoading(false); }; const toggleSection = (section: SectionType) => { setSelectedSections((prev) => prev.includes(section) ? prev.filter((s) => s !== section) : [...prev, section] ); }; const toggleFavorite = (paper: Paper) => { if (isFavorited(paper.id)) { removeFavorite(paper.id); } else { addFavorite(paper.id, paper.title, paper.authors); } onRefreshFavorites(); setPapers((prev) => [...prev]); // trigger re-render }; const viewDetail = async (paper: Paper) => { let p = paper; if (!p.sectionsLoaded && selectedSections.some((s) => s !== 'abstract')) { p = { ...p, sectionsLoading: true }; setPapers((prev) => prev.map((x) => (x.id === p.id ? p : x))); p = await loadSectionsForPaper(p); p.sectionsLoading = false; setPapers((prev) => prev.map((x) => (x.id === p.id ? p : x))); } onViewDetail(p); }; return (
{/* Search Bar */}
setQuery(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && doSearch(0)} placeholder="Search ArXiv papers by keyword..." className="flex-1 px-4 py-3 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent text-gray-800 placeholder-gray-400" />
{/* Section Filters */}
获取章节 Sections: {SECTION_OPTIONS.map((opt) => ( ))} {papers.length > 0 && selectedSections.some((s) => s !== 'abstract') && ( )}
{/* Results Info */} {total > 0 && (
共找到 Found {total.toLocaleString()} 篇文章, 已加载 loaded {papers.length}
)} {error && (
⚠️ {error}
)} {/* Paper List */}
{papers.map((paper) => ( viewDetail(paper)} onToggleFavorite={() => toggleFavorite(paper)} isFav={isFavorited(paper.id)} /> ))}
{/* Load More */} {papers.length < total && (
)} {!loading && papers.length === 0 && query && (
📄

未找到结果 No results found

请尝试其他关键词 Try different keywords

)} {!query && papers.length === 0 && (
🔬

ArXiv Research Explorer

输入关键词搜索学术论文

Enter keywords to search academic papers

)}
); } function PaperCard({ paper, selectedSections, onViewDetail, onToggleFavorite, isFav, }: { paper: Paper; selectedSections: SectionType[]; onViewDetail: () => void; onToggleFavorite: () => void; isFav: boolean; }) { const date = paper.published ? new Date(paper.published).toLocaleDateString() : ''; const previewText = paper.abstract.length > 300 ? paper.abstract.substring(0, 300) + '...' : paper.abstract; return (

{paper.title}

{paper.authors.slice(0, 3).join(', ')}{paper.authors.length > 3 ? ` +${paper.authors.length - 3}` : ''} · {date} · {paper.id} {paper.categories.slice(0, 2).map((c) => ( {c} ))}
{selectedSections.includes('abstract') && (

{previewText}

)}
📄 PDF 🔗 ArXiv {paper.sectionsLoading && ( 加载章节中... )} {paper.sectionsLoaded && ( ✓ 已加载 Loaded )} {paper.sectionsError && ( ⚠ {paper.sectionsError} )}
); }