import { useCallback, useEffect, useMemo, useState } from "react"; import { addSearchToHistory, getRecentSearches, historyDatabase, type ImageResults, type SearchEntry, type TextResults, } from "../modules/history"; import { addLogEntry } from "../modules/logEntries"; import { getSettings } from "../modules/pubSub"; import { groupSearchResultsByDate, searchWithFuzzy, } from "../modules/stringFormatters"; interface UseSearchHistoryOptions { limit?: number; threshold?: number; enableGrouping?: boolean; enablePagination?: boolean; pageSize?: number; } interface UseSearchHistoryReturn { recentSearches: SearchEntry[]; filteredSearches: SearchEntry[]; groupedSearches: Record; isLoading: boolean; error: string | null; currentPage: number; totalPages: number; hasNextPage: boolean; hasPreviousPage: boolean; retryLastOperation: () => Promise; clearError: () => void; searchHistory: (query: string) => void; addToHistory: ( query: string, results: TextResults | ImageResults, source?: "user" | "followup" | "suggestion", ) => Promise; togglePin: (searchId: number) => Promise; deleteEntry: (searchId: number) => Promise; clearAll: () => Promise; refreshHistory: () => Promise; nextPage: () => void; previousPage: () => void; goToPage: (page: number) => void; } export function useSearchHistory( options: UseSearchHistoryOptions = {}, ): UseSearchHistoryReturn { const { limit = 50, enableGrouping = true, enablePagination = false, pageSize = 20, } = options; const [recentSearches, setRecentSearches] = useState([]); const [filteredSearches, setFilteredSearches] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [currentQuery, setCurrentQuery] = useState(""); const [lastFailedOperation, setLastFailedOperation] = useState< (() => Promise) | null >(null); const [currentPage, setCurrentPage] = useState(0); const [allSearches, setAllSearches] = useState([]); const refreshHistory = useCallback(async () => { try { setIsLoading(true); setError(null); const searches = await getRecentSearches( enablePagination ? 1000 : limit * 2, ); if (enablePagination) { setAllSearches(searches); } else { setRecentSearches(searches); } if (currentQuery) { const filtered = searchWithFuzzy( searches, currentQuery, (search) => search.query, enablePagination ? searches.length : limit, ).map((result) => result.item); if (enablePagination) { const startIndex = currentPage * pageSize; const endIndex = startIndex + pageSize; setFilteredSearches(filtered.slice(startIndex, endIndex)); } else { setFilteredSearches(filtered.slice(0, limit)); } } else { if (enablePagination) { const startIndex = currentPage * pageSize; const endIndex = startIndex + pageSize; const pageResults = searches.slice(startIndex, endIndex); setRecentSearches(pageResults); setFilteredSearches(pageResults); } else { const results = searches.slice(0, limit); setRecentSearches(results); setFilteredSearches(results); } } } catch (err) { const errorMsg = `Failed to load search history: ${err}`; setError(errorMsg); addLogEntry(errorMsg); setLastFailedOperation(() => refreshHistory); } finally { setIsLoading(false); } }, [limit, currentQuery, enablePagination, pageSize, currentPage]); const searchHistory = useCallback( (query: string) => { setCurrentQuery(query); if (!query.trim()) { setFilteredSearches(recentSearches); return; } const results = searchWithFuzzy( recentSearches, query, (search) => search.query, limit, ).map((result) => result.item); setFilteredSearches(results); }, [recentSearches, limit], ); const addToHistory = useCallback( async ( query: string, results: TextResults | ImageResults, source: "user" | "followup" | "suggestion" = "user", ) => { try { await addSearchToHistory(query, results, source); await refreshHistory(); } catch (err) { const errorMsg = `Failed to add search to history: ${err}`; setError(errorMsg); addLogEntry(errorMsg); setLastFailedOperation( () => () => addToHistory(query, results, source), ); } }, [refreshHistory], ); const togglePin = useCallback( async (searchId: number) => { try { const search = await historyDatabase.searches.get(searchId); if (search) { await historyDatabase.searches.update(searchId, { isPinned: !search.isPinned, }); await refreshHistory(); addLogEntry( `${search.isPinned ? "Unpinned" : "Pinned"} search: ${search.query}`, ); } } catch (err) { const errorMsg = `Failed to toggle pin: ${err}`; setError(errorMsg); addLogEntry(errorMsg); setLastFailedOperation(() => () => togglePin(searchId)); } }, [refreshHistory], ); const deleteEntry = useCallback( async (searchId: number) => { try { const search = await historyDatabase.searches.get(searchId); await historyDatabase.searches.delete(searchId); await refreshHistory(); if (search) { addLogEntry(`Deleted search: ${search.query}`); } } catch (err) { const errorMsg = `Failed to delete search entry: ${err}`; setError(errorMsg); addLogEntry(errorMsg); setLastFailedOperation(() => () => deleteEntry(searchId)); } }, [refreshHistory], ); const clearAll = useCallback(async () => { try { await historyDatabase.searches.clear(); setRecentSearches([]); setFilteredSearches([]); addLogEntry("All search history cleared"); } catch (err) { const errorMsg = `Failed to clear history: ${err}`; setError(errorMsg); addLogEntry(errorMsg); setLastFailedOperation(() => () => clearAll()); } }, []); const groupedSearches = useMemo(() => { const globalSettings = getSettings(); if ( !enableGrouping || !globalSettings.historyGroupByDate || !filteredSearches.length ) { return {}; } const searchesWithTimestamp = filteredSearches.map((search) => ({ item: search, timestamp: search.timestamp, })); const grouped = groupSearchResultsByDate(searchesWithTimestamp); const result: Record = {}; for (const [key, value] of Object.entries(grouped)) { result[key] = value.map((item) => item.item); } return result; }, [filteredSearches, enableGrouping]); useEffect(() => { refreshHistory(); }, [refreshHistory]); useEffect(() => { const intervalId = setInterval(() => { if (!isLoading) { refreshHistory(); } }, 30000); return () => { clearInterval(intervalId); }; }, [refreshHistory, isLoading]); const retryLastOperation = useCallback(async () => { if (lastFailedOperation) { setError(null); try { await lastFailedOperation(); setLastFailedOperation(null); } catch (err) { const errorMsg = `Retry failed: ${err}`; setError(errorMsg); addLogEntry(errorMsg); } } }, [lastFailedOperation]); const clearError = useCallback(() => { setError(null); setLastFailedOperation(null); }, []); const nextPage = useCallback(() => { if (enablePagination) { setCurrentPage((prev) => prev + 1); } }, [enablePagination]); const previousPage = useCallback(() => { if (enablePagination && currentPage > 0) { setCurrentPage((prev) => prev - 1); } }, [enablePagination, currentPage]); const goToPage = useCallback( (page: number) => { if (enablePagination && page >= 0) { setCurrentPage(page); } }, [enablePagination], ); const dataSource = currentQuery ? filteredSearches : enablePagination ? allSearches : recentSearches; const totalPages = enablePagination ? Math.ceil(dataSource.length / pageSize) : 1; const hasNextPage = enablePagination && currentPage < totalPages - 1; const hasPreviousPage = enablePagination && currentPage > 0; return { recentSearches, filteredSearches, groupedSearches, isLoading, error, currentPage, totalPages, hasNextPage, hasPreviousPage, retryLastOperation, clearError, searchHistory, addToHistory, togglePin, deleteEntry, clearAll, refreshHistory, nextPage, previousPage, goToPage, }; }