ArXivResearchExplorer / src /components /HighlightsView.tsx
RBJin's picture
Upload 20 files
81cb6e0 verified
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>
);
}