feat: add AI info
This commit is contained in:
@@ -22,6 +22,7 @@ jobs:
|
|||||||
VITE_APPWRITE_USERSETTINGSID: ${{ secrets.VITE_APPWRITE_USERSETTINGSID }}
|
VITE_APPWRITE_USERSETTINGSID: ${{ secrets.VITE_APPWRITE_USERSETTINGSID }}
|
||||||
VITE_APPWRITE_MONTLYCALORIESID: ${{ secrets.VITE_APPWRITE_MONTLYCALORIESID }}
|
VITE_APPWRITE_MONTLYCALORIESID: ${{ secrets.VITE_APPWRITE_MONTLYCALORIESID }}
|
||||||
VITE_APPWRITE_SHAREDDAYSID: ${{ secrets.VITE_APPWRITE_SHAREDDAYSID }}
|
VITE_APPWRITE_SHAREDDAYSID: ${{ secrets.VITE_APPWRITE_SHAREDDAYSID }}
|
||||||
|
VITE_CLAUDE_API_KEY: ${{ secrets.VITE_CLAUDE_API_KEY }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
|
|||||||
+48
@@ -290,6 +290,54 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- AI Nutrition Info Card -->
|
||||||
|
<div class="display-none" id="ai-nutrition-card">
|
||||||
|
<div class="ai-nutrition-container">
|
||||||
|
<h3 class="ai-nutrition-title">
|
||||||
|
<span class="ai-badge">AI</span>
|
||||||
|
Detailed Nutrition Information
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<!-- Loading State -->
|
||||||
|
<div id="ai-nutrition-loading" class="ai-loading">
|
||||||
|
<div class="ai-spinner"></div>
|
||||||
|
<p>Analyzing nutritional content...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error State -->
|
||||||
|
<div id="ai-nutrition-error" class="ai-error hidden">
|
||||||
|
<p>Unable to load detailed nutrition info. Please try again.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<div id="ai-nutrition-content" class="ai-nutrition-content hidden">
|
||||||
|
<!-- Vitamins Section -->
|
||||||
|
<div class="ai-nutrition-section">
|
||||||
|
<h4 class="ai-section-title">Vitamins</h4>
|
||||||
|
<ul id="ai-vitamins-list" class="ai-list"></ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Minerals Section -->
|
||||||
|
<div class="ai-nutrition-section">
|
||||||
|
<h4 class="ai-section-title">Minerals</h4>
|
||||||
|
<ul id="ai-minerals-list" class="ai-list"></ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Benefits Section -->
|
||||||
|
<div class="ai-nutrition-section">
|
||||||
|
<h4 class="ai-section-title">Health Benefits</h4>
|
||||||
|
<ul id="ai-benefits-list" class="ai-list"></ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Notes Section -->
|
||||||
|
<div class="ai-nutrition-section" id="ai-notes-section">
|
||||||
|
<h4 class="ai-section-title">Notes</h4>
|
||||||
|
<p id="ai-notes-text" class="ai-notes"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section-title">Food Log</div>
|
<div class="section-title">Food Log</div>
|
||||||
|
|||||||
Generated
+49
@@ -9,6 +9,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@anthropic-ai/sdk": "^0.71.0",
|
||||||
"appwrite": "^21.5.0",
|
"appwrite": "^21.5.0",
|
||||||
"sweetalert": "^2.1.2"
|
"sweetalert": "^2.1.2"
|
||||||
},
|
},
|
||||||
@@ -17,6 +18,35 @@
|
|||||||
"vite": "^7.2.6"
|
"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": {
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
"version": "0.25.6",
|
"version": "0.25.6",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz",
|
"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": "^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": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.11",
|
"version": "3.3.11",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||||
@@ -984,6 +1027,12 @@
|
|||||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
"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": {
|
"node_modules/typescript": {
|
||||||
"version": "5.9.3",
|
"version": "5.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
"vite": "^7.2.6"
|
"vite": "^7.2.6"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@anthropic-ai/sdk": "^0.71.0",
|
||||||
"appwrite": "^21.5.0",
|
"appwrite": "^21.5.0",
|
||||||
"sweetalert": "^2.1.2"
|
"sweetalert": "^2.1.2"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { 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';
|
||||||
|
|
||||||
// App state
|
// App state
|
||||||
let selectedDate = new Date();
|
let selectedDate = new Date();
|
||||||
@@ -278,15 +279,16 @@ function clearEditing() {
|
|||||||
getInputById('foodSearchInput').value = '';
|
getInputById('foodSearchInput').value = '';
|
||||||
getInputById('gramAmount').value = '100';
|
getInputById('gramAmount').value = '100';
|
||||||
showFoodPreview(false);
|
showFoodPreview(false);
|
||||||
|
hideAINutritionCard();
|
||||||
getDivById('add-foot-title').innerHTML = 'Add Food Entry';
|
getDivById('add-foot-title').innerHTML = 'Add Food Entry';
|
||||||
getButtonById('add-food-btn').innerHTML = 'Add Food';
|
getButtonById('add-food-btn').innerHTML = 'Add Food';
|
||||||
getButtonById('cancel-food-btn').style.display = 'none';
|
getButtonById('cancel-food-btn').style.display = 'none';
|
||||||
|
|
||||||
// Clear edit-specific hidden inputs
|
// Clear edit-specific hidden inputs
|
||||||
getInputById('foodIdToUpdate').value = '';
|
getInputById('foodIdToUpdate').value = '';
|
||||||
getInputById('foodTimeToUpdate').value = '';
|
getInputById('foodTimeToUpdate').value = '';
|
||||||
getInputById('foodCaloriesToUpdate').value = '';
|
getInputById('foodCaloriesToUpdate').value = '';
|
||||||
|
|
||||||
selectedFood = null;
|
selectedFood = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -361,11 +363,17 @@ const setupEventListeners = () => {
|
|||||||
const target = e.target as HTMLInputElement;
|
const target = e.target as HTMLInputElement;
|
||||||
if (target.value === ''){
|
if (target.value === ''){
|
||||||
showFoodPreview(false);
|
showFoodPreview(false);
|
||||||
|
hideAINutritionCard();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
getInputById('gramAmount').addEventListener('change', () => {
|
getInputById('gramAmount').addEventListener('change', () => {
|
||||||
previewCalories();
|
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', () => {
|
getButtonById('add-food-btn').addEventListener('click', () => {
|
||||||
@@ -556,13 +564,92 @@ function displaySearchResults(results: FoodItem[]) {
|
|||||||
appState.currentHighlightIndex = -1;
|
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) {
|
function selectFood(food: FoodItem) {
|
||||||
selectedFood = food;
|
selectedFood = food;
|
||||||
|
|
||||||
getInputById('foodSearchInput').value = food.name;
|
getInputById('foodSearchInput').value = food.name;
|
||||||
hideSearchResults();
|
hideSearchResults();
|
||||||
getInputById('gramAmount').focus();
|
getInputById('gramAmount').focus();
|
||||||
previewCalories(food);
|
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) {
|
async function setFoodToEdit(foodId: string) {
|
||||||
|
|||||||
@@ -490,6 +490,7 @@ label {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
height: fit-content;
|
height: fit-content;
|
||||||
|
display: none; /* Hidden by default, shown when editing */
|
||||||
}
|
}
|
||||||
|
|
||||||
.cancel-btn:hover {
|
.cancel-btn:hover {
|
||||||
@@ -901,3 +902,142 @@ label {
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
cursor: not-allowed !important;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user