Spaces:
Running
Running
| import { useState, useEffect } from 'react'; | |
| import { Highlight, SectionType } from '../types'; | |
| import { getHighlights, removeHighlight } from '../utils/storage'; | |
| interface Props { | |
| refreshKey: number; | |
| onViewPaper: (paperId: string) => void; | |
| } | |
| const SECTION_LABELS: Record<SectionType, string> = { | |
| abstract: '摘要 Abstract', | |
| introduction: '引言 Introduction', | |
| relatedWork: '相关工作 Related Work', | |
| methods: '方法 Methods', | |
| }; | |
| export default function HighlightsView({ refreshKey, onViewPaper }: Props) { | |
| const [highlights, setHighlights] = useState<Highlight[]>([]); | |
| const [filter, setFilter] = useState<string>('all'); | |
| const [searchText, setSearchText] = useState(''); | |
| useEffect(() => { | |
| setHighlights(getHighlights()); | |
| }, [refreshKey]); | |
| // Group by paper | |
| const grouped = highlights.reduce( | |
| (acc, h) => { | |
| if (!acc[h.paperId]) { | |
| acc[h.paperId] = { title: h.paperTitle, paperId: h.paperId, items: [] }; | |
| } | |
| acc[h.paperId].items.push(h); | |
| return acc; | |
| }, | |
| {} as Record<string, { title: string; paperId: string; items: Highlight[] }> | |
| ); | |
| const filteredHighlights = highlights.filter((h) => { | |
| if (filter !== 'all' && h.section !== filter) return false; | |
| if (searchText && !h.text.toLowerCase().includes(searchText.toLowerCase())) return false; | |
| return true; | |
| }); | |
| const filteredGrouped = Object.values(grouped) | |
| .map((group) => ({ | |
| ...group, | |
| items: group.items.filter((h) => { | |
| if (filter !== 'all' && h.section !== filter) return false; | |
| if (searchText && !h.text.toLowerCase().includes(searchText.toLowerCase())) return false; | |
| return true; | |
| }), | |
| })) | |
| .filter((group) => group.items.length > 0); | |
| const handleDelete = (id: string) => { | |
| removeHighlight(id); | |
| setHighlights(getHighlights()); | |
| }; | |
| const exportHighlights = () => { | |
| const text = filteredHighlights | |
| .map( | |
| (h) => | |
| `"${h.text}"\n — ${h.paperTitle} (${h.paperId}), ${SECTION_LABELS[h.section]}\n ${new Date(h.timestamp).toLocaleString()}\n` | |
| ) | |
| .join('\n---\n\n'); | |
| const blob = new Blob([text], { type: 'text/plain;charset=utf-8' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = `arxiv_highlights_${new Date().toISOString().slice(0, 10)}.txt`; | |
| a.click(); | |
| URL.revokeObjectURL(url); | |
| }; | |
| return ( | |
| <div className="max-w-5xl mx-auto"> | |
| <div className="flex items-center justify-between mb-6"> | |
| <h2 className="text-xl font-bold text-gray-800"> | |
| 📌 文本收藏 Highlights | |
| <span className="text-sm font-normal text-gray-400 ml-2"> | |
| ({highlights.length} 条) | |
| </span> | |
| </h2> | |
| {highlights.length > 0 && ( | |
| <button | |
| onClick={exportHighlights} | |
| className="px-4 py-2 text-sm bg-emerald-50 text-emerald-600 rounded-lg hover:bg-emerald-100 transition-colors font-medium" | |
| > | |
| 📥 导出 Export | |
| </button> | |
| )} | |
| </div> | |
| {/* Filters */} | |
| {highlights.length > 0 && ( | |
| <div className="flex flex-wrap items-center gap-3 mb-6"> | |
| <input | |
| type="text" | |
| value={searchText} | |
| onChange={(e) => setSearchText(e.target.value)} | |
| placeholder="搜索收藏内容 Search highlights..." | |
| className="px-4 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-1 focus:ring-indigo-500 w-64" | |
| /> | |
| <div className="flex gap-1"> | |
| <button | |
| onClick={() => setFilter('all')} | |
| className={`px-3 py-1.5 text-xs rounded-lg transition-colors ${ | |
| filter === 'all' | |
| ? 'bg-indigo-100 text-indigo-700 font-medium' | |
| : 'bg-gray-50 text-gray-500 hover:bg-gray-100' | |
| }`} | |
| > | |
| 全部 All | |
| </button> | |
| {(['abstract', 'introduction', 'relatedWork', 'methods'] as SectionType[]).map( | |
| (section) => ( | |
| <button | |
| key={section} | |
| onClick={() => setFilter(section)} | |
| className={`px-3 py-1.5 text-xs rounded-lg transition-colors ${ | |
| filter === section | |
| ? 'bg-indigo-100 text-indigo-700 font-medium' | |
| : 'bg-gray-50 text-gray-500 hover:bg-gray-100' | |
| }`} | |
| > | |
| {SECTION_LABELS[section]} | |
| </button> | |
| ) | |
| )} | |
| </div> | |
| </div> | |
| )} | |
| {filteredGrouped.length === 0 ? ( | |
| <div className="text-center py-20 text-gray-400"> | |
| <div className="text-5xl mb-4">📌</div> | |
| <p>暂无文本收藏</p> | |
| <p className="text-sm mt-1">No highlights saved yet</p> | |
| <p className="text-sm mt-2">在论文详情中选中文本即可收藏</p> | |
| <p className="text-sm">Select text in paper detail to save highlights</p> | |
| </div> | |
| ) : ( | |
| <div className="space-y-6"> | |
| {filteredGrouped.map((group) => ( | |
| <div | |
| key={group.paperId} | |
| className="bg-white rounded-xl border border-gray-100 shadow-sm overflow-hidden" | |
| > | |
| {/* Paper Header */} | |
| <div className="px-5 py-3 bg-gray-50 border-b border-gray-100 flex items-center gap-3"> | |
| <span className="text-sm font-medium text-gray-700 flex-1 truncate"> | |
| 📄 {group.title} | |
| </span> | |
| <span className="text-xs text-gray-400 shrink-0">{group.paperId}</span> | |
| <button | |
| onClick={() => onViewPaper(group.paperId)} | |
| className="text-xs text-indigo-600 hover:text-indigo-700 font-medium shrink-0" | |
| > | |
| 查看文章 View | |
| </button> | |
| </div> | |
| {/* Highlights */} | |
| <div className="divide-y divide-gray-50"> | |
| {group.items.map((highlight) => ( | |
| <div key={highlight.id} className="px-5 py-3 hover:bg-gray-50/50 transition-colors"> | |
| <div className="flex items-start gap-3"> | |
| <span className="mt-1 text-indigo-400 text-lg leading-none">"</span> | |
| <div className="flex-1 min-w-0"> | |
| <p className="text-sm text-gray-700 leading-relaxed italic"> | |
| {highlight.text} | |
| </p> | |
| <div className="mt-1.5 flex items-center gap-2 text-xs text-gray-400"> | |
| <span className="bg-indigo-50 text-indigo-500 px-2 py-0.5 rounded"> | |
| {SECTION_LABELS[highlight.section]} | |
| </span> | |
| <span>{new Date(highlight.timestamp).toLocaleString()}</span> | |
| </div> | |
| </div> | |
| <button | |
| onClick={() => handleDelete(highlight.id)} | |
| className="shrink-0 p-1 text-gray-300 hover:text-red-500 transition-colors" | |
| title="删除 Delete" | |
| > | |
| ✕ | |
| </button> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } | |