285 lines
9.4 KiB
JavaScript
285 lines
9.4 KiB
JavaScript
|
|
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);
|
||
|
|
});
|