diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..51c5235 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +.next/ +venv/ +__pycache__/ +*.db +*.pyc +.env +dist/ diff --git a/backend/app/integrations/openai_client.py b/backend/app/integrations/openai_client.py index d7f398c..928d519 100644 --- a/backend/app/integrations/openai_client.py +++ b/backend/app/integrations/openai_client.py @@ -9,6 +9,17 @@ Responda SEMPRE em JSON válido com esta estrutura exata: "summary": "", "positives": ["", ...], "negatives": ["", ...], + "nutrition": { + "calorias": "", + "gordura_total": "", + "gordura_saturada": "", + "acucar": "", + "sodio": "", + "fibras": "", + "proteinas": "", + "carboidratos": "" + }, + "nutrition_verdict": "", "ingredients": [ { "name": "", @@ -17,9 +28,23 @@ Responda SEMPRE em JSON válido com esta estrutura exata: "classification": "", "reason": "" } - ] + ], + "recipe": { + "title": "", + "description": "", + "prep_time": "", + "calories": "", + "ingredients_list": ["", "", ...], + "steps": ["", "", ...], + "tip": "" + } } +Para a receita: +- Se o produto for SAUDÁVEL (score > 70): sugira uma receita usando o produto +- Se o produto for RUIM (score <= 70): sugira uma alternativa saudável que substitua o produto +- A receita deve ser simples, rápida e brasileira quando possível + Critérios para o score: - 90-100: Alimento natural, minimamente processado, sem aditivos - 70-89: Bom, com poucos aditivos ou processamento leve @@ -27,6 +52,7 @@ Critérios para o score: - 30-49: Ruim, ultraprocessado com vários aditivos - 0-29: Péssimo, alto em açúcar/sódio/gordura trans, muitos aditivos +Use os dados nutricionais fornecidos quando disponíveis. Estime quando não disponíveis. Considere Nutri-Score, classificação NOVA, e ingredientes problemáticos. Seja direto e honesto. Use linguagem simples.""" @@ -36,14 +62,18 @@ async def analyze_product(product_data: dict) -> dict: client = AsyncOpenAI(api_key=settings.OPENAI_API_KEY) + nutrition_info = product_data.get('nutrition', {}) + nutrition_str = json.dumps(nutrition_info, ensure_ascii=False) if nutrition_info else 'Não disponível' + user_msg = f"""Produto: {product_data.get('name', 'Desconhecido')} Marca: {product_data.get('brand', '')} Categoria: {product_data.get('category', '')} Ingredientes: {product_data.get('ingredients_text', 'Não disponível')} Nutri-Score: {product_data.get('nutri_score', 'N/A')} NOVA: {product_data.get('nova_group', 'N/A')} +Dados Nutricionais: {nutrition_str} -Analise este produto.""" +Analise este produto com informações nutricionais detalhadas e sugira uma receita.""" try: resp = await client.chat.completions.create( @@ -54,7 +84,7 @@ Analise este produto.""" ], response_format={"type": "json_object"}, temperature=0.3, - timeout=15, + timeout=30, ) return json.loads(resp.choices[0].message.content) except Exception as e: @@ -82,5 +112,8 @@ def _mock_analysis(product_data: dict) -> dict: "summary": f"Análise baseada em regras para {product_data.get('name', 'este produto')}. Configure OPENAI_API_KEY para análise completa com IA.", "positives": ["Dados nutricionais disponíveis"], "negatives": ["Análise IA indisponível - usando fallback"], - "ingredients": [] + "nutrition": {}, + "nutrition_verdict": "", + "ingredients": [], + "recipe": None } diff --git a/backend/app/models/product.py b/backend/app/models/product.py index 3402ef3..be3bfd1 100644 --- a/backend/app/models/product.py +++ b/backend/app/models/product.py @@ -14,4 +14,4 @@ class Product(Base): nova_group = Column(Integer) nutrition_json = Column(Text) # JSON string image_url = Column(String) - updated_at = Column(DateTime, default=lambda: datetime.now(timezone.utc)) + updated_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)) diff --git a/backend/app/models/scan.py b/backend/app/models/scan.py index 54e98dc..f202910 100644 --- a/backend/app/models/scan.py +++ b/backend/app/models/scan.py @@ -12,4 +12,4 @@ class Scan(Base): score = Column(Integer) summary = Column(Text) analysis_json = Column(Text) # Full AI analysis as JSON - scanned_at = Column(DateTime, default=lambda: datetime.now(timezone.utc)) + scanned_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)) diff --git a/backend/app/models/user.py b/backend/app/models/user.py index 49da7fa..efc9599 100644 --- a/backend/app/models/user.py +++ b/backend/app/models/user.py @@ -9,4 +9,4 @@ class User(Base): name = Column(String, nullable=False) password_hash = Column(String, nullable=False) is_premium = Column(Boolean, default=False) - created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc)) + created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)) diff --git a/backend/app/routers/scan.py b/backend/app/routers/scan.py index fb95f99..0ac0bf6 100644 --- a/backend/app/routers/scan.py +++ b/backend/app/routers/scan.py @@ -91,6 +91,9 @@ async def scan_product(req: ScanRequest, user: User = Depends(get_current_user), ingredients=analysis.get("ingredients", []), nutri_score=product_data.get("nutri_score"), nova_group=product_data.get("nova_group"), + nutrition=analysis.get("nutrition"), + nutrition_verdict=analysis.get("nutrition_verdict"), + recipe=analysis.get("recipe"), source=source, ) @@ -104,3 +107,67 @@ async def get_history(user: User = Depends(get_current_user), db: AsyncSession = id=s.id, barcode=s.barcode, product_name=s.product_name, brand=s.brand, score=s.score, scanned_at=s.scanned_at ) for s in scans] + +@router.get("/history/{scan_id}") +async def get_scan_detail(scan_id: int, user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)): + result = await db.execute( + select(Scan).where(Scan.id == scan_id, Scan.user_id == user.id) + ) + scan = result.scalar_one_or_none() + if not scan: + raise HTTPException(status_code=404, detail="Scan não encontrado") + + analysis = json.loads(scan.analysis_json or '{}') + # Also get product info + prod_result = await db.execute(select(Product).where(Product.barcode == scan.barcode)) + product = prod_result.scalar_one_or_none() + + return { + "id": scan.id, + "barcode": scan.barcode, + "product_name": scan.product_name, + "brand": scan.brand, + "score": scan.score, + "summary": scan.summary, + "scanned_at": scan.scanned_at.isoformat() if scan.scanned_at else None, + "category": product.category if product else None, + "image_url": product.image_url if product else None, + "nutri_score": product.nutri_score if product else None, + "nova_group": product.nova_group if product else None, + "positives": analysis.get("positives", []), + "negatives": analysis.get("negatives", []), + "ingredients": analysis.get("ingredients", []), + } + +@router.get("/history/{scan_id}") +async def get_scan_detail(scan_id: int, user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)): + result = await db.execute( + select(Scan).where(Scan.id == scan_id, Scan.user_id == user.id) + ) + scan = result.scalar_one_or_none() + if not scan: + raise HTTPException(status_code=404, detail="Scan não encontrado") + + analysis = json.loads(scan.analysis_json or '{}') + prod_result = await db.execute(select(Product).where(Product.barcode == scan.barcode)) + product = prod_result.scalar_one_or_none() + + return { + "id": scan.id, + "barcode": scan.barcode, + "product_name": scan.product_name, + "brand": scan.brand, + "score": scan.score, + "summary": scan.summary, + "scanned_at": scan.scanned_at.isoformat() if scan.scanned_at else None, + "category": product.category if product else None, + "image_url": product.image_url if product else None, + "nutri_score": product.nutri_score if product else None, + "nova_group": product.nova_group if product else None, + "positives": analysis.get("positives", []), + "negatives": analysis.get("negatives", []), + "ingredients": analysis.get("ingredients", []), + "nutrition": analysis.get("nutrition", {}), + "nutrition_verdict": analysis.get("nutrition_verdict", ""), + "recipe": analysis.get("recipe"), + } diff --git a/backend/app/schemas/scan.py b/backend/app/schemas/scan.py index 557b877..eb3144b 100644 --- a/backend/app/schemas/scan.py +++ b/backend/app/schemas/scan.py @@ -1,5 +1,5 @@ from pydantic import BaseModel -from typing import Optional, List +from typing import Optional, List, Any, Dict from datetime import datetime class ScanRequest(BaseModel): @@ -9,9 +9,18 @@ class IngredientAnalysis(BaseModel): name: str popular_name: Optional[str] = None explanation: str - classification: str # "good", "warning", "bad" + classification: str reason: str +class RecipeInfo(BaseModel): + title: Optional[str] = None + description: Optional[str] = None + prep_time: Optional[str] = None + calories: Optional[str] = None + ingredients_list: Optional[List[str]] = None + steps: Optional[List[str]] = None + tip: Optional[str] = None + class ScanResult(BaseModel): barcode: str product_name: Optional[str] = None @@ -23,6 +32,9 @@ class ScanResult(BaseModel): positives: List[str] negatives: List[str] ingredients: List[IngredientAnalysis] + nutrition: Optional[Dict[str, Any]] = None + nutrition_verdict: Optional[str] = None + recipe: Optional[RecipeInfo] = None nutri_score: Optional[str] = None nova_group: Optional[int] = None source: str = "open_food_facts" diff --git a/frontend/public/manifest.json b/frontend/public/manifest.json index 7fcc5dd..d51de2c 100644 --- a/frontend/public/manifest.json +++ b/frontend/public/manifest.json @@ -3,22 +3,35 @@ "short_name": "ALETHEIA", "description": "Escaneie rótulos de alimentos com IA", "start_url": "/", + "scope": "/", "display": "standalone", "orientation": "portrait", "theme_color": "#1A7A4C", - "background_color": "#1A7A4C", + "background_color": "#0A0A0F", "icons": [ { "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png", - "purpose": "any maskable" + "purpose": "any" }, { "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png", - "purpose": "any maskable" + "purpose": "any" + }, + { + "src": "/icons/icon-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "/icons/icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" } ] } diff --git a/frontend/public/sw.js b/frontend/public/sw.js index 9b0a1ee..d252fc4 100644 --- a/frontend/public/sw.js +++ b/frontend/public/sw.js @@ -1,4 +1,4 @@ -const CACHE_NAME = 'aletheia-v1'; +const CACHE_NAME = 'aletheia-v4'; const STATIC_ASSETS = ['/', '/manifest.json', '/icons/icon-192.png', '/icons/icon-512.png']; self.addEventListener('install', (event) => { @@ -19,6 +19,7 @@ self.addEventListener('activate', (event) => { self.addEventListener('fetch', (event) => { if (event.request.method !== 'GET') return; + if (event.request.url.includes('/api/')) return; event.respondWith( fetch(event.request) .then((response) => { diff --git a/frontend/src/app/history/page.tsx b/frontend/src/app/history/page.tsx index 960f8df..c60226b 100644 --- a/frontend/src/app/history/page.tsx +++ b/frontend/src/app/history/page.tsx @@ -8,6 +8,8 @@ import { useAuthStore } from '@/stores/authStore'; export default function HistoryPage() { const [scans, setScans] = useState([]); const [loading, setLoading] = useState(true); + const [detail, setDetail] = useState(null); + const [detailLoading, setDetailLoading] = useState(false); const { hydrate } = useAuthStore(); const router = useRouter(); @@ -17,7 +19,197 @@ export default function HistoryPage() { api.history().then(setScans).catch(() => {}).finally(() => setLoading(false)); }, []); - const getScoreColor = (s: number) => s >= 71 ? 'text-green-400' : s >= 51 ? 'text-yellow-400' : s >= 31 ? 'text-orange-400' : 'text-red-400'; + const openDetail = async (id: number) => { + setDetailLoading(true); + try { + const data = await api.scanDetail(id); + setDetail(data); + } catch { } + setDetailLoading(false); + }; + + const getScoreLabel = (s: number) => { + if (s >= 90) return { label: 'Excelente', emoji: '🌟', desc: 'Alimento natural e saudável' }; + if (s >= 70) return { label: 'Bom', emoji: '✅', desc: 'Saudável, com poucos aditivos' }; + if (s >= 50) return { label: 'Regular', emoji: '⚠️', desc: 'Processado, consumir com moderação' }; + if (s >= 30) return { label: 'Ruim', emoji: '🔶', desc: 'Ultraprocessado, vários aditivos' }; + return { label: 'Péssimo', emoji: '🚫', desc: 'Muito prejudicial à saúde' }; + }; + + const getScoreColor = (s: number) => s >= 71 ? '#10B981' : s >= 51 ? '#EAB308' : s >= 31 ? '#F97316' : '#EF4444'; + const getScoreClass = (s: number) => s >= 71 ? 'text-green-400' : s >= 51 ? 'text-yellow-400' : s >= 31 ? 'text-orange-400' : 'text-red-400'; + const getClassIcon = (c: string) => c === 'good' ? '🟢' : c === 'warning' ? '🟡' : '🔴'; + const getClassColor = (c: string) => c === 'good' ? 'text-green-400' : c === 'warning' ? 'text-yellow-400' : 'text-red-400'; + + const getNutritionBar = (label: string, value: string, level: string) => { + const barColor = level === 'low' ? 'bg-green-500' : level === 'mid' ? 'bg-yellow-500' : 'bg-red-500'; + const pillColor = level === 'low' ? 'text-green-400 bg-green-500/10' : level === 'mid' ? 'text-yellow-400 bg-yellow-500/10' : 'text-red-400 bg-red-500/10'; + const pillText = level === 'low' ? 'Baixo' : level === 'mid' ? 'Médio' : 'Alto'; + const width = level === 'low' ? '25%' : level === 'mid' ? '55%' : '85%'; + return ( +
+
+ {label} +
+ {value} + {pillText} +
+
+
+
+
+
+ ); + }; + + const guessLevel = (nutrient: string, val: string) => { + const num = parseFloat(val) || 0; + if (nutrient === 'acucar') return num > 15 ? 'high' : num > 5 ? 'mid' : 'low'; + if (nutrient === 'gordura_total' || nutrient === 'gordura_saturada') return num > 10 ? 'high' : num > 3 ? 'mid' : 'low'; + if (nutrient === 'sodio') return num > 400 ? 'high' : num > 120 ? 'mid' : 'low'; + if (nutrient === 'fibras') return num > 5 ? 'low' : num > 2 ? 'mid' : 'high'; + if (nutrient === 'proteinas') return num > 10 ? 'low' : num > 3 ? 'mid' : 'high'; + if (nutrient === 'calorias') return num > 300 ? 'high' : num > 150 ? 'mid' : 'low'; + return 'mid'; + }; + + // Detail view + if (detail) { + const color = getScoreColor(detail.score); + const dashArray = detail.score * 3.267 + ' 326.7'; + const nutrition = detail.nutrition || {}; + const recipe = detail.recipe; + + return ( +
+ + +
+

{detail.product_name || 'Produto'}

+ {detail.brand &&

{detail.brand}

} + {detail.category &&

{detail.category}

} +
+ + + + +
+ {detail.score} + /100 +
+
+
+ {getScoreLabel(detail.score).emoji} + {getScoreLabel(detail.score).label} +
+
+ {detail.nutri_score && detail.nutri_score !== 'unknown' && Nutri-Score: {detail.nutri_score}} + {detail.nova_group && NOVA: {detail.nova_group}} +
+
+ +
+

+ {getScoreLabel(detail.score).emoji} Por que é {getScoreLabel(detail.score).label}? +

+

{detail.summary}

+

{getScoreLabel(detail.score).desc}

+
+ + {/* Nutrition */} + {Object.keys(nutrition).length > 0 && ( +
+

📊 Informações Nutricionais

+ {detail.nutrition_verdict &&

{detail.nutrition_verdict}

} + {nutrition.calorias && getNutritionBar('Calorias', nutrition.calorias, guessLevel('calorias', nutrition.calorias))} + {nutrition.acucar && getNutritionBar('Açúcar', nutrition.acucar, guessLevel('acucar', nutrition.acucar))} + {nutrition.gordura_total && getNutritionBar('Gordura Total', nutrition.gordura_total, guessLevel('gordura_total', nutrition.gordura_total))} + {nutrition.gordura_saturada && getNutritionBar('Gordura Saturada', nutrition.gordura_saturada, guessLevel('gordura_saturada', nutrition.gordura_saturada))} + {nutrition.sodio && getNutritionBar('Sódio', nutrition.sodio, guessLevel('sodio', nutrition.sodio))} + {nutrition.carboidratos && getNutritionBar('Carboidratos', nutrition.carboidratos, guessLevel('carboidratos', nutrition.carboidratos))} + {nutrition.fibras && getNutritionBar('Fibras', nutrition.fibras, guessLevel('fibras', nutrition.fibras))} + {nutrition.proteinas && getNutritionBar('Proteínas', nutrition.proteinas, guessLevel('proteinas', nutrition.proteinas))} +
+ )} + + {/* Positives & Negatives */} +
+ {detail.positives?.length > 0 && ( +
+

✅ Positivos

+ {detail.positives.map((p: string, i: number) => ( +

• {p}

+ ))} +
+ )} + {detail.negatives?.length > 0 && ( +
+

❌ Negativos

+ {detail.negatives.map((n: string, i: number) => ( +

• {n}

+ ))} +
+ )} +
+ + {/* Ingredients */} + {detail.ingredients?.length > 0 && ( +
+

📋 Ingredientes

+
+ {detail.ingredients.map((ing: any, i: number) => ( +
+
+ {getClassIcon(ing.classification)} + + {ing.name}{ing.popular_name && ing.popular_name !== ing.name ? ' (' + ing.popular_name + ')' : ''} + +
+

{ing.explanation}

+

{ing.reason}

+
+ ))} +
+
+ )} + + {/* Recipe */} + {recipe && ( +
+

🍳 {detail.score > 70 ? 'Receita com este produto' : 'Alternativa Saudável'}

+

{recipe.title}

+

{recipe.description}

+
+ {recipe.prep_time && ⏱ {recipe.prep_time}} + {recipe.calories && 🔥 {recipe.calories}} +
+
+

Ingredientes:

+ {recipe.ingredients_list?.map((ing: string, i: number) => ( +

• {ing}

+ ))} +
+
+

Preparo:

+ {recipe.steps?.map((step: string, i: number) => ( +

{i + 1}. {step}

+ ))} +
+ {recipe.tip && ( +
+

💡 {recipe.tip}

+
+ )} +
+ )} + +

+ Escaneado em {new Date(detail.scanned_at).toLocaleString('pt-BR')} +

+
+ ); + } return (
@@ -37,17 +229,30 @@ export default function HistoryPage() { ) : (
{scans.map(s => ( -
+
+
+ {s.score} + +
+ ))}
)} + + {detailLoading && ( +
+
+
👁️
+

Carregando detalhes...

+
+
+ )}
); } diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index c1ed2e3..d4d8965 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -6,7 +6,7 @@ import { ServiceWorkerRegister } from '@/components/ServiceWorkerRegister' export const metadata: Metadata = { title: 'ALETHEIA — A verdade sobre o que você come', description: 'Escaneie qualquer produto e nossa IA revela o que a indústria alimentícia esconde nos rótulos.', - manifest: '/manifest.json', + // manifest moved to head link appleWebApp: { capable: true, statusBarStyle: 'black-translucent', @@ -28,7 +28,7 @@ export default function RootLayout({ children }: { children: React.ReactNode }) return ( - + {children} diff --git a/frontend/src/app/scan/page.tsx b/frontend/src/app/scan/page.tsx index f7ce626..767582e 100644 --- a/frontend/src/app/scan/page.tsx +++ b/frontend/src/app/scan/page.tsx @@ -64,6 +64,14 @@ export default function ScanPage() { } }; + const getScoreLabel = (s: number) => { + if (s >= 90) return { label: 'Excelente', emoji: '🌟', desc: 'Alimento natural e saudável' }; + if (s >= 70) return { label: 'Bom', emoji: '✅', desc: 'Saudável, com poucos aditivos' }; + if (s >= 50) return { label: 'Regular', emoji: '⚠️', desc: 'Processado, consumir com moderação' }; + if (s >= 30) return { label: 'Ruim', emoji: '🔶', desc: 'Ultraprocessado, vários aditivos' }; + return { label: 'Péssimo', emoji: '🚫', desc: 'Muito prejudicial à saúde' }; + }; + const getScoreColor = (score: number) => { if (score >= 71) return '#10B981'; if (score >= 51) return '#EAB308'; @@ -83,64 +91,134 @@ export default function ScanPage() { return '🔴'; }; + const getNutritionBar = (label: string, value: string, level: string) => { + const barColor = level === 'low' ? 'bg-green-500' : level === 'mid' ? 'bg-yellow-500' : 'bg-red-500'; + const pillColor = level === 'low' ? 'text-green-400 bg-green-500/10' : level === 'mid' ? 'text-yellow-400 bg-yellow-500/10' : 'text-red-400 bg-red-500/10'; + const pillText = level === 'low' ? 'Baixo' : level === 'mid' ? 'Médio' : 'Alto'; + const width = level === 'low' ? '25%' : level === 'mid' ? '55%' : '85%'; + return ( +
+
+ {label} +
+ {value} + {pillText} +
+
+
+
+
+
+ ); + }; + + const guessLevel = (nutrient: string, val: string) => { + const num = parseFloat(val) || 0; + if (nutrient === 'acucar') return num > 15 ? 'high' : num > 5 ? 'mid' : 'low'; + if (nutrient === 'gordura_total' || nutrient === 'gordura_saturada') return num > 10 ? 'high' : num > 3 ? 'mid' : 'low'; + if (nutrient === 'sodio') return num > 400 ? 'high' : num > 120 ? 'mid' : 'low'; + if (nutrient === 'fibras') return num > 5 ? 'low' : num > 2 ? 'mid' : 'high'; // inverted: more fiber = better + if (nutrient === 'proteinas') return num > 10 ? 'low' : num > 3 ? 'mid' : 'high'; // inverted + if (nutrient === 'calorias') return num > 300 ? 'high' : num > 150 ? 'mid' : 'low'; + return 'mid'; + }; + // Result view if (result) { const color = getScoreColor(result.score); + const dashArray = result.score * 3.267 + ' 326.7'; + const nutrition = result.nutrition || {}; + const recipe = result.recipe; + return (
- + {/* Score */} -
+

{result.product_name || 'Produto'}

{result.brand &&

{result.brand}

} -
+
+ strokeDasharray={dashArray} strokeLinecap="round" className="transition-all duration-1000" />
{result.score} - /100 + /100
+
+ {getScoreLabel(result.score).emoji} + {getScoreLabel(result.score).label} +
+ {result.nutri_score && result.nutri_score !== 'unknown' && ( +
+ Nutri-Score: {result.nutri_score} + {result.nova_group && NOVA: {result.nova_group}} +
+ )}
- {/* Summary */} + {/* Why this score */}
+

+ {getScoreLabel(result.score).emoji} Por que é {getScoreLabel(result.score).label}? +

{result.summary}

+

{getScoreLabel(result.score).desc}

+ {/* Nutrition Table */} + {Object.keys(nutrition).length > 0 && ( +
+

📊 Informações Nutricionais

+ {result.nutrition_verdict && ( +

{result.nutrition_verdict}

+ )} + {nutrition.calorias && getNutritionBar('Calorias', nutrition.calorias, guessLevel('calorias', nutrition.calorias))} + {nutrition.acucar && getNutritionBar('Açúcar', nutrition.acucar, guessLevel('acucar', nutrition.acucar))} + {nutrition.gordura_total && getNutritionBar('Gordura Total', nutrition.gordura_total, guessLevel('gordura_total', nutrition.gordura_total))} + {nutrition.gordura_saturada && getNutritionBar('Gordura Saturada', nutrition.gordura_saturada, guessLevel('gordura_saturada', nutrition.gordura_saturada))} + {nutrition.sodio && getNutritionBar('Sódio', nutrition.sodio, guessLevel('sodio', nutrition.sodio))} + {nutrition.carboidratos && getNutritionBar('Carboidratos', nutrition.carboidratos, guessLevel('carboidratos', nutrition.carboidratos))} + {nutrition.fibras && getNutritionBar('Fibras', nutrition.fibras, guessLevel('fibras', nutrition.fibras))} + {nutrition.proteinas && getNutritionBar('Proteínas', nutrition.proteinas, guessLevel('proteinas', nutrition.proteinas))} +
+ )} + {/* Positives & Negatives */} - {result.positives?.length > 0 && ( -
-

✅ Positivos

- {result.positives.map((p: string, i: number) => ( -

• {p}

- ))} -
- )} - {result.negatives?.length > 0 && ( -
-

❌ Negativos

- {result.negatives.map((n: string, i: number) => ( -

• {n}

- ))} -
- )} +
+ {result.positives?.length > 0 && ( +
+

✅ Positivos

+ {result.positives.map((p: string, i: number) => ( +

• {p}

+ ))} +
+ )} + {result.negatives?.length > 0 && ( +
+

❌ Negativos

+ {result.negatives.map((n: string, i: number) => ( +

• {n}

+ ))} +
+ )} +
{/* Ingredients */} {result.ingredients?.length > 0 && ( -
-

📋 Ingredientes

+
+

📋 Ingredientes

{result.ingredients.map((ing: any, i: number) => (
{getClassIcon(ing.classification)} - - {ing.name}{ing.popular_name && ing.popular_name !== ing.name ? ` (${ing.popular_name})` : ''} + + {ing.name}{ing.popular_name && ing.popular_name !== ing.name ? ' (' + ing.popular_name + ')' : ''}

{ing.explanation}

@@ -151,11 +229,53 @@ export default function ScanPage() {
)} - {/* Share */} + {/* Recipe */} + {recipe && ( +
+

🍳 {result.score > 70 ? 'Receita com este produto' : 'Alternativa Saudável'}

+

{recipe.title}

+

{recipe.description}

+
+ {recipe.prep_time && ⏱ {recipe.prep_time}} + {recipe.calories && 🔥 {recipe.calories}} +
+
+

Ingredientes:

+ {recipe.ingredients_list?.map((ing: string, i: number) => ( +

• {ing}

+ ))} +
+
+

Preparo:

+ {recipe.steps?.map((step: string, i: number) => ( +

{i + 1}. {step}

+ ))} +
+ {recipe.tip && ( +
+

💡 {recipe.tip}

+
+ )} +
+ )} + + {/* Score Legend */} +
+

📏 O que significa o Score?

+
+
🌟
90-100 Excelente
+
70-89 Bom
+
⚠️
50-69 Regular
+
🔶
30-49 Ruim
+
🚫
0-29 Péssimo
+
+
+ + {/* Actions */}