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>
<button type="submit" class="settings-btn">Save</button>
</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>
</section>
+47
View File
@@ -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;
}
+63 -21
View File
@@ -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<string, NutritionInfo>();
// 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 };
}
}
+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 { 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) {