feat: add cache to localstore and clear options
This commit is contained in:
+16
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
+59
-17
@@ -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 } {
|
||||||
|
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 {
|
return {
|
||||||
size: nutritionCache.size,
|
size: keys.length,
|
||||||
keys: Array.from(nutritionCache.keys())
|
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
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user