diff --git a/index.html b/index.html index b88b1e3..352ee4d 100644 --- a/index.html +++ b/index.html @@ -363,9 +363,9 @@ diff --git a/settings.css b/settings.css index 930879e..1baef4c 100644 --- a/settings.css +++ b/settings.css @@ -51,3 +51,50 @@ border: 1px solid #5A7BC4; transform: translateY(-1px); } + +.cache-info-section { + backdrop-filter: blur(10px); + border-radius: 10px; + padding: 25px; + min-width: 300px; + background: rgba(40, 40, 40, 0.9); + border: 1px solid rgba(100, 100, 100, 0.3); + margin-top: 20px; +} + +.cache-stats { + display: flex; + flex-direction: column; + gap: 15px; + margin-bottom: 20px; +} + +.cache-stat-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px; + background: rgba(60, 60, 60, 0.5); + border-radius: 6px; +} + +.cache-stat-label { + color: #b0b0b0; + font-size: 0.95rem; +} + +.cache-stat-value { + color: #e0e0e0; + font-size: 1.1rem; + font-weight: 600; +} + +.clear-cache-btn { + background: #d66b6b; + border: 1px solid #d66b6b; +} + +.clear-cache-btn:hover { + background: #c45a5a; + border: 1px solid #c45a5a; +} diff --git a/src/claudeService.ts b/src/claudeService.ts index 7cb0592..0e5fbdd 100644 --- a/src/claudeService.ts +++ b/src/claudeService.ts @@ -15,37 +15,56 @@ export interface NutritionInfo { notes: string; } -// In-memory cache for nutrition info -// Key format: "foodName_grams" (e.g., "apple_100", "chicken breast_150") -const nutritionCache = new Map(); +// localStorage-based cache for nutrition info +// Key format: "nutrition_cache_foodName_grams" (e.g., "nutrition_cache_apple_100") +const CACHE_PREFIX = 'nutrition_cache_'; /** * Generate cache key from food name and grams */ function getCacheKey(foodName: string, grams: number): string { - return `${foodName.toLowerCase().trim()}_${grams}`; + return `${CACHE_PREFIX}${foodName.toLowerCase().trim()}_${grams}`; } /** * Get cached nutrition info if available */ function getCachedNutrition(foodName: string, grams: number): NutritionInfo | null { - const key = getCacheKey(foodName, grams); - return nutritionCache.get(key) || null; + try { + const key = getCacheKey(foodName, grams); + const cached = localStorage.getItem(key); + if (cached) { + return JSON.parse(cached) as NutritionInfo; + } + return null; + } catch (error) { + console.error('Error reading from cache:', error); + return null; + } } /** * Store nutrition info in cache */ function cacheNutrition(foodName: string, grams: number, info: NutritionInfo): void { - const key = getCacheKey(foodName, grams); - nutritionCache.set(key, info); - console.log(`Cached nutrition info for: ${key} (total cached: ${nutritionCache.size})`); + try { + const key = getCacheKey(foodName, grams); + localStorage.setItem(key, JSON.stringify(info)); + const stats = getCacheStats(); + console.log(`Cached nutrition info for: ${key} (total cached: ${stats.size})`); + } catch (error) { + console.error('Error writing to cache:', error); + // If localStorage is full, try to clear some space + if (error instanceof Error && error.name === 'QuotaExceededError') { + console.warn('localStorage quota exceeded, clearing old cache entries'); + clearNutritionCache(); + } + } } /** * Get detailed nutritional information for a food item using Claude AI - * Uses in-memory cache to avoid repeated API calls for the same food/amount + * Uses localStorage cache to avoid repeated API calls for the same food/amount * @param foodName - The name of the food item * @returns Promise with nutritional information */ @@ -117,21 +136,44 @@ Be concise but informative. Only include significant amounts of vitamins and min } /** - * Clear the nutrition info cache (optional utility function) - * Useful for testing or if you want to force fresh API calls + * Clear the nutrition info cache + * Removes all cached nutrition data from localStorage */ export function clearNutritionCache(): void { - const size = nutritionCache.size; - nutritionCache.clear(); - console.log(`Cleared nutrition cache (${size} entries removed)`); + try { + const keys = Object.keys(localStorage).filter(key => key.startsWith(CACHE_PREFIX)); + keys.forEach(key => localStorage.removeItem(key)); + console.log(`Cleared nutrition cache (${keys.length} entries removed)`); + } catch (error) { + console.error('Error clearing cache:', error); + } } /** - * Get cache statistics + * Get cache statistics including size and storage usage */ -export function getCacheStats(): { size: number; keys: string[] } { - return { - size: nutritionCache.size, - keys: Array.from(nutritionCache.keys()) - }; +export function getCacheStats(): { size: number; keys: string[]; sizeInBytes: number; sizeInKB: number } { + try { + const keys = Object.keys(localStorage).filter(key => key.startsWith(CACHE_PREFIX)); + + // Calculate total size in bytes + let totalBytes = 0; + keys.forEach(key => { + const value = localStorage.getItem(key); + if (value) { + // Each character in localStorage is stored as UTF-16, which is 2 bytes per character + totalBytes += (key.length + value.length) * 2; + } + }); + + return { + size: keys.length, + keys: keys.map(k => k.replace(CACHE_PREFIX, '')), + sizeInBytes: totalBytes, + sizeInKB: Math.round(totalBytes / 1024 * 100) / 100 + }; + } catch (error) { + console.error('Error getting cache stats:', error); + return { size: 0, keys: [], sizeInBytes: 0, sizeInKB: 0 }; + } } diff --git a/src/index.ts b/src/index.ts index 9b017c8..0fd2f44 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,7 +6,7 @@ import swal from 'sweetalert'; import { closeMobileMenu, delay, getCleanName, getIcon, handleMobileCalendarClick, hideLoading, hideSearchResults, navigateResultsKeyboard, QUICK_DELAY, scrollToCalendarView, showLoading, toggleCardHandler, toggleMobileMenu } from './Utils.ts'; import { showAuthForms, toggleAuthForms, showRegisterForm, showLoginForm, hideAuthForms, closeAuthModal } from './auth.ts'; import { appState } from "./state"; -import { getNutritionInfo, NutritionInfo } from './claudeService'; +import { getNutritionInfo, NutritionInfo, clearNutritionCache, getCacheStats } from './claudeService'; // App state let selectedDate = new Date(); @@ -178,6 +178,45 @@ async function handleSaveSettings(e: SubmitEvent) { } } +function updateCacheStats() { + try { + const stats = getCacheStats(); + const cacheItemCountEl = document.getElementById('cacheItemCount'); + const cacheSizeEl = document.getElementById('cacheSize'); + + if (cacheItemCountEl) { + cacheItemCountEl.textContent = stats.size.toString(); + } + + if (cacheSizeEl) { + cacheSizeEl.textContent = `${stats.sizeInKB} KB`; + } + } catch (error) { + console.error('Error updating cache stats:', error); + } +} + +async function handleClearCache() { + try { + const result = await swal({ + title: 'Clear Cache?', + text: 'This will remove all cached nutrition data. You may need to fetch this data again from the AI.', + icon: 'warning', + buttons: ['Cancel', 'Clear Cache'], + dangerMode: true, + }); + + if (result) { + clearNutritionCache(); + updateCacheStats(); + swal('Success', 'Cache cleared successfully!', 'success'); + } + } catch (error) { + console.error('Error clearing cache:', error); + swal('Error', 'Failed to clear cache', 'error'); + } +} + async function handleLogout() { showLoading(); @@ -225,6 +264,10 @@ async function toggleSettingsView() { getDivById('app-content').classList.add('hidden'); getDivById('settings-content').classList.remove('hidden'); + + // Update cache statistics display + updateCacheStats(); + hideLoading(); } catch (error) { hideLoading(); @@ -407,14 +450,14 @@ const setupEventListeners = () => { document.getElementById('registerForm')?.addEventListener('submit', handleRegister); // Desktop header buttons - document.getElementById('logoutBtn')?.addEventListener('click', handleLogout); - document.getElementById('settingsBtn')?.addEventListener('click', toggleSettingsView); - document.getElementById('calendarBtn')?.addEventListener('click', scrollToCalendarView); + getButtonById('logoutBtn').addEventListener('click', handleLogout); + getButtonById('settingsBtn').addEventListener('click', toggleSettingsView); + getButtonById('calendarBtn').addEventListener('click', scrollToCalendarView); // Mobile header buttons - document.getElementById('logoutBtnMobile')?.addEventListener('click', handleLogout); - document.getElementById('settingsBtnMobile')?.addEventListener('click', handleMobileSettingsClick); - document.getElementById('calendarBtnMobile')?.addEventListener('click', handleMobileCalendarClick); + getButtonById('logoutBtnMobile').addEventListener('click', handleLogout); + getButtonById('settingsBtnMobile').addEventListener('click', handleMobileSettingsClick); + getButtonById('calendarBtnMobile').addEventListener('click', handleMobileCalendarClick); // Mobile menu toggle document.getElementById('mobileMenuToggle')?.addEventListener('click', toggleMobileMenu); @@ -422,10 +465,13 @@ const setupEventListeners = () => { // Settings form document.getElementById('settingsForm')?.addEventListener('submit', handleSaveSettings); + // Clear cache button + getButtonById('clearCacheBtn')?.addEventListener('click', handleClearCache); + // Share button event listeners - document.getElementById('shareBtn')?.addEventListener('click', handleShareClick); - document.getElementById('shareBtnMobile')?.addEventListener('click', handleShareClick); - document.getElementById('close-share-modal')?.addEventListener('click', closeShareModal); + getButtonById('shareBtn').addEventListener('click', handleShareClick); + getButtonById('shareBtnMobile').addEventListener('click', handleShareClick); + getButtonById('close-share-modal').addEventListener('click', closeShareModal); document.getElementById('copy-share-link')?.addEventListener('click', copyShareLink); // Landing page CTA button event listeners @@ -1025,6 +1071,7 @@ const addFood = async () => { getInputById('foodSearchInput').value = ''; gramAmount.value = '100'; showFoodPreview(false); + hideAINutritionCard(); await delay(QUICK_DELAY); renderCalendar(); } catch (error) {