File size: 7,801 Bytes
979853c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
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
    }
  }
}