feat: add AI info
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
import Anthropic from '@anthropic-ai/sdk';
|
||||
|
||||
const API_KEY = import.meta.env.VITE_CLAUDE_API_KEY;
|
||||
|
||||
// Initialize Claude client
|
||||
const anthropic = new Anthropic({
|
||||
apiKey: API_KEY,
|
||||
dangerouslyAllowBrowser: true // Allow usage in browser (for development/demo purposes)
|
||||
});
|
||||
|
||||
export interface NutritionInfo {
|
||||
vitamins: string[];
|
||||
minerals: string[];
|
||||
benefits: string[];
|
||||
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>();
|
||||
|
||||
/**
|
||||
* Generate cache key from food name and grams
|
||||
*/
|
||||
function getCacheKey(foodName: string, grams: number): string {
|
||||
return `${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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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})`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param foodName - The name of the food item
|
||||
* @param grams - Amount in grams
|
||||
* @returns Promise with nutritional information
|
||||
*/
|
||||
export async function getNutritionInfo(foodName: string, grams: number): Promise<NutritionInfo> {
|
||||
// Check cache first
|
||||
const cached = getCachedNutrition(foodName, grams);
|
||||
if (cached) {
|
||||
console.log(`Using cached nutrition info for: ${foodName} (${grams}g)`);
|
||||
return cached;
|
||||
}
|
||||
|
||||
console.log(`Fetching nutrition info from AI for: ${foodName} (${grams}g)`);
|
||||
|
||||
try {
|
||||
const message = await anthropic.messages.create({
|
||||
model: 'claude-haiku-4-5-20251001',
|
||||
max_tokens: 1024,
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: `Provide detailed nutritional information for ${grams}g of ${foodName}.
|
||||
|
||||
IMPORTANT: Respond in Brazilian Portuguese (pt-BR).
|
||||
|
||||
Focus on:
|
||||
1. Key vitamins (e.g., A, B complex, C, D, E, K)
|
||||
2. Important minerals (e.g., iron, calcium, magnesium, zinc, potassium)
|
||||
3. Main health benefits
|
||||
4. Any important notes or considerations
|
||||
|
||||
Format your response as JSON with this structure:
|
||||
{
|
||||
"vitamins": ["Vitamina A: descrição", "Vitamina C: descrição", ...],
|
||||
"minerals": ["Ferro: descrição", "Cálcio: descrição", ...],
|
||||
"benefits": ["benefício 1", "benefício 2", ...],
|
||||
"notes": "Informações adicionais importantes"
|
||||
}
|
||||
|
||||
Be concise but informative. Only include significant amounts of vitamins and minerals. All text must be in Brazilian Portuguese.`
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// Extract the text content from Claude's response
|
||||
const textContent = message.content.find(block => block.type === 'text');
|
||||
if (!textContent || textContent.type !== 'text') {
|
||||
throw new Error('No text content in response');
|
||||
}
|
||||
|
||||
// Parse the JSON response
|
||||
const responseText = textContent.text;
|
||||
|
||||
// Extract JSON from the response (handle cases where Claude adds markdown formatting)
|
||||
const jsonMatch = responseText.match(/\{[\s\S]*\}/);
|
||||
if (!jsonMatch) {
|
||||
throw new Error('Could not parse JSON from response');
|
||||
}
|
||||
|
||||
const nutritionInfo: NutritionInfo = JSON.parse(jsonMatch[0]);
|
||||
|
||||
// Cache the result for future use
|
||||
cacheNutrition(foodName, grams, nutritionInfo);
|
||||
|
||||
return nutritionInfo;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error getting nutrition info from Claude:', error);
|
||||
throw new Error('Failed to get nutrition information. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the nutrition info cache (optional utility function)
|
||||
* Useful for testing or if you want to force fresh API calls
|
||||
*/
|
||||
export function clearNutritionCache(): void {
|
||||
const size = nutritionCache.size;
|
||||
nutritionCache.clear();
|
||||
console.log(`Cleared nutrition cache (${size} entries removed)`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache statistics
|
||||
*/
|
||||
export function getCacheStats(): { size: number; keys: string[] } {
|
||||
return {
|
||||
size: nutritionCache.size,
|
||||
keys: Array.from(nutritionCache.keys())
|
||||
};
|
||||
}
|
||||
+91
-4
@@ -6,6 +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';
|
||||
|
||||
// App state
|
||||
let selectedDate = new Date();
|
||||
@@ -278,15 +279,16 @@ function clearEditing() {
|
||||
getInputById('foodSearchInput').value = '';
|
||||
getInputById('gramAmount').value = '100';
|
||||
showFoodPreview(false);
|
||||
hideAINutritionCard();
|
||||
getDivById('add-foot-title').innerHTML = 'Add Food Entry';
|
||||
getButtonById('add-food-btn').innerHTML = 'Add Food';
|
||||
getButtonById('cancel-food-btn').style.display = 'none';
|
||||
|
||||
|
||||
// Clear edit-specific hidden inputs
|
||||
getInputById('foodIdToUpdate').value = '';
|
||||
getInputById('foodTimeToUpdate').value = '';
|
||||
getInputById('foodCaloriesToUpdate').value = '';
|
||||
|
||||
|
||||
selectedFood = null;
|
||||
}
|
||||
|
||||
@@ -361,11 +363,17 @@ const setupEventListeners = () => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
if (target.value === ''){
|
||||
showFoodPreview(false);
|
||||
hideAINutritionCard();
|
||||
}
|
||||
});
|
||||
|
||||
getInputById('gramAmount').addEventListener('change', () => {
|
||||
previewCalories();
|
||||
// Reload AI nutrition info with new amount if food is selected
|
||||
if (selectedFood) {
|
||||
const grams = parseFloat(getInputById('gramAmount').value) || 100;
|
||||
loadAINutritionInfo(selectedFood.name, grams);
|
||||
}
|
||||
});
|
||||
|
||||
getButtonById('add-food-btn').addEventListener('click', () => {
|
||||
@@ -556,13 +564,92 @@ function displaySearchResults(results: FoodItem[]) {
|
||||
appState.currentHighlightIndex = -1;
|
||||
}
|
||||
|
||||
// AI Nutrition Card Functions
|
||||
function showAINutritionCard() {
|
||||
const card = getDivById('ai-nutrition-card');
|
||||
card.classList.remove('display-none');
|
||||
card.classList.add('display-block');
|
||||
}
|
||||
|
||||
function hideAINutritionCard() {
|
||||
const card = getDivById('ai-nutrition-card');
|
||||
card.classList.add('display-none');
|
||||
card.classList.remove('display-block');
|
||||
}
|
||||
|
||||
function showAILoading() {
|
||||
getDivById('ai-nutrition-loading').classList.remove('hidden');
|
||||
getDivById('ai-nutrition-error').classList.add('hidden');
|
||||
getDivById('ai-nutrition-content').classList.add('hidden');
|
||||
}
|
||||
|
||||
function showAIError() {
|
||||
getDivById('ai-nutrition-loading').classList.add('hidden');
|
||||
getDivById('ai-nutrition-error').classList.remove('hidden');
|
||||
getDivById('ai-nutrition-content').classList.add('hidden');
|
||||
}
|
||||
|
||||
function showAIContent(nutritionInfo: NutritionInfo) {
|
||||
getDivById('ai-nutrition-loading').classList.add('hidden');
|
||||
getDivById('ai-nutrition-error').classList.add('hidden');
|
||||
getDivById('ai-nutrition-content').classList.remove('hidden');
|
||||
|
||||
// Populate vitamins
|
||||
const vitaminsList = document.getElementById('ai-vitamins-list');
|
||||
if (vitaminsList) {
|
||||
vitaminsList.innerHTML = nutritionInfo.vitamins.map(v => `<li>${v}</li>`).join('');
|
||||
}
|
||||
|
||||
// Populate minerals
|
||||
const mineralsList = document.getElementById('ai-minerals-list');
|
||||
if (mineralsList) {
|
||||
mineralsList.innerHTML = nutritionInfo.minerals.map(m => `<li>${m}</li>`).join('');
|
||||
}
|
||||
|
||||
// Populate benefits
|
||||
const benefitsList = document.getElementById('ai-benefits-list');
|
||||
if (benefitsList) {
|
||||
benefitsList.innerHTML = nutritionInfo.benefits.map(b => `<li>${b}</li>`).join('');
|
||||
}
|
||||
|
||||
// Populate notes
|
||||
const notesText = document.getElementById('ai-notes-text');
|
||||
const notesSection = document.getElementById('ai-notes-section');
|
||||
if (notesText && notesSection && nutritionInfo.notes) {
|
||||
notesText.textContent = nutritionInfo.notes;
|
||||
notesSection.classList.remove('hidden');
|
||||
} else if (notesSection) {
|
||||
notesSection.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAINutritionInfo(foodName: string, grams: number) {
|
||||
showAINutritionCard();
|
||||
showAILoading();
|
||||
|
||||
try {
|
||||
const nutritionInfo = await getNutritionInfo(foodName, grams);
|
||||
showAIContent(nutritionInfo);
|
||||
} catch (error) {
|
||||
console.error('Error loading AI nutrition info:', error);
|
||||
showAIError();
|
||||
}
|
||||
}
|
||||
|
||||
function selectFood(food: FoodItem) {
|
||||
selectedFood = food;
|
||||
|
||||
|
||||
getInputById('foodSearchInput').value = food.name;
|
||||
hideSearchResults();
|
||||
hideSearchResults();
|
||||
getInputById('gramAmount').focus();
|
||||
previewCalories(food);
|
||||
|
||||
// Show cancel button so user can clear selection
|
||||
getButtonById('cancel-food-btn').style.display = 'inline-block';
|
||||
|
||||
// Load AI nutrition info for the selected food
|
||||
const grams = parseFloat(getInputById('gramAmount').value) || 100;
|
||||
loadAINutritionInfo(food.name, grams);
|
||||
}
|
||||
|
||||
async function setFoodToEdit(foodId: string) {
|
||||
|
||||
Reference in New Issue
Block a user