feat: add cache to localstore and clear options

This commit is contained in:
2025-12-10 12:21:17 -03:00
parent 4117c54d24
commit 37e3d4268b
4 changed files with 185 additions and 33 deletions
+16
View File
@@ -389,6 +389,22 @@
</div> </div>
<button type="submit" class="settings-btn">Save</button> <button type="submit" class="settings-btn">Save</button>
</form> </form>
<div class="date-display" style="margin-top: 40px;">AI Nutrition Cache</div>
<div class="cache-info-section">
<div class="cache-stats">
<div class="cache-stat-item">
<span class="cache-stat-label">Cached Items:</span>
<span class="cache-stat-value" id="cacheItemCount">0</span>
</div>
<div class="cache-stat-item">
<span class="cache-stat-label">Cache Size:</span>
<span class="cache-stat-value" id="cacheSize">0 KB</span>
</div>
</div>
<button type="button" id="clearCacheBtn" class="settings-btn clear-cache-btn">Clear Cache</button>
</div>
</div> </div>
</section> </section>
+47
View File
@@ -51,3 +51,50 @@
border: 1px solid #5A7BC4; border: 1px solid #5A7BC4;
transform: translateY(-1px); 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;
}
+61 -19
View File
@@ -15,37 +15,56 @@ export interface NutritionInfo {
notes: string; notes: string;
} }
// In-memory cache for nutrition info // localStorage-based cache for nutrition info
// Key format: "foodName_grams" (e.g., "apple_100", "chicken breast_150") // Key format: "nutrition_cache_foodName_grams" (e.g., "nutrition_cache_apple_100")
const nutritionCache = new Map<string, NutritionInfo>(); const CACHE_PREFIX = 'nutrition_cache_';
/** /**
* Generate cache key from food name and grams * Generate cache key from food name and grams
*/ */
function getCacheKey(foodName: string, grams: number): string { 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 * Get cached nutrition info if available
*/ */
function getCachedNutrition(foodName: string, grams: number): NutritionInfo | null { function getCachedNutrition(foodName: string, grams: number): NutritionInfo | null {
try {
const key = getCacheKey(foodName, grams); const key = getCacheKey(foodName, grams);
return nutritionCache.get(key) || null; 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 * Store nutrition info in cache
*/ */
function cacheNutrition(foodName: string, grams: number, info: NutritionInfo): void { function cacheNutrition(foodName: string, grams: number, info: NutritionInfo): void {
try {
const key = getCacheKey(foodName, grams); const key = getCacheKey(foodName, grams);
nutritionCache.set(key, info); localStorage.setItem(key, JSON.stringify(info));
console.log(`Cached nutrition info for: ${key} (total cached: ${nutritionCache.size})`); 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 * 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 * @param foodName - The name of the food item
* @returns Promise with nutritional information * @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) * Clear the nutrition info cache
* Useful for testing or if you want to force fresh API calls * Removes all cached nutrition data from localStorage
*/ */
export function clearNutritionCache(): void { export function clearNutritionCache(): void {
const size = nutritionCache.size; try {
nutritionCache.clear(); const keys = Object.keys(localStorage).filter(key => key.startsWith(CACHE_PREFIX));
console.log(`Cleared nutrition cache (${size} entries removed)`); 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[] } { export function getCacheStats(): { size: number; keys: string[]; sizeInBytes: number; sizeInKB: number } {
return { try {
size: nutritionCache.size, const keys = Object.keys(localStorage).filter(key => key.startsWith(CACHE_PREFIX));
keys: Array.from(nutritionCache.keys())
}; // 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 };
}
} }
+57 -10
View File
@@ -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 { 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 { showAuthForms, toggleAuthForms, showRegisterForm, showLoginForm, hideAuthForms, closeAuthModal } from './auth.ts';
import { appState } from "./state"; import { appState } from "./state";
import { getNutritionInfo, NutritionInfo } from './claudeService'; import { getNutritionInfo, NutritionInfo, clearNutritionCache, getCacheStats } from './claudeService';
// App state // App state
let selectedDate = new Date(); 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() { async function handleLogout() {
showLoading(); showLoading();
@@ -225,6 +264,10 @@ async function toggleSettingsView() {
getDivById('app-content').classList.add('hidden'); getDivById('app-content').classList.add('hidden');
getDivById('settings-content').classList.remove('hidden'); getDivById('settings-content').classList.remove('hidden');
// Update cache statistics display
updateCacheStats();
hideLoading(); hideLoading();
} catch (error) { } catch (error) {
hideLoading(); hideLoading();
@@ -407,14 +450,14 @@ const setupEventListeners = () => {
document.getElementById('registerForm')?.addEventListener('submit', handleRegister); document.getElementById('registerForm')?.addEventListener('submit', handleRegister);
// Desktop header buttons // Desktop header buttons
document.getElementById('logoutBtn')?.addEventListener('click', handleLogout); getButtonById('logoutBtn').addEventListener('click', handleLogout);
document.getElementById('settingsBtn')?.addEventListener('click', toggleSettingsView); getButtonById('settingsBtn').addEventListener('click', toggleSettingsView);
document.getElementById('calendarBtn')?.addEventListener('click', scrollToCalendarView); getButtonById('calendarBtn').addEventListener('click', scrollToCalendarView);
// Mobile header buttons // Mobile header buttons
document.getElementById('logoutBtnMobile')?.addEventListener('click', handleLogout); getButtonById('logoutBtnMobile').addEventListener('click', handleLogout);
document.getElementById('settingsBtnMobile')?.addEventListener('click', handleMobileSettingsClick); getButtonById('settingsBtnMobile').addEventListener('click', handleMobileSettingsClick);
document.getElementById('calendarBtnMobile')?.addEventListener('click', handleMobileCalendarClick); getButtonById('calendarBtnMobile').addEventListener('click', handleMobileCalendarClick);
// Mobile menu toggle // Mobile menu toggle
document.getElementById('mobileMenuToggle')?.addEventListener('click', toggleMobileMenu); document.getElementById('mobileMenuToggle')?.addEventListener('click', toggleMobileMenu);
@@ -422,10 +465,13 @@ const setupEventListeners = () => {
// Settings form // Settings form
document.getElementById('settingsForm')?.addEventListener('submit', handleSaveSettings); document.getElementById('settingsForm')?.addEventListener('submit', handleSaveSettings);
// Clear cache button
getButtonById('clearCacheBtn')?.addEventListener('click', handleClearCache);
// Share button event listeners // Share button event listeners
document.getElementById('shareBtn')?.addEventListener('click', handleShareClick); getButtonById('shareBtn').addEventListener('click', handleShareClick);
document.getElementById('shareBtnMobile')?.addEventListener('click', handleShareClick); getButtonById('shareBtnMobile').addEventListener('click', handleShareClick);
document.getElementById('close-share-modal')?.addEventListener('click', closeShareModal); getButtonById('close-share-modal').addEventListener('click', closeShareModal);
document.getElementById('copy-share-link')?.addEventListener('click', copyShareLink); document.getElementById('copy-share-link')?.addEventListener('click', copyShareLink);
// Landing page CTA button event listeners // Landing page CTA button event listeners
@@ -1025,6 +1071,7 @@ const addFood = async () => {
getInputById('foodSearchInput').value = ''; getInputById('foodSearchInput').value = '';
gramAmount.value = '100'; gramAmount.value = '100';
showFoodPreview(false); showFoodPreview(false);
hideAINutritionCard();
await delay(QUICK_DELAY); await delay(QUICK_DELAY);
renderCalendar(); renderCalendar();
} catch (error) { } catch (error) {