diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 4327de1..33eec2f 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -22,6 +22,7 @@ jobs:
VITE_APPWRITE_USERSETTINGSID: ${{ secrets.VITE_APPWRITE_USERSETTINGSID }}
VITE_APPWRITE_MONTLYCALORIESID: ${{ secrets.VITE_APPWRITE_MONTLYCALORIESID }}
VITE_APPWRITE_SHAREDDAYSID: ${{ secrets.VITE_APPWRITE_SHAREDDAYSID }}
+ VITE_CLAUDE_API_KEY: ${{ secrets.VITE_CLAUDE_API_KEY }}
steps:
- name: Checkout
diff --git a/index.html b/index.html
index 79e5732..b88b1e3 100644
--- a/index.html
+++ b/index.html
@@ -290,6 +290,54 @@
+
+
+
+
+
+ AI
+ Detailed Nutrition Information
+
+
+
+
+
+
Analyzing nutritional content...
+
+
+
+
+
Unable to load detailed nutrition info. Please try again.
+
+
+
+
+
+
Food Log
diff --git a/package-lock.json b/package-lock.json
index 98b06b8..8952ebe 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,6 +9,7 @@
"version": "1.0.0",
"license": "ISC",
"dependencies": {
+ "@anthropic-ai/sdk": "^0.71.0",
"appwrite": "^21.5.0",
"sweetalert": "^2.1.2"
},
@@ -17,6 +18,35 @@
"vite": "^7.2.6"
}
},
+ "node_modules/@anthropic-ai/sdk": {
+ "version": "0.71.0",
+ "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.71.0.tgz",
+ "integrity": "sha512-go1XeWXmpxuiTkosSXpb8tokLk2ZLkIRcXpbWVwJM6gH5OBtHOVsfPfGuqI1oW7RRt4qc59EmYbrXRZ0Ng06Jw==",
+ "license": "MIT",
+ "dependencies": {
+ "json-schema-to-ts": "^3.1.1"
+ },
+ "bin": {
+ "anthropic-ai-sdk": "bin/cli"
+ },
+ "peerDependencies": {
+ "zod": "^3.25.0 || ^4.0.0"
+ },
+ "peerDependenciesMeta": {
+ "zod": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
+ "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.6",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz",
@@ -833,6 +863,19 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
+ "node_modules/json-schema-to-ts": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz",
+ "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.18.3",
+ "ts-algebra": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
@@ -984,6 +1027,12 @@
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
+ "node_modules/ts-algebra": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz",
+ "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==",
+ "license": "MIT"
+ },
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
diff --git a/package.json b/package.json
index 74f44e4..1314b38 100644
--- a/package.json
+++ b/package.json
@@ -16,6 +16,7 @@
"vite": "^7.2.6"
},
"dependencies": {
+ "@anthropic-ai/sdk": "^0.71.0",
"appwrite": "^21.5.0",
"sweetalert": "^2.1.2"
}
diff --git a/src/claudeService.ts b/src/claudeService.ts
new file mode 100644
index 0000000..d995cdc
--- /dev/null
+++ b/src/claudeService.ts
@@ -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();
+
+/**
+ * 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 {
+ // 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())
+ };
+}
diff --git a/src/index.ts b/src/index.ts
index 864ede4..422f637 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -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 => `${v}`).join('');
+ }
+
+ // Populate minerals
+ const mineralsList = document.getElementById('ai-minerals-list');
+ if (mineralsList) {
+ mineralsList.innerHTML = nutritionInfo.minerals.map(m => `${m}`).join('');
+ }
+
+ // Populate benefits
+ const benefitsList = document.getElementById('ai-benefits-list');
+ if (benefitsList) {
+ benefitsList.innerHTML = nutritionInfo.benefits.map(b => `${b}`).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) {
diff --git a/style.css b/style.css
index e06dd5d..67ebb2e 100644
--- a/style.css
+++ b/style.css
@@ -490,6 +490,7 @@ label {
font-weight: 600;
transition: all 0.3s ease;
height: fit-content;
+ display: none; /* Hidden by default, shown when editing */
}
.cancel-btn:hover {
@@ -901,3 +902,142 @@ label {
pointer-events: none;
cursor: not-allowed !important;
}
+
+/* AI Nutrition Info Card */
+#ai-nutrition-card {
+ margin-top: 20px;
+ animation: fadeIn 0.3s ease-in;
+}
+
+.ai-nutrition-container {
+ background: rgba(40, 40, 40, 0.6);
+ backdrop-filter: blur(10px);
+ border: 1px solid rgba(107, 141, 214, 0.3);
+ border-radius: 12px;
+ padding: 25px;
+}
+
+.ai-nutrition-title {
+ font-size: 1.2rem;
+ font-weight: 600;
+ color: white;
+ margin: 0 0 20px 0;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.ai-badge {
+ background: linear-gradient(135deg, #6B8DD6 0%, #9B7BC6 100%);
+ color: white;
+ padding: 4px 10px;
+ border-radius: 6px;
+ font-size: 0.75rem;
+ font-weight: 700;
+ letter-spacing: 0.5px;
+}
+
+/* Loading State */
+.ai-loading {
+ text-align: center;
+ padding: 30px 20px;
+}
+
+.ai-spinner {
+ width: 40px;
+ height: 40px;
+ margin: 0 auto 15px;
+ border: 3px solid rgba(107, 141, 214, 0.3);
+ border-top: 3px solid #6B8DD6;
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+}
+
+.ai-loading p {
+ color: rgba(255, 255, 255, 0.7);
+ font-size: 0.95rem;
+}
+
+/* Error State */
+.ai-error {
+ text-align: center;
+ padding: 20px;
+ color: rgba(255, 107, 107, 0.9);
+ font-size: 0.95rem;
+}
+
+/* Content */
+.ai-nutrition-content {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+.ai-nutrition-section {
+ background: rgba(30, 30, 30, 0.4);
+ border-radius: 8px;
+ padding: 15px;
+}
+
+.ai-section-title {
+ font-size: 1rem;
+ font-weight: 600;
+ color: #6B8DD6;
+ margin: 0 0 12px 0;
+ padding-bottom: 8px;
+ border-bottom: 1px solid rgba(107, 141, 214, 0.2);
+}
+
+.ai-list {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.ai-list li {
+ color: rgba(255, 255, 255, 0.85);
+ font-size: 0.9rem;
+ line-height: 1.5;
+ padding-left: 20px;
+ position: relative;
+}
+
+.ai-list li:before {
+ content: "•";
+ position: absolute;
+ left: 8px;
+ color: #6B8DD6;
+ font-weight: bold;
+}
+
+.ai-notes {
+ color: rgba(255, 255, 255, 0.85);
+ font-size: 0.9rem;
+ line-height: 1.6;
+ margin: 0;
+}
+
+/* Responsive adjustments */
+@media (max-width: 768px) {
+ .ai-nutrition-container {
+ padding: 20px;
+ }
+
+ .ai-nutrition-title {
+ font-size: 1.1rem;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 8px;
+ }
+
+ .ai-list li {
+ font-size: 0.85rem;
+ }
+
+ .ai-notes {
+ font-size: 0.85rem;
+ }
+}