chore: init monorepo snapshot
This commit is contained in:
@@ -0,0 +1,259 @@
|
||||
import { searchHistoryMaxItems, searchHistoryVersion } from '@/lib/constants'
|
||||
|
||||
/**
|
||||
* SearchHistoryManager - Manages search history persistence in localStorage
|
||||
*
|
||||
* This utility class handles:
|
||||
* - Storing and retrieving search history from localStorage
|
||||
* - Managing history size limits
|
||||
* - Sorting by access time and frequency
|
||||
* - Version compatibility
|
||||
*/
|
||||
|
||||
export interface SearchHistoryItem {
|
||||
label: string // Label name
|
||||
lastAccessed: number // Last access timestamp
|
||||
accessCount: number // Access count for sorting optimization
|
||||
}
|
||||
|
||||
export interface SearchHistoryData {
|
||||
items: SearchHistoryItem[]
|
||||
version: string // Data version for compatibility
|
||||
workspace?: string // Workspace isolation (if needed)
|
||||
}
|
||||
|
||||
export class SearchHistoryManager {
|
||||
private static readonly STORAGE_KEY = 'lightrag_search_history'
|
||||
private static readonly MAX_HISTORY = searchHistoryMaxItems
|
||||
private static readonly VERSION = searchHistoryVersion
|
||||
|
||||
/**
|
||||
* Get search history from localStorage
|
||||
* @returns Array of search history items sorted by last accessed time (descending)
|
||||
*/
|
||||
static getHistory(): SearchHistoryItem[] {
|
||||
try {
|
||||
const data = localStorage.getItem(this.STORAGE_KEY)
|
||||
if (!data) return []
|
||||
|
||||
const parsed: SearchHistoryData = JSON.parse(data)
|
||||
|
||||
// Version compatibility check
|
||||
if (parsed.version !== this.VERSION) {
|
||||
console.warn(`Search history version mismatch. Expected ${this.VERSION}, got ${parsed.version}. Clearing history.`)
|
||||
this.clearHistory()
|
||||
return []
|
||||
}
|
||||
|
||||
// Ensure items is an array
|
||||
if (!Array.isArray(parsed.items)) {
|
||||
console.warn('Invalid search history format. Clearing history.')
|
||||
this.clearHistory()
|
||||
return []
|
||||
}
|
||||
|
||||
// Sort by last accessed time (descending) then by access count (descending)
|
||||
return parsed.items.sort((a, b) => {
|
||||
if (b.lastAccessed !== a.lastAccessed) {
|
||||
return b.lastAccessed - a.lastAccessed
|
||||
}
|
||||
return (b.accessCount || 0) - (a.accessCount || 0)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error reading search history:', error)
|
||||
this.clearHistory()
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a label to search history (or update if exists)
|
||||
* @param label Label to add to history
|
||||
*/
|
||||
static addToHistory(label: string): void {
|
||||
if (!label || typeof label !== 'string' || label.trim() === '') {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const history = this.getHistory()
|
||||
const now = Date.now()
|
||||
const trimmedLabel = label.trim()
|
||||
|
||||
// Find existing item
|
||||
const existingIndex = history.findIndex(item => item.label === trimmedLabel)
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
// Update existing item
|
||||
const existingItem = history[existingIndex]
|
||||
existingItem.lastAccessed = now
|
||||
existingItem.accessCount = (existingItem.accessCount || 0) + 1
|
||||
|
||||
// Move to front (will be sorted properly when saved)
|
||||
history.splice(existingIndex, 1)
|
||||
history.unshift(existingItem)
|
||||
} else {
|
||||
// Add new item to the beginning
|
||||
history.unshift({
|
||||
label: trimmedLabel,
|
||||
lastAccessed: now,
|
||||
accessCount: 1
|
||||
})
|
||||
}
|
||||
|
||||
// Limit history size
|
||||
if (history.length > this.MAX_HISTORY) {
|
||||
history.splice(this.MAX_HISTORY)
|
||||
}
|
||||
|
||||
// Save to localStorage
|
||||
const data: SearchHistoryData = {
|
||||
items: history,
|
||||
version: this.VERSION
|
||||
}
|
||||
|
||||
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(data))
|
||||
} catch (error) {
|
||||
console.error('Error saving search history:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all search history
|
||||
*/
|
||||
static clearHistory(): void {
|
||||
try {
|
||||
localStorage.removeItem(this.STORAGE_KEY)
|
||||
} catch (error) {
|
||||
console.error('Error clearing search history:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize history with default popular labels if empty
|
||||
* @param popularLabels Array of popular labels to use as defaults
|
||||
*/
|
||||
static async initializeWithDefaults(popularLabels: string[]): Promise<void> {
|
||||
const history = this.getHistory()
|
||||
|
||||
if (history.length === 0 && popularLabels.length > 0) {
|
||||
try {
|
||||
const now = Date.now()
|
||||
const defaultItems: SearchHistoryItem[] = popularLabels.map((label, index) => ({
|
||||
label: label.trim(),
|
||||
lastAccessed: now - index, // Ensure proper ordering
|
||||
accessCount: 0 // Mark as default/popular items
|
||||
}))
|
||||
|
||||
const data: SearchHistoryData = {
|
||||
items: defaultItems,
|
||||
version: this.VERSION
|
||||
}
|
||||
|
||||
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(data))
|
||||
} catch (error) {
|
||||
console.error('Error initializing search history with defaults:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent searches (items with accessCount > 0)
|
||||
* @param limit Maximum number of recent searches to return
|
||||
* @returns Array of recent search items
|
||||
*/
|
||||
static getRecentSearches(limit: number = 10): SearchHistoryItem[] {
|
||||
const history = this.getHistory()
|
||||
return history
|
||||
.filter(item => item.accessCount > 0)
|
||||
.slice(0, limit)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get popular recommendations (items with accessCount = 0, i.e., defaults)
|
||||
* @param limit Maximum number of recommendations to return
|
||||
* @returns Array of popular recommendation items
|
||||
*/
|
||||
static getPopularRecommendations(limit?: number): SearchHistoryItem[] {
|
||||
const history = this.getHistory()
|
||||
const recommendations = history.filter(item => item.accessCount === 0)
|
||||
return limit ? recommendations.slice(0, limit) : recommendations
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all history items as simple string array
|
||||
* @param limit Maximum number of items to return
|
||||
* @returns Array of label strings
|
||||
*/
|
||||
static getHistoryLabels(limit?: number): string[] {
|
||||
const history = this.getHistory()
|
||||
const labels = history.map(item => item.label)
|
||||
return limit ? labels.slice(0, limit) : labels
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a label exists in history
|
||||
* @param label Label to check
|
||||
* @returns True if label exists in history
|
||||
*/
|
||||
static hasLabel(label: string): boolean {
|
||||
if (!label || typeof label !== 'string') return false
|
||||
const history = this.getHistory()
|
||||
return history.some(item => item.label === label.trim())
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a specific label from history
|
||||
* @param label Label to remove
|
||||
*/
|
||||
static removeLabel(label: string): void {
|
||||
if (!label || typeof label !== 'string') return
|
||||
|
||||
try {
|
||||
const history = this.getHistory()
|
||||
const trimmedLabel = label.trim()
|
||||
const filteredHistory = history.filter(item => item.label !== trimmedLabel)
|
||||
|
||||
if (filteredHistory.length !== history.length) {
|
||||
const data: SearchHistoryData = {
|
||||
items: filteredHistory,
|
||||
version: this.VERSION
|
||||
}
|
||||
|
||||
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(data))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error removing label from search history:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get storage statistics
|
||||
* @returns Object with history statistics
|
||||
*/
|
||||
static getStats(): {
|
||||
totalItems: number
|
||||
recentSearches: number
|
||||
popularRecommendations: number
|
||||
storageSize: number
|
||||
} {
|
||||
const history = this.getHistory()
|
||||
const recentCount = history.filter(item => item.accessCount > 0).length
|
||||
const popularCount = history.filter(item => item.accessCount === 0).length
|
||||
|
||||
let storageSize = 0
|
||||
try {
|
||||
const data = localStorage.getItem(this.STORAGE_KEY)
|
||||
storageSize = data ? data.length : 0
|
||||
} catch {
|
||||
// Ignore error
|
||||
}
|
||||
|
||||
return {
|
||||
totalItems: history.length,
|
||||
recentSearches: recentCount,
|
||||
popularRecommendations: popularCount,
|
||||
storageSize
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user