Spaces:
Running
Running
| import { Highlight, Category, FavoritePaper } from '../types'; | |
| const KEYS = { | |
| highlights: 'arxiv_explorer_highlights', | |
| categories: 'arxiv_explorer_categories', | |
| favorites: 'arxiv_explorer_favorites', | |
| cachedSections: 'arxiv_explorer_sections_cache', | |
| }; | |
| function loadJson<T>(key: string, fallback: T): T { | |
| try { | |
| const data = localStorage.getItem(key); | |
| return data ? JSON.parse(data) : fallback; | |
| } catch { | |
| return fallback; | |
| } | |
| } | |
| function saveJson<T>(key: string, data: T) { | |
| try { | |
| localStorage.setItem(key, JSON.stringify(data)); | |
| } catch (e) { | |
| console.warn('Failed to save to localStorage:', e); | |
| } | |
| } | |
| // ==================== Highlights ==================== | |
| export function getHighlights(): Highlight[] { | |
| return loadJson<Highlight[]>(KEYS.highlights, []); | |
| } | |
| export function addHighlight(highlight: Omit<Highlight, 'id' | 'timestamp'>): Highlight { | |
| const highlights = getHighlights(); | |
| const newHighlight: Highlight = { | |
| ...highlight, | |
| id: `hl_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, | |
| timestamp: Date.now(), | |
| }; | |
| highlights.unshift(newHighlight); | |
| saveJson(KEYS.highlights, highlights); | |
| return newHighlight; | |
| } | |
| export function removeHighlight(id: string) { | |
| const highlights = getHighlights().filter((h) => h.id !== id); | |
| saveJson(KEYS.highlights, highlights); | |
| } | |
| // ==================== Categories ==================== | |
| const defaultCategories: Category[] = [ | |
| { id: 'uncategorized', name: '未分类 / Uncategorized', color: '#6b7280' }, | |
| ]; | |
| export function getCategories(): Category[] { | |
| return loadJson<Category[]>(KEYS.categories, defaultCategories); | |
| } | |
| export function addCategory(name: string, color: string): Category { | |
| const categories = getCategories(); | |
| const newCat: Category = { | |
| id: `cat_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`, | |
| name, | |
| color, | |
| }; | |
| categories.push(newCat); | |
| saveJson(KEYS.categories, categories); | |
| return newCat; | |
| } | |
| export function removeCategory(id: string) { | |
| if (id === 'uncategorized') return; | |
| const categories = getCategories().filter((c) => c.id !== id); | |
| saveJson(KEYS.categories, categories); | |
| // Move papers in deleted category to uncategorized | |
| const favorites = getFavorites(); | |
| favorites.forEach((f) => { | |
| if (f.categoryId === id) f.categoryId = 'uncategorized'; | |
| }); | |
| saveJson(KEYS.favorites, favorites); | |
| } | |
| export function updateCategory(id: string, name: string, color: string) { | |
| const categories = getCategories().map((c) => | |
| c.id === id ? { ...c, name, color } : c | |
| ); | |
| saveJson(KEYS.categories, categories); | |
| } | |
| // ==================== Favorites ==================== | |
| export function getFavorites(): FavoritePaper[] { | |
| return loadJson<FavoritePaper[]>(KEYS.favorites, []); | |
| } | |
| export function addFavorite( | |
| paperId: string, | |
| paperTitle: string, | |
| paperAuthors: string[], | |
| categoryId: string = 'uncategorized' | |
| ): FavoritePaper { | |
| const favorites = getFavorites(); | |
| const existing = favorites.find((f) => f.paperId === paperId); | |
| if (existing) return existing; | |
| const fav: FavoritePaper = { | |
| paperId, | |
| paperTitle, | |
| paperAuthors, | |
| categoryId, | |
| timestamp: Date.now(), | |
| }; | |
| favorites.unshift(fav); | |
| saveJson(KEYS.favorites, favorites); | |
| return fav; | |
| } | |
| export function removeFavorite(paperId: string) { | |
| const favorites = getFavorites().filter((f) => f.paperId !== paperId); | |
| saveJson(KEYS.favorites, favorites); | |
| } | |
| export function moveFavoriteToCategory(paperId: string, categoryId: string) { | |
| const favorites = getFavorites().map((f) => | |
| f.paperId === paperId ? { ...f, categoryId } : f | |
| ); | |
| saveJson(KEYS.favorites, favorites); | |
| } | |
| export function isFavorited(paperId: string): boolean { | |
| return getFavorites().some((f) => f.paperId === paperId); | |
| } | |
| // ==================== Section Cache ==================== | |
| interface CachedSections { | |
| [arxivId: string]: { | |
| introduction?: string; | |
| relatedWork?: string; | |
| methods?: string; | |
| references?: Array<{ key: string; number: string; text: string; arxivId?: string }>; | |
| timestamp: number; | |
| }; | |
| } | |
| export function getCachedSections(arxivId: string) { | |
| const cache = loadJson<CachedSections>(KEYS.cachedSections, {}); | |
| const entry = cache[arxivId]; | |
| if (entry && Date.now() - entry.timestamp < 7 * 24 * 60 * 60 * 1000) { | |
| return entry; | |
| } | |
| return null; | |
| } | |
| export function setCachedSections( | |
| arxivId: string, | |
| data: { | |
| introduction?: string; | |
| relatedWork?: string; | |
| methods?: string; | |
| references?: Array<{ key: string; number: string; text: string; arxivId?: string }>; | |
| } | |
| ) { | |
| const cache = loadJson<CachedSections>(KEYS.cachedSections, {}); | |
| // Limit cache size | |
| const keys = Object.keys(cache); | |
| if (keys.length > 100) { | |
| const sorted = keys.sort((a, b) => (cache[a].timestamp || 0) - (cache[b].timestamp || 0)); | |
| sorted.slice(0, 20).forEach((k) => delete cache[k]); | |
| } | |
| cache[arxivId] = { ...data, timestamp: Date.now() }; | |
| saveJson(KEYS.cachedSections, cache); | |
| } | |