foodsnap/image-renderer/index.js

285 lines
9.4 KiB
JavaScript
Raw Permalink Normal View History

const express = require('express');
const puppeteer = require('puppeteer');
const cors = require('cors');
const app = express();
app.use(cors());
app.use(express.json({ limit: '10mb' }));
app.post('/api/render', async (req, res) => {
const { data } = req.body;
if (!data || !data.items) {
return res.status(400).json({ error: 'Missing analysis data' });
}
const { items, total, health_score, confidence, tip, insights } = data;
let scoreEmoji = '🟢';
if (health_score < 50) scoreEmoji = '🔴';
else if (health_score < 80) scoreEmoji = '🟡';
// Build items HTML
const itemsHtml = items.map(it => `
<div class="item-row">
<div>
<span class="item-name">${it.name || 'Desconhecido'}</span><br/>
<span class="item-portion">${it.portion || 'N/A'}</span>
</div>
<div class="item-calories">${Math.round(it.calories || 0)} kcal</div>
</div>
`).join('');
// Build insights HTML
let insightsHtml = '';
if (insights && insights.length > 0) {
insightsHtml = `
<div class="section-title">VEREDITO DA IA</div>
<ul class="insights-list">
${insights.map(v => `<li>${v}</li>`).join('')}
</ul>
`;
}
// Beautiful Instagram-like Card HTML
const html = `
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap');
body {
margin: 0;
padding: 0;
font-family: 'Inter', sans-serif;
background-color: transparent;
display: flex;
justify-content: center;
align-items: center;
width: 1080px;
height: 1080px;
}
.card {
width: 900px;
height: auto;
background: linear-gradient(145deg, #1f2937, #111827);
border-radius: 40px;
padding: 60px;
box-shadow: 0 40px 60px -15px rgba(0,0,0,0.5), inset 0 2px 4px rgba(255,255,255,0.1);
color: white;
border: 1px solid #374151;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 40px;
border-bottom: 2px solid #374151;
padding-bottom: 30px;
}
.brand {
font-size: 36px;
font-weight: 800;
background: linear-gradient(90deg, #34d399, #10b981);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.score-box {
background: rgba(255,255,255,0.05);
padding: 15px 30px;
border-radius: 20px;
border: 1px solid #374151;
font-size: 28px;
font-weight: 800;
display: flex;
align-items: center;
gap: 15px;
}
.main-stats {
display: flex;
gap: 30px;
margin-bottom: 40px;
}
.stat-box {
flex: 1;
background: rgba(16, 185, 129, 0.1);
border: 1px solid rgba(16, 185, 129, 0.2);
border-radius: 24px;
padding: 30px;
text-align: center;
}
.stat-box.calories {
background: rgba(239, 68, 68, 0.1);
border-color: rgba(239, 68, 68, 0.2);
}
.stat-value {
font-size: 48px;
font-weight: 800;
margin-bottom: 10px;
}
.stat-label {
font-size: 18px;
color: #9ca3af;
text-transform: uppercase;
letter-spacing: 2px;
font-weight: 600;
}
.macros {
display: flex;
gap: 20px;
margin-bottom: 40px;
}
.macro {
flex: 1;
background: rgba(255,255,255,0.03);
border-radius: 20px;
padding: 25px;
border: 1px solid #374151;
}
.macro-label {
color: #9ca3af;
font-size: 18px;
font-weight: 600;
margin-bottom: 5px;
}
.macro-value {
font-size: 32px;
font-weight: 800;
}
.blue-text { color: #60a5fa; }
.purple-text { color: #a78bfa; }
.yellow-text { color: #fbbf24; }
.section-title {
font-size: 20px;
color: #9ca3af;
text-transform: uppercase;
letter-spacing: 2px;
font-weight: 600;
margin-bottom: 20px;
border-bottom: 1px solid #374151;
padding-bottom: 10px;
}
.item-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
font-size: 24px;
}
.item-name { font-weight: 600; }
.item-portion { color: #9ca3af; font-size: 18px; }
.item-calories { font-weight: 800; color: #10b981; }
.insights-list {
list-style-type: none;
padding: 0;
margin: 0 0 40px 0;
}
.insights-list li {
font-size: 22px;
margin-bottom: 15px;
line-height: 1.5;
background: rgba(255,255,255,0.03);
padding: 15px 20px;
border-radius: 12px;
border: 1px solid #374151;
}
.tip-box {
margin-top: 30px;
background: rgba(59, 130, 246, 0.1);
border: 1px solid rgba(59, 130, 246, 0.2);
padding: 25px;
border-radius: 20px;
font-size: 22px;
line-height: 1.5;
}
.tip-title {
color: #60a5fa;
font-weight: 800;
margin-bottom: 10px;
text-transform: uppercase;
letter-spacing: 1px;
}
</style>
</head>
<body id="capture">
<div class="card">
<div class="header">
<div class="brand">FoodSnap AI</div>
<div class="score-box">
Score Nutricional: ${health_score || 0}/100 ${scoreEmoji}
</div>
</div>
<div class="main-stats">
<div class="stat-box calories">
<div class="stat-value text-red-400" style="color: #f87171;">${Math.round(total.calories || 0)}</div>
<div class="stat-label">Kcal</div>
</div>
<div class="stat-box">
<div class="stat-value text-emerald-400" style="color: #34d399;">${confidence === 'high' ? '98%' : '85%'}</div>
<div class="stat-label">Precisão IA</div>
</div>
</div>
<div class="macros">
<div class="macro">
<div class="macro-label">Proteínas</div>
<div class="macro-value blue-text">${total.protein || 0}g</div>
</div>
<div class="macro">
<div class="macro-label">Carboidratos</div>
<div class="macro-value purple-text">${total.carbs || 0}g</div>
</div>
<div class="macro">
<div class="macro-label">Gorduras</div>
<div class="macro-value yellow-text">${total.fat || 0}g</div>
</div>
</div>
${insightsHtml}
<div class="section-title">O QUE A IA IDENTIFICOU</div>
${itemsHtml}
${tip && tip.text ? `
<div class="tip-box">
<div class="tip-title">💡 Insight do Nutricionista IA</div>
${tip.text}
</div>
` : ''}
</div>
</body>
</html>
`;
try {
const browser = await puppeteer.launch({
args: ['--no-sandbox', '--disable-setuid-sandbox'],
headless: "new"
});
const page = await browser.newPage();
await page.setViewport({ width: 1080, height: 1080, deviceScaleFactor: 2 });
await page.setContent(html, { waitUntil: 'networkidle0' });
// Wait for fonts to load implicitly or network idle
const element = await page.$('#capture');
const buffer = await element.screenshot({ type: 'png' });
await browser.close();
res.set('Content-Type', 'image/png');
res.send(buffer);
} catch (err) {
console.error("Puppeteer render error:", err);
res.status(500).json({ error: 'Failed to generate image' });
}
});
const PORT = process.env.PORT || 3001;
app.listen(PORT, () => {
console.log("Image Renderer active on port", PORT);
});