Shaikhsarib commited on
Commit
07651ef
Β·
verified Β·
1 Parent(s): 2433156

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +910 -141
index.html CHANGED
@@ -9,6 +9,7 @@
9
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
10
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
11
  <script src="https://cdn.jsdelivr.net/npm/tesseract.js@5/dist/tesseract.min.js"></script>
 
12
  <style>
13
  :root {
14
  --forest: #1E3A2F; --moss: #2D5241; --sage: #4A7C59;
@@ -587,6 +588,262 @@
587
  .severity-pill.severe { background:#FEE2E2; color:#991B1B; }
588
  .severity-pill.moderate { background:#FEF3C7; color:#92400E; }
589
  .severity-pill.mild { background:#ECFDF5; color:#065F46; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
590
  </style>
591
  </head>
592
  <body>
@@ -595,9 +852,6 @@
595
  <div class="header">
596
  <div class="logo">Eat<em>lytic</em></div>
597
  <div class="header-right">
598
- <div id="quota-pill" style="display:none;align-items:center;gap:4px;background:rgba(255,255,255,0.12);border:1px solid rgba(255,255,255,0.2);color:var(--cream);padding:5px 11px;border-radius:50px;font-size:0.72rem;font-weight:600;">
599
- 🌿 <span id="quota-count">5</span> left
600
- </div>
601
  <button class="hdr-btn" onclick="openLang()"><i class="fas fa-globe"></i> <span id="lang-display">EN</span></button>
602
  </div>
603
  </div>
@@ -700,6 +954,9 @@
700
  <p>Tap or drag photo</p>
701
  </div>
702
  </div>
 
 
 
703
  </div>
704
 
705
  <!-- Preview -->
@@ -717,6 +974,7 @@
717
  <div class="state-tip" id="state-tip"></div>
718
  </div>
719
  </div>
 
720
  <button class="btn-primary" id="analyze-main-btn" onclick="analyze()" style="display:none">
721
  <i class="fas fa-microscope" style="margin-right:8px"></i>
722
  <span id="analyze-btn-text">Analyze Label</span>
@@ -815,6 +1073,56 @@
815
  </div>
816
  </div>
817
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
818
  <!-- LOADER -->
819
  <div id="loader">
820
  <div class="loader-ring"></div>
@@ -946,6 +1254,8 @@ window.onload = () => {
946
  currentPersona = savedPersona;
947
  renderLangOptions();
948
  updateProfileStats();
 
 
949
  };
950
  // ===== TABS =====
951
  function switchTab(tab) {
@@ -1181,6 +1491,26 @@ function handleBlurNotice(blurInfo) {
1181
  }
1182
  notice.classList.add('visible');
1183
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1184
  async function analyze() {
1185
  if (!blob) return;
1186
  hideError();
@@ -1217,8 +1547,9 @@ async function analyze() {
1217
  if (!res.ok) throw new Error(`Server error ${res.status}: ${res.statusText}`);
1218
  const data = await res.json();
1219
  if (data.error === 'quota_exceeded') {
1220
- showContextualError('generic','🌿','Daily Limit Reached',
1221
- data.message || `You've used all ${data.quota?.limit ?? 5} free scans today. Come back tomorrow or upgrade to Pro.`);
 
1222
  return;
1223
  }
1224
  if (data.error === 'image_quality') {
@@ -1241,10 +1572,9 @@ async function analyze() {
1241
  return;
1242
  }
1243
  if (data.error) throw new Error(data.error);
1244
- // Update quota pill display
1245
- if (data.quota) updateQuotaPill(data.quota);
1246
  // Show blur notice if blur was detected
1247
  handleBlurNotice(data.blur_info);
 
1248
  lastResult = data;
1249
  renderReport(data);
1250
  if (saveHistory) addToHistory(data);
@@ -1312,108 +1642,119 @@ function renderReport(d) {
1312
  });
1313
  }
1314
  function buildBody(d) {
1315
- // ── Safely extract all arrays with fallbacks ──────────────────
1316
  const nutrients = Array.isArray(d.nutrient_breakdown) ? d.nutrient_breakdown : [];
1317
  const pros = Array.isArray(d.pros) ? d.pros : [];
1318
  const cons = Array.isArray(d.cons) ? d.cons : [];
1319
  const ageW = Array.isArray(d.age_warnings) ? d.age_warnings : [];
1320
  const warnings = Array.isArray(d.health_warnings) ? d.health_warnings : [];
1321
- const allergens = Array.isArray(d.allergens) ? d.allergens : [];
1322
  const contras = Array.isArray(d.contraindications) ? d.contraindications : [];
 
1323
  const ingList = Array.isArray(d.ingredients_list) ? d.ingredients_list : [];
1324
  const spotlight = Array.isArray(d.ingredients_spotlight) ? d.ingredients_spotlight : [];
1325
  let html = '';
1326
 
1327
- // ── 0a. Blur info banner ──────────────────────────────────────
1328
  const bi = d.blur_info;
1329
  if (bi && bi.detected) {
1330
  if (bi.deblurred && bi.image_b64) {
1331
  html += `<div class="r-section" style="padding:14px">
1332
  <div class="deblur-report-badge">πŸ”§ Blur Corrected β€” Enhanced Image Used</div>
1333
- <div class="deblur-img-wrap"><img src="${bi.image_b64}" alt="Enhanced" loading="lazy"></div>
 
 
1334
  <div style="font-size:0.76rem;color:var(--mist);line-height:1.5">
1335
- Image was <strong>${escHtml(bi.severity)}ly blurry</strong> (score ${bi.score}/100)
1336
- and enhanced before OCR.
 
1337
  ${bi.method_log ? `<br><span style="font-family:monospace;font-size:0.68rem;color:#A16207">Pipeline: ${escHtml(bi.method_log)}</span>` : ''}
1338
- </div></div>`;
 
1339
  } else {
1340
  html += `<div class="r-section" style="padding:14px;background:#FFFBEB;border:1.5px solid #FCD34D;border-radius:var(--radius-sm)">
1341
  <div class="deblur-report-badge">⚠️ Blur Detected</div>
1342
  <div style="font-size:0.76rem;color:#78350F;line-height:1.5">
1343
- Blur detected (severity: <strong>${escHtml(bi.severity)}</strong>, score ${bi.score}/100).
 
1344
  Retake with better lighting for higher accuracy.
1345
- </div></div>`;
 
1346
  }
1347
  }
1348
 
1349
- // ── 0b. Low-confidence notice ─────────────────────────────────
1350
  if (d.is_low_confidence) {
1351
  html += `<div class="r-section" style="padding:12px;background:#FFF7ED;border:1.5px solid #FDBA74;border-radius:var(--radius-sm)">
1352
  <div style="font-size:0.8rem;color:#92400E;font-weight:600">⚠️ Partial Label β€” Lower Confidence</div>
1353
- <div style="font-size:0.75rem;color:#92400E;margin-top:4px">
1354
- Only partial label text was readable. Retake with a clearer photo for full accuracy.
1355
- </div></div>`;
1356
  }
1357
 
1358
- // ── 1. Health Warnings β€” most critical, shown first ───────────
1359
  if (warnings.length) {
1360
- const sevOrder = {critical:0, moderate:1, low:2};
1361
- const sorted = [...warnings].sort((a,b)=>(sevOrder[a.severity]??2)-(sevOrder[b.severity]??2));
1362
- const S = {
1363
- critical:{bg:'#FEF2F2',border:'#F87171',icon:'🚨',color:'#991B1B',label:'CRITICAL'},
1364
- moderate:{bg:'#FFFBEB',border:'#FCD34D',icon:'⚠️', color:'#92400E',label:'MODERATE'},
1365
- low: {bg:'#F0F9FF',border:'#7DD3FC',icon:'ℹ️', color:'#075985',label:'NOTE'},
1366
  };
1367
- html += `<div class="r-section"><div class="r-title">Health Warnings</div>
1368
- ${sorted.map(w => {
1369
- const s = S[w.severity] || S.low;
1370
- const tags = Array.isArray(w.affected_groups) && w.affected_groups.length
1371
- ? `<div style="margin-top:5px;display:flex;flex-wrap:wrap;gap:4px">
1372
- ${w.affected_groups.map(g=>`<span style="background:${s.border}33;color:${s.color};font-size:0.64rem;font-weight:700;padding:2px 8px;border-radius:50px">${escHtml(g)}</span>`).join('')}
1373
- </div>` : '';
1374
- return `<div style="background:${s.bg};border-left:4px solid ${s.border};border-radius:6px;padding:10px 12px;margin-bottom:8px">
1375
- <div style="display:flex;align-items:center;gap:6px;margin-bottom:3px">
1376
- <span>${s.icon}</span>
1377
- <span style="font-size:0.67rem;font-weight:800;color:${s.color};letter-spacing:0.05em">${s.label}</span>
1378
- <span style="font-size:0.7rem;color:var(--mist)">${escHtml(w.category||'')}</span>
1379
- </div>
1380
- <div style="font-size:0.8rem;line-height:1.5;color:${s.color}">${escHtml(w.warning)}</div>
1381
- ${tags}</div>`;
1382
- }).join('')}</div>`;
 
 
 
1383
  }
1384
 
1385
- // ── 2. Allergens ──────────────────────────────────────────────
1386
  if (allergens.length) {
1387
- html += `<div class="r-section"><div class="r-title">Allergen Information</div>
1388
- <div style="display:flex;flex-direction:column;gap:6px">
1389
- ${allergens.map(a=>`<div style="background:#FEF2F2;border-left:3px solid #F87171;border-radius:5px;padding:7px 12px;font-size:0.8rem;color:#991B1B;font-weight:500">⚠️ ${escHtml(a)}</div>`).join('')}
1390
- </div></div>`;
 
 
1391
  }
1392
 
1393
- // ── 3. Contraindications ──────────────────────────────────────
1394
  if (contras.length) {
1395
- html += `<div class="r-section"><div class="r-title">Not Suitable For</div>
1396
- <div style="display:flex;flex-direction:column;gap:6px">
1397
- ${contras.map(c=>`<div style="background:#FFF7ED;border-left:3px solid #FB923C;border-radius:5px;padding:7px 12px;font-size:0.8rem;color:#92400E;font-weight:500">🚫 ${escHtml(c)}</div>`).join('')}
1398
- </div></div>`;
 
 
1399
  }
1400
 
1401
- // ── 4. Simple Explanation ─────────────────────────────────────
1402
  if (d.eli5_explanation) {
1403
- html += `<div class="r-section"><div class="r-title">Simple Explanation</div>
1404
- <div class="eli5-box"><span class="eli5-label">🍭 Easy Read</span>${escHtml(d.eli5_explanation)}</div>
 
 
 
 
1405
  </div>`;
1406
  }
1407
 
1408
- // ── 5. Nutritional Breakdown ──────────────────────────────────
1409
  if (nutrients.length) {
1410
- const MAXES = {g:50, mg:2000, '%':100, kcal:600, kj:2500};
1411
- // Serving-size badge tells the user which reference the numbers use
1412
- const badge = d.serving_size
1413
- ? `<div style="display:inline-flex;align-items:center;gap:6px;background:#EAF5EE;border:1.5px solid #7FB896;border-radius:8px;padding:5px 12px;margin-bottom:12px;font-size:0.75rem;font-weight:700;color:#2D5241;">πŸ“ Values shown per ${escHtml(d.serving_size)}</div>`
1414
- : '';
1415
- html += `<div class="r-section"><div class="r-title">Nutritional Breakdown</div>
1416
- ${badge}
1417
  <div class="nutrient-legend">
1418
  <div class="n-leg"><div class="n-dot" style="background:#2D7A4F"></div>Good</div>
1419
  <div class="n-leg"><div class="n-dot" style="background:#C9A84C"></div>Moderate</div>
@@ -1421,19 +1762,14 @@ function buildBody(d) {
1421
  <div class="n-leg"><div class="n-dot" style="background:#8B0000"></div>Bad</div>
1422
  </div>
1423
  ${nutrients.map(n => {
1424
- const r = n.rating || 'moderate';
1425
- // FIX: safe parse strips unit chars accidentally included in value
1426
- // e.g. LLM returns "34.0g" β†’ parseFloat("34.0g") = NaN β†’ bars disappear
1427
- const numVal = parseFloat(String(n.value).replace(/[^0-9.]/g, ''));
1428
- const unit = (n.unit || '').toLowerCase().trim();
1429
- const max = MAXES[unit] || 100;
1430
- const pct = isNaN(numVal) ? 0 : Math.min(100, (numVal / max) * 100);
1431
- const disp = isNaN(numVal) ? escHtml(String(n.value)) : numVal;
1432
  return `<div class="nutrient-item">
1433
  <div class="nutrient-row">
1434
  <div class="n-name">${escHtml(n.name)}</div>
1435
  <div class="n-bar-wrap"><div class="n-bar bar-${r}" data-w="${pct.toFixed(1)}"></div></div>
1436
- <div class="n-val">${disp}${escHtml(n.unit||'')}</div>
1437
  </div>
1438
  ${n.impact ? `<div class="n-impact">${escHtml(n.impact)}</div>` : ''}
1439
  </div>`;
@@ -1441,90 +1777,121 @@ function buildBody(d) {
1441
  </div>`;
1442
  }
1443
 
1444
- // ── 6. Pros & Cons ────────────────────────────────────────────
1445
  if (pros.length || cons.length) {
1446
- html += `<div class="r-section"><div class="r-title">Pros &amp; Cons</div>
 
1447
  <div class="pc-grid">
1448
  <div>
1449
  <div class="pc-label p">Benefits</div>
1450
- ${pros.map(p=>`<div class="pc-item"><div class="pc-dot p"></div><span>${escHtml(p)}</span></div>`).join('')}
1451
  </div>
1452
  <div>
1453
  <div class="pc-label c">Watch Out</div>
1454
- ${cons.map(c=>`<div class="pc-item"><div class="pc-dot c"></div><span>${escHtml(c)}</span></div>`).join('')}
1455
  </div>
1456
- </div></div>`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1457
  }
1458
 
1459
- // ── 7. Product Analysis paragraphs ────────────────────────────
1460
  if (d.paragraph_benefits || d.paragraph_uniqueness) {
1461
- html += `<div class="r-section"><div class="r-title">Product Analysis</div>
 
1462
  ${d.paragraph_benefits ? `<p class="para-text">${escHtml(d.paragraph_benefits)}</p>` : ''}
1463
  ${d.paragraph_uniqueness ? `<p class="para-text">${escHtml(d.paragraph_uniqueness)}</p>` : ''}
1464
  </div>`;
1465
  }
1466
 
1467
- // ── 8. Age Group Guidance ─────────────────────────────────────
1468
  if (ageW.length) {
1469
- html += `<div class="r-section"><div class="r-title">By Age Group</div>
 
1470
  <div class="age-grid">
1471
- ${ageW.map(w=>`
1472
- <div class="age-card ${w.status||'caution'}">
1473
- <div class="age-emoji">${w.emoji||'πŸ‘€'}</div>
1474
  <div class="age-grp">${escHtml(w.group)}</div>
1475
  <div class="age-msg">${escHtml(w.message)}</div>
1476
  </div>`).join('')}
1477
- </div></div>`;
1478
- }
1479
-
1480
- // ── 9. Ingredient Spotlight ───────────────────────────────────
1481
- if (spotlight.length) {
1482
- html += `<div class="r-section"><div class="r-title">Ingredient Spotlight</div>
1483
- <div style="display:flex;flex-direction:column;gap:10px">
1484
- ${spotlight.map(s=>`
1485
- <div style="background:var(--parchment);border-radius:8px;padding:10px 14px">
1486
- <div style="font-size:0.85rem;font-weight:700;color:var(--forest);margin-bottom:2px">${escHtml(s.name||'')}</div>
1487
- <div style="font-size:0.67rem;font-weight:700;color:var(--mist);text-transform:uppercase;letter-spacing:0.05em;margin-bottom:5px">${escHtml(s.role||'')}</div>
1488
- <div style="font-size:0.8rem;line-height:1.5;color:var(--ink)">${escHtml(s.curiosity||'')}</div>
1489
- </div>`).join('')}
1490
- </div></div>`;
1491
  }
1492
 
1493
- // ── 10. Full Ingredients List ─────────────────────────────────
1494
- if (ingList.length) {
1495
- html += `<div class="r-section"><div class="r-title">Ingredients (as printed)</div>
1496
- <div style="font-size:0.78rem;line-height:1.8;color:var(--ink);background:var(--parchment);padding:10px 14px;border-radius:8px">
1497
- ${ingList.map((ing,i)=>`<span style="color:var(--mist);font-size:0.68rem;font-weight:700">${i+1}.</span>&nbsp;${escHtml(ing)}`).join('&nbsp;&middot;&nbsp;')}
1498
- </div></div>`;
 
1499
  }
1500
 
1501
- // ── 11. Molecular Insight ─────────────────────────────────────
1502
  if (d.molecular_insight) {
1503
- html += `<div class="r-section"><div class="r-title">Molecular Insight</div>
 
1504
  <div class="mol-box">πŸ”¬ ${escHtml(d.molecular_insight)}</div>
1505
  </div>`;
1506
  }
1507
 
1508
- // ── 12. Healthy Consumption Guide ─────────────────────────────
1509
- const consItems = [
1510
- d.daily_limit ? {icon:'πŸ“',label:'Daily Limit', text:d.daily_limit} : null,
1511
- d.optimal_timing ? {icon:'⏰',label:'Best Time to Eat', text:d.optimal_timing} : null,
1512
- d.overconsumption_effects ? {icon:'⚑',label:'Too Much Causes', text:d.overconsumption_effects} : null,
1513
- d.dietary_advice ? {icon:'πŸ’‘',label:'Dietary Tip', text:d.dietary_advice} : null,
1514
- ].filter(Boolean);
1515
- if (consItems.length) {
1516
- html += `<div class="r-section"><div class="r-title">Healthy Consumption Guide</div>
1517
- <div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
1518
- ${consItems.map(ci=>`
1519
- <div style="background:var(--parchment);border-radius:8px;padding:10px 12px">
1520
- <div style="font-size:1.2rem;margin-bottom:4px">${ci.icon}</div>
1521
- <div style="font-size:0.64rem;font-weight:700;color:var(--mist);text-transform:uppercase;letter-spacing:0.05em;margin-bottom:4px">${ci.label}</div>
1522
- <div style="font-size:0.78rem;line-height:1.5;color:var(--ink)">${escHtml(ci.text)}</div>
1523
  </div>`).join('')}
1524
- </div></div>`;
 
 
 
 
 
 
 
 
 
 
 
1525
  }
1526
 
1527
- // ── 13. Better Alternative ────────────────────────────────────
1528
  if (d.better_alternative) {
1529
  html += `<div class="r-section" style="padding:0;overflow:hidden">
1530
  <div class="alt-box">
@@ -1533,22 +1900,11 @@ function buildBody(d) {
1533
  <div class="alt-lbl">Better Alternative</div>
1534
  <div class="alt-text">${escHtml(d.better_alternative)}</div>
1535
  </div>
1536
- </div></div>`;
 
1537
  }
1538
  return html;
1539
  }
1540
- function updateQuotaPill(quota) {
1541
- if (!quota) return;
1542
- const pill = document.getElementById('quota-pill');
1543
- const count = document.getElementById('quota-count');
1544
- if (!pill || !count) return;
1545
- count.textContent = quota.remaining;
1546
- pill.style.display = 'flex';
1547
- if (quota.remaining <= 1) {
1548
- pill.style.background = 'rgba(220,38,38,0.3)';
1549
- pill.style.borderColor = 'rgba(252,165,165,0.5)';
1550
- }
1551
- }
1552
  function escHtml(str) {
1553
  // Only escape the four chars that break HTML structure.
1554
  // Do NOT encode quotes in text nodes β€” causes doubled encoding.
@@ -1676,14 +2032,427 @@ function setLang(code, save = true) {
1676
  updateProfileStats();
1677
  }
1678
  // ===== SHARE =====
 
1679
  async function shareReport() {
1680
  if (!lastResult) return;
1681
- const text = `🌿 Eatlytic Food Report\n\n${lastResult.product_name}\nScore: ${lastResult.score}/10 β€” ${lastResult.verdict}\n\n${lastResult.summary}`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1682
  try {
1683
- if (navigator.share) { await navigator.share({ title:'Eatlytic Report', text }); }
1684
- else { await navigator.clipboard.writeText(text); showToast('πŸ“‹ Copied to clipboard!'); }
1685
- } catch { showToast('πŸ“‹ Report copied!'); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1686
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1687
  function showToast(msg = 'βœ… Done!') {
1688
  const t = document.getElementById('toast');
1689
  t.textContent = msg;
 
9
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
10
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
11
  <script src="https://cdn.jsdelivr.net/npm/tesseract.js@5/dist/tesseract.min.js"></script>
12
+ <script src="https://cdn.jsdelivr.net/npm/@zxing/browser@0.1.4/umd/index.min.js"></script>
13
  <style>
14
  :root {
15
  --forest: #1E3A2F; --moss: #2D5241; --sage: #4A7C59;
 
588
  .severity-pill.severe { background:#FEE2E2; color:#991B1B; }
589
  .severity-pill.moderate { background:#FEF3C7; color:#92400E; }
590
  .severity-pill.mild { background:#ECFDF5; color:#065F46; }
591
+ /* ── Scan Quota Counter ───────────────────────────────── */
592
+ #scan-counter { text-align:center; margin:8px 0 4px; min-height:22px; }
593
+ .quota-badge {
594
+ display:inline-block; font-size:0.72rem; font-weight:600;
595
+ padding:4px 12px; border-radius:20px;
596
+ background:#1A2942; color:#7DD3FC; border:1px solid #1E3A5F;
597
+ letter-spacing:0.02em;
598
+ }
599
+ .quota-badge.empty { background:#3F1515; color:#FCA5A5; border-color:#7F1D1D; }
600
+
601
+ /* ── Health Warnings ──────────────────────────────────── */
602
+ .warnings-list { display:flex; flex-direction:column; gap:10px; }
603
+ .warn-card {
604
+ border-radius:10px; padding:12px 14px;
605
+ }
606
+ .warn-header { display:flex; align-items:center; gap:8px; margin-bottom:6px; flex-wrap:wrap; }
607
+ .warn-icon { font-size:1rem; }
608
+ .warn-sev { font-size:0.7rem; font-weight:800; letter-spacing:0.06em; text-transform:uppercase; }
609
+ .warn-cat { font-size:0.72rem; font-weight:600; color:var(--mist); }
610
+ .warn-text { font-size:0.8rem; line-height:1.5; font-weight:500; }
611
+ .warn-groups { display:flex; flex-wrap:wrap; gap:4px; margin-top:8px; }
612
+ .warn-tag {
613
+ font-size:0.66rem; padding:2px 8px; border-radius:10px;
614
+ background:rgba(0,0,0,0.1); color:inherit; font-weight:600;
615
+ }
616
+
617
+ /* ── Allergens ────────────────────────────────────────── */
618
+ .allergen-list { display:flex; flex-direction:column; gap:6px; }
619
+ .allergen-item {
620
+ font-size:0.8rem; font-weight:600; color:#92400E;
621
+ background:#FFFBEB; border:1px solid #FCD34D;
622
+ padding:8px 12px; border-radius:8px;
623
+ }
624
+
625
+ /* ── Contraindications ────────────────────────────────── */
626
+ .contra-list { display:flex; flex-direction:column; gap:6px; }
627
+ .contra-item {
628
+ font-size:0.79rem; color:#6B21A8;
629
+ background:#FAF5FF; border:1px solid #C084FC;
630
+ padding:8px 12px; border-radius:8px; line-height:1.4;
631
+ }
632
+
633
+ /* ── Consumption Guidance ─────────────────────────────── */
634
+ .consumption-grid { display:grid; grid-template-columns:1fr 1fr; gap:10px; }
635
+ .cons-card {
636
+ background:#F0F9FF; border:1px solid #BAE6FD;
637
+ border-radius:10px; padding:12px; min-height:80px;
638
+ }
639
+ .cons-card.cons-full { grid-column:1/-1; }
640
+ .cons-icon { font-size:1.1rem; margin-bottom:4px; }
641
+ .cons-label { font-size:0.68rem; font-weight:700; color:#0369A1; text-transform:uppercase; letter-spacing:0.05em; margin-bottom:4px; }
642
+ .cons-text { font-size:0.78rem; color:#0C4A6E; line-height:1.45; }
643
+
644
+ /* ── Curiosity + Wellness ─────────────────────────────── */
645
+ .curiosity-box {
646
+ background:#F0FDF4; border:1px solid #86EFAC;
647
+ border-radius:10px; padding:12px 14px;
648
+ font-size:0.8rem; color:#14532D; line-height:1.5; margin-bottom:8px;
649
+ }
650
+ .wellness-box {
651
+ background:#FFF7ED; border:1px solid #FED7AA;
652
+ border-radius:10px; padding:12px 14px;
653
+ font-size:0.8rem; color:#7C2D12; line-height:1.5;
654
+ }
655
+
656
+ /* ── Ingredients Spotlight ────────────────────────────── */
657
+ .spotlight-list { display:flex; flex-direction:column; gap:8px; }
658
+ .spotlight-item {
659
+ background:var(--surface-2); border-radius:10px;
660
+ padding:12px 14px; cursor:pointer; transition:background 0.2s;
661
+ border:1px solid transparent;
662
+ }
663
+ .spotlight-item:hover { background:#1A2942; border-color:#2A3E5A; }
664
+ .spot-name { font-size:0.85rem; font-weight:700; color:var(--sand); margin-bottom:2px; }
665
+ .spot-role { font-size:0.68rem; font-weight:600; color:var(--mist); text-transform:uppercase; letter-spacing:0.05em; margin-bottom:6px; }
666
+ .spot-fact { font-size:0.77rem; color:var(--text); line-height:1.4; margin-bottom:6px; }
667
+ .spot-tap { font-size:0.68rem; color:#60A5FA; font-weight:600; }
668
+
669
+ /* ── Ingredients Full List ────────────────────────────── */
670
+ .ingredients-full { display:flex; flex-wrap:wrap; gap:6px; }
671
+ .ing-chip {
672
+ display:inline-block; font-size:0.72rem;
673
+ background:#1A2942; color:#94A3B8;
674
+ border:1px solid #2A3E5A; border-radius:6px;
675
+ padding:3px 8px; line-height:1.4;
676
+ }
677
+
678
+ /* ══ ONBOARDING OVERLAY ══════════════════════════════════ */
679
+ #onboarding {
680
+ position:fixed; inset:0; z-index:9000;
681
+ background:var(--forest);
682
+ display:flex; flex-direction:column;
683
+ align-items:center; justify-content:space-between;
684
+ padding:40px 28px 36px; animation:fadeIn .3s ease;
685
+ }
686
+ #onboarding.hidden { display:none; }
687
+ @keyframes fadeIn { from{opacity:0;transform:scale(.97)} to{opacity:1;transform:scale(1)} }
688
+ .ob-skip {
689
+ align-self:flex-end; color:rgba(255,255,255,0.45);
690
+ font-size:0.78rem; font-weight:600; cursor:pointer;
691
+ padding:6px 0; letter-spacing:.5px;
692
+ }
693
+ .ob-slides { flex:1; display:flex; flex-direction:column;
694
+ align-items:center; justify-content:center; gap:24px;
695
+ text-align:center; width:100%; }
696
+ .ob-icon { font-size:4.5rem; line-height:1; }
697
+ .ob-title { font-size:1.65rem; font-weight:700;
698
+ color:var(--cream); line-height:1.2; }
699
+ .ob-desc { font-size:0.95rem; color:rgba(255,255,255,.65);
700
+ line-height:1.6; max-width:300px; }
701
+ .ob-dots { display:flex; gap:8px; }
702
+ .ob-dot { width:8px; height:8px; border-radius:50%;
703
+ background:rgba(255,255,255,.25); transition:background .3s; }
704
+ .ob-dot.active { background:var(--gold); width:22px; border-radius:4px; }
705
+ .ob-actions { display:flex; flex-direction:column; gap:12px; width:100%; }
706
+ .ob-next {
707
+ width:100%; padding:15px; border-radius:14px;
708
+ background:var(--gold); border:none; color:#1A1000;
709
+ font-size:1rem; font-weight:700; cursor:pointer;
710
+ font-family:'DM Sans',sans-serif; letter-spacing:.3px;
711
+ }
712
+ .ob-persona-row {
713
+ display:flex; flex-wrap:wrap; gap:8px; justify-content:center;
714
+ margin-top:8px;
715
+ }
716
+ .ob-persona-chip {
717
+ padding:8px 16px; border-radius:50px;
718
+ border:1.5px solid rgba(255,255,255,.25);
719
+ color:rgba(255,255,255,.7); font-size:0.82rem; font-weight:600;
720
+ cursor:pointer; transition:all .2s; font-family:'DM Sans',sans-serif;
721
+ }
722
+ .ob-persona-chip.selected {
723
+ background:var(--gold); border-color:var(--gold); color:#1A1000;
724
+ }
725
+ /* ══ INGREDIENT BOTTOM SHEET ══════════════════════════════ */
726
+ #ing-sheet {
727
+ position:fixed; inset:0; z-index:8000;
728
+ pointer-events:none; opacity:0; transition:opacity .25s;
729
+ }
730
+ #ing-sheet.open { pointer-events:all; opacity:1; }
731
+ .ing-backdrop {
732
+ position:absolute; inset:0; background:rgba(0,0,0,.45);
733
+ }
734
+ .ing-panel {
735
+ position:absolute; bottom:0; left:0; right:0;
736
+ background:var(--cream); border-radius:22px 22px 0 0;
737
+ padding:24px 22px 36px;
738
+ max-height:80dvh; overflow-y:auto;
739
+ transform:translateY(100%); transition:transform .3s cubic-bezier(.32,.72,0,1);
740
+ }
741
+ #ing-sheet.open .ing-panel { transform:translateY(0); }
742
+ .ing-handle {
743
+ width:36px; height:4px; border-radius:2px;
744
+ background:var(--whisper); margin:0 auto 20px;
745
+ }
746
+ .ing-loading { text-align:center; color:var(--mist); padding:40px 0; font-size:0.9rem; }
747
+ .ing-spinner {
748
+ width:30px; height:30px; border:3px solid var(--whisper);
749
+ border-top-color:var(--forest); border-radius:50%;
750
+ animation:spin .8s linear infinite; margin:0 auto 12px;
751
+ }
752
+ @keyframes spin { to{transform:rotate(360deg)} }
753
+ .ing-name { font-size:1.3rem; font-weight:700; color:var(--forest); margin-bottom:4px; }
754
+ .ing-verdict-pill {
755
+ display:inline-block; padding:3px 12px; border-radius:50px;
756
+ font-size:0.73rem; font-weight:700; letter-spacing:.5px;
757
+ margin-bottom:16px;
758
+ }
759
+ .ing-verdict-pill.safe { background:#D6F0E0; color:#1A5C38; }
760
+ .ing-verdict-pill.aware { background:#FFF8E1; color:#7A5800; }
761
+ .ing-verdict-pill.caution { background:#FDEDEC; color:#922B21; }
762
+ .ing-row { margin-bottom:14px; }
763
+ .ing-row-label {
764
+ font-size:0.68rem; font-weight:700; text-transform:uppercase;
765
+ letter-spacing:1px; color:var(--mist); margin-bottom:4px;
766
+ }
767
+ .ing-row-text { font-size:0.9rem; color:var(--ink); line-height:1.5; }
768
+ .ing-curiosity {
769
+ background:linear-gradient(135deg,#E8F4F0,#F0F9F5);
770
+ border-left:4px solid var(--forest);
771
+ padding:12px 14px; border-radius:0 10px 10px 0;
772
+ font-size:0.85rem; color:var(--forest); line-height:1.5;
773
+ margin-top:4px;
774
+ }
775
+ .ing-close-btn {
776
+ width:100%; margin-top:20px; padding:13px;
777
+ background:var(--forest); color:var(--cream);
778
+ border:none; border-radius:12px; font-size:0.95rem;
779
+ font-weight:700; cursor:pointer; font-family:'DM Sans',sans-serif;
780
+ }
781
+ /* ══ SHARE CARD OVERLAY ════════════════════════════════════ */
782
+ #share-overlay {
783
+ position:fixed; inset:0; z-index:8500;
784
+ background:rgba(0,0,0,.7); display:none;
785
+ align-items:center; justify-content:center;
786
+ padding:20px;
787
+ }
788
+ #share-overlay.open { display:flex; }
789
+ .share-card-wrap {
790
+ background:var(--white); border-radius:20px;
791
+ padding:20px; width:100%; max-width:360px;
792
+ box-shadow:0 20px 60px rgba(0,0,0,.35);
793
+ }
794
+ #shareCanvas {
795
+ width:100%; border-radius:12px;
796
+ display:block; margin-bottom:14px;
797
+ }
798
+ .share-actions { display:grid; grid-template-columns:1fr 1fr; gap:10px; }
799
+ .share-action-btn {
800
+ padding:11px; border-radius:10px; border:none;
801
+ font-size:0.85rem; font-weight:700;
802
+ cursor:pointer; font-family:'DM Sans',sans-serif;
803
+ }
804
+ .share-action-btn.primary { background:var(--forest); color:var(--cream); }
805
+ .share-action-btn.secondary { background:var(--whisper); color:var(--ink); }
806
+ /* ══ BARCODE BUTTON ════════════════════════════════════════ */
807
+ .barcode-btn {
808
+ display:flex; align-items:center; gap:8px;
809
+ width:100%; padding:11px 16px; border-radius:12px;
810
+ background:var(--white); border:1.5px solid var(--whisper);
811
+ color:var(--mist); font-size:0.85rem; font-weight:600;
812
+ cursor:pointer; font-family:'DM Sans',sans-serif;
813
+ margin-top:8px; transition:all .2s;
814
+ }
815
+ .barcode-btn:hover { border-color:var(--sage); color:var(--forest); }
816
+ /* ══ BARCODE SCANNER ═══════════════════════════════════════ */
817
+ #barcode-overlay {
818
+ position:fixed; inset:0; z-index:7000;
819
+ background:#000; display:none;
820
+ flex-direction:column; align-items:center; justify-content:center;
821
+ }
822
+ #barcode-overlay.open { display:flex; }
823
+ #barcode-video {
824
+ width:100%; max-width:400px;
825
+ border-radius:16px; display:block;
826
+ }
827
+ .barcode-guide {
828
+ position:absolute;
829
+ width:240px; height:80px;
830
+ border:2px solid var(--gold);
831
+ border-radius:8px;
832
+ box-shadow:0 0 0 4000px rgba(0,0,0,.5);
833
+ }
834
+ .barcode-hint {
835
+ position:absolute; bottom:100px;
836
+ color:rgba(255,255,255,.75); font-size:0.85rem;
837
+ text-align:center; padding:0 30px;
838
+ }
839
+ .barcode-cancel {
840
+ position:absolute; bottom:44px;
841
+ background:rgba(255,255,255,.15); color:#fff;
842
+ border:1.5px solid rgba(255,255,255,.3);
843
+ padding:10px 30px; border-radius:50px;
844
+ font-size:0.9rem; font-weight:600; cursor:pointer;
845
+ font-family:'DM Sans',sans-serif;
846
+ }
847
  </style>
848
  </head>
849
  <body>
 
852
  <div class="header">
853
  <div class="logo">Eat<em>lytic</em></div>
854
  <div class="header-right">
 
 
 
855
  <button class="hdr-btn" onclick="openLang()"><i class="fas fa-globe"></i> <span id="lang-display">EN</span></button>
856
  </div>
857
  </div>
 
954
  <p>Tap or drag photo</p>
955
  </div>
956
  </div>
957
+ <button class="barcode-btn" onclick="startBarcodeScanner()">
958
+ <i class="fas fa-barcode"></i> Scan Barcode Instead
959
+ </button>
960
  </div>
961
 
962
  <!-- Preview -->
 
974
  <div class="state-tip" id="state-tip"></div>
975
  </div>
976
  </div>
977
+ <div id="scan-counter"></div>
978
  <button class="btn-primary" id="analyze-main-btn" onclick="analyze()" style="display:none">
979
  <i class="fas fa-microscope" style="margin-right:8px"></i>
980
  <span id="analyze-btn-text">Analyze Label</span>
 
1073
  </div>
1074
  </div>
1075
 
1076
+ <!-- ONBOARDING OVERLAY (3-screen, shown on first run) -->
1077
+ <div id="onboarding" class="hidden">
1078
+ <div class="ob-skip" onclick="closeOnboarding()">Skip</div>
1079
+ <div class="ob-slides">
1080
+ <!-- Slide content injected by JS -->
1081
+ </div>
1082
+ <div class="ob-actions">
1083
+ <div class="ob-dots" id="ob-dots" style="justify-content:center;margin-bottom:8px"></div>
1084
+ <button class="ob-next" id="ob-next-btn" onclick="nextOnboardStep()">Get Started β†’</button>
1085
+ </div>
1086
+ </div>
1087
+
1088
+ <!-- INGREDIENT SHEET (bottom sheet, fetches /ingredient-info) -->
1089
+ <div id="ing-sheet" onclick="closeIngSheet(event)">
1090
+ <div class="ing-backdrop"></div>
1091
+ <div class="ing-panel">
1092
+ <div class="ing-handle"></div>
1093
+ <div id="ing-content">
1094
+ <div class="ing-loading">
1095
+ <div class="ing-spinner"></div>
1096
+ Loading ingredient info…
1097
+ </div>
1098
+ </div>
1099
+ <button class="ing-close-btn" onclick="closeIngSheet()">Close</button>
1100
+ </div>
1101
+ </div>
1102
+
1103
+ <!-- SHARE CARD OVERLAY (canvas-generated PNG) -->
1104
+ <div id="share-overlay" onclick="closeShareOverlay(event)">
1105
+ <div class="share-card-wrap">
1106
+ <canvas id="shareCanvas" width="680" height="400"></canvas>
1107
+ <div class="share-actions">
1108
+ <button class="share-action-btn primary" onclick="downloadShareCard()">
1109
+ <i class="fas fa-download"></i> Save Image
1110
+ </button>
1111
+ <button class="share-action-btn secondary" onclick="nativeShare()">
1112
+ <i class="fas fa-share-alt"></i> Share
1113
+ </button>
1114
+ </div>
1115
+ </div>
1116
+ </div>
1117
+
1118
+ <!-- BARCODE SCANNER OVERLAY -->
1119
+ <div id="barcode-overlay">
1120
+ <video id="barcode-video" autoplay playsinline muted></video>
1121
+ <div class="barcode-guide"></div>
1122
+ <div class="barcode-hint">πŸ“¦ Point at any product barcode (EAN / UPC)</div>
1123
+ <button class="barcode-cancel" onclick="stopBarcodeScanner()">Cancel</button>
1124
+ </div>
1125
+
1126
  <!-- LOADER -->
1127
  <div id="loader">
1128
  <div class="loader-ring"></div>
 
1254
  currentPersona = savedPersona;
1255
  renderLangOptions();
1256
  updateProfileStats();
1257
+ initScanCounter();
1258
+ initOnboarding();
1259
  };
1260
  // ===== TABS =====
1261
  function switchTab(tab) {
 
1491
  }
1492
  notice.classList.add('visible');
1493
  }
1494
+ // ===== SCAN QUOTA COUNTER =====
1495
+ async function initScanCounter() {
1496
+ try {
1497
+ const r = await fetch(`${API}/scan-quota`);
1498
+ const q = await r.json();
1499
+ updateScanCounter(q);
1500
+ } catch { /* silent fail */ }
1501
+ }
1502
+ function updateScanCounter(q) {
1503
+ if (!q) return;
1504
+ const el = document.getElementById('scan-counter');
1505
+ if (!el) return;
1506
+ const rem = q.remaining ?? (q.limit - q.used);
1507
+ const lim = q.limit ?? 5;
1508
+ if (rem <= 0) {
1509
+ el.innerHTML = `<span class="quota-badge empty">πŸ”’ Daily limit reached β€” <a href="#" style="color:inherit;font-weight:700">Upgrade to Pro</a></span>`;
1510
+ } else {
1511
+ el.innerHTML = `<span class="quota-badge" style="opacity:${rem <= 2 ? 1 : 0.75}">⚑ ${rem}/${lim} free scans today</span>`;
1512
+ }
1513
+ }
1514
  async function analyze() {
1515
  if (!blob) return;
1516
  hideError();
 
1547
  if (!res.ok) throw new Error(`Server error ${res.status}: ${res.statusText}`);
1548
  const data = await res.json();
1549
  if (data.error === 'quota_exceeded') {
1550
+ showContextualError('quota','πŸ”’','Daily Limit Reached',
1551
+ data.message || `You've used all ${data.quota?.limit ?? 5} free scans for today. Come back tomorrow or upgrade to Pro.`);
1552
+ updateScanCounter(data.quota);
1553
  return;
1554
  }
1555
  if (data.error === 'image_quality') {
 
1572
  return;
1573
  }
1574
  if (data.error) throw new Error(data.error);
 
 
1575
  // Show blur notice if blur was detected
1576
  handleBlurNotice(data.blur_info);
1577
+ if (data.quota) updateScanCounter(data.quota);
1578
  lastResult = data;
1579
  renderReport(data);
1580
  if (saveHistory) addToHistory(data);
 
1642
  });
1643
  }
1644
  function buildBody(d) {
 
1645
  const nutrients = Array.isArray(d.nutrient_breakdown) ? d.nutrient_breakdown : [];
1646
  const pros = Array.isArray(d.pros) ? d.pros : [];
1647
  const cons = Array.isArray(d.cons) ? d.cons : [];
1648
  const ageW = Array.isArray(d.age_warnings) ? d.age_warnings : [];
1649
  const warnings = Array.isArray(d.health_warnings) ? d.health_warnings : [];
 
1650
  const contras = Array.isArray(d.contraindications) ? d.contraindications : [];
1651
+ const allergens = Array.isArray(d.allergens) ? d.allergens : [];
1652
  const ingList = Array.isArray(d.ingredients_list) ? d.ingredients_list : [];
1653
  const spotlight = Array.isArray(d.ingredients_spotlight) ? d.ingredients_spotlight : [];
1654
  let html = '';
1655
 
1656
+ // 0. Blur info section
1657
  const bi = d.blur_info;
1658
  if (bi && bi.detected) {
1659
  if (bi.deblurred && bi.image_b64) {
1660
  html += `<div class="r-section" style="padding:14px">
1661
  <div class="deblur-report-badge">πŸ”§ Blur Corrected β€” Enhanced Image Used</div>
1662
+ <div class="deblur-img-wrap">
1663
+ <img src="${bi.image_b64}" alt="Deblurred image" loading="lazy">
1664
+ </div>
1665
  <div style="font-size:0.76rem;color:var(--mist);line-height:1.5">
1666
+ The original image was <strong>${escHtml(bi.severity)}ly blurry</strong>
1667
+ (score&nbsp;${bi.score}/100). It was processed through a
1668
+ multi-stage enhancement pipeline before OCR and analysis.
1669
  ${bi.method_log ? `<br><span style="font-family:monospace;font-size:0.68rem;color:#A16207">Pipeline: ${escHtml(bi.method_log)}</span>` : ''}
1670
+ </div>
1671
+ </div>`;
1672
  } else {
1673
  html += `<div class="r-section" style="padding:14px;background:#FFFBEB;border:1.5px solid #FCD34D;border-radius:var(--radius-sm)">
1674
  <div class="deblur-report-badge">⚠️ Blur Detected</div>
1675
  <div style="font-size:0.76rem;color:#78350F;line-height:1.5">
1676
+ Blur detected (severity:&nbsp;<strong>${escHtml(bi.severity)}</strong>,
1677
+ score&nbsp;${bi.score}/100). Original image used after comparison.
1678
  Retake with better lighting for higher accuracy.
1679
+ </div>
1680
+ </div>`;
1681
  }
1682
  }
1683
 
1684
+ // 0b. Low-confidence notice
1685
  if (d.is_low_confidence) {
1686
  html += `<div class="r-section" style="padding:12px;background:#FFF7ED;border:1.5px solid #FDBA74;border-radius:var(--radius-sm)">
1687
  <div style="font-size:0.8rem;color:#92400E;font-weight:600">⚠️ Partial Label β€” Lower Confidence</div>
1688
+ <div style="font-size:0.75rem;color:#92400E;margin-top:4px">OCR could only extract partial text. Results are directional β€” retake with a clearer photo for full accuracy.</div>
1689
+ </div>`;
 
1690
  }
1691
 
1692
+ // 1. Health Warnings (CRITICAL β€” shown first after image info)
1693
  if (warnings.length) {
1694
+ const sevOrder = { critical: 0, moderate: 1, low: 2 };
1695
+ const sorted = [...warnings].sort((a, b) => (sevOrder[a.severity] ?? 2) - (sevOrder[b.severity] ?? 2));
1696
+ const sevStyle = {
1697
+ critical: { bg: '#FEF2F2', border: '#F87171', icon: '🚨', color: '#991B1B', label: 'CRITICAL' },
1698
+ moderate: { bg: '#FFFBEB', border: '#FCD34D', icon: '⚠️', color: '#92400E', label: 'MODERATE' },
1699
+ low: { bg: '#F0F9FF', border: '#7DD3FC', icon: 'ℹ️', color: '#075985', label: 'NOTE' },
1700
  };
1701
+ html += `<div class="r-section">
1702
+ <div class="r-title">Health Warnings</div>
1703
+ <div class="warnings-list">
1704
+ ${sorted.map(w => {
1705
+ const s = sevStyle[w.severity] || sevStyle.low;
1706
+ const groups = Array.isArray(w.affected_groups) && w.affected_groups.length
1707
+ ? `<div class="warn-groups">${w.affected_groups.map(g => `<span class="warn-tag">${escHtml(g)}</span>`).join('')}</div>` : '';
1708
+ return `<div class="warn-card" style="background:${s.bg};border-left:4px solid ${s.border}">
1709
+ <div class="warn-header">
1710
+ <span class="warn-icon">${s.icon}</span>
1711
+ <span class="warn-sev" style="color:${s.color}">${s.label}</span>
1712
+ <span class="warn-cat">${escHtml(w.category || '')}</span>
1713
+ </div>
1714
+ <div class="warn-text" style="color:${s.color}">${escHtml(w.warning)}</div>
1715
+ ${groups}
1716
+ </div>`;
1717
+ }).join('')}
1718
+ </div>
1719
+ </div>`;
1720
  }
1721
 
1722
+ // 2. Allergens
1723
  if (allergens.length) {
1724
+ html += `<div class="r-section">
1725
+ <div class="r-title">Allergen Information</div>
1726
+ <div class="allergen-list">
1727
+ ${allergens.map(a => `<div class="allergen-item">⚠️ ${escHtml(a)}</div>`).join('')}
1728
+ </div>
1729
+ </div>`;
1730
  }
1731
 
1732
+ // 3. Contraindications
1733
  if (contras.length) {
1734
+ html += `<div class="r-section">
1735
+ <div class="r-title">Contraindications</div>
1736
+ <div class="contra-list">
1737
+ ${contras.map(c => `<div class="contra-item">🚫 ${escHtml(c)}</div>`).join('')}
1738
+ </div>
1739
+ </div>`;
1740
  }
1741
 
1742
+ // 4. Simple Explanation
1743
  if (d.eli5_explanation) {
1744
+ html += `<div class="r-section">
1745
+ <div class="r-title">Simple Explanation</div>
1746
+ <div class="eli5-box">
1747
+ <span class="eli5-label">🍭 Easy Read</span>
1748
+ ${escHtml(d.eli5_explanation)}
1749
+ </div>
1750
  </div>`;
1751
  }
1752
 
1753
+ // 5. Nutritional Breakdown
1754
  if (nutrients.length) {
1755
+ const MAXES = { g: 50, mg: 2000, '%': 100, kcal: 500, kj: 2000 };
1756
+ html += `<div class="r-section">
1757
+ <div class="r-title">Nutritional Breakdown</div>
 
 
 
 
1758
  <div class="nutrient-legend">
1759
  <div class="n-leg"><div class="n-dot" style="background:#2D7A4F"></div>Good</div>
1760
  <div class="n-leg"><div class="n-dot" style="background:#C9A84C"></div>Moderate</div>
 
1762
  <div class="n-leg"><div class="n-dot" style="background:#8B0000"></div>Bad</div>
1763
  </div>
1764
  ${nutrients.map(n => {
1765
+ const r = n.rating || 'moderate';
1766
+ const max = MAXES[n.unit] || 100;
1767
+ const pct = Math.min(100, (parseFloat(n.value) / max) * 100);
 
 
 
 
 
1768
  return `<div class="nutrient-item">
1769
  <div class="nutrient-row">
1770
  <div class="n-name">${escHtml(n.name)}</div>
1771
  <div class="n-bar-wrap"><div class="n-bar bar-${r}" data-w="${pct.toFixed(1)}"></div></div>
1772
+ <div class="n-val">${n.value}${n.unit || ''}</div>
1773
  </div>
1774
  ${n.impact ? `<div class="n-impact">${escHtml(n.impact)}</div>` : ''}
1775
  </div>`;
 
1777
  </div>`;
1778
  }
1779
 
1780
+ // 6. Pros & Cons
1781
  if (pros.length || cons.length) {
1782
+ html += `<div class="r-section">
1783
+ <div class="r-title">Pros &amp; Cons</div>
1784
  <div class="pc-grid">
1785
  <div>
1786
  <div class="pc-label p">Benefits</div>
1787
+ ${pros.map(p => `<div class="pc-item"><div class="pc-dot p"></div><span>${escHtml(p)}</span></div>`).join('')}
1788
  </div>
1789
  <div>
1790
  <div class="pc-label c">Watch Out</div>
1791
+ ${cons.map(c => `<div class="pc-item"><div class="pc-dot c"></div><span>${escHtml(c)}</span></div>`).join('')}
1792
  </div>
1793
+ </div>
1794
+ </div>`;
1795
+ }
1796
+
1797
+ // 7. Consumption Guidance (NEW)
1798
+ const hasConsumption = d.daily_limit || d.optimal_timing || d.overconsumption_effects || d.dietary_advice;
1799
+ if (hasConsumption) {
1800
+ html += `<div class="r-section">
1801
+ <div class="r-title">Healthy Consumption Guidance</div>
1802
+ <div class="consumption-grid">
1803
+ ${d.daily_limit ? `<div class="cons-card">
1804
+ <div class="cons-icon">πŸ“</div>
1805
+ <div class="cons-label">Daily Limit</div>
1806
+ <div class="cons-text">${escHtml(d.daily_limit)}</div>
1807
+ </div>` : ''}
1808
+ ${d.optimal_timing ? `<div class="cons-card">
1809
+ <div class="cons-icon">⏰</div>
1810
+ <div class="cons-label">Best Time to Consume</div>
1811
+ <div class="cons-text">${escHtml(d.optimal_timing)}</div>
1812
+ </div>` : ''}
1813
+ ${d.overconsumption_effects ? `<div class="cons-card cons-full">
1814
+ <div class="cons-icon">πŸ“ˆ</div>
1815
+ <div class="cons-label">Effects of Overconsumption</div>
1816
+ <div class="cons-text">${escHtml(d.overconsumption_effects)}</div>
1817
+ </div>` : ''}
1818
+ ${d.dietary_advice ? `<div class="cons-card cons-full">
1819
+ <div class="cons-icon">πŸ₯—</div>
1820
+ <div class="cons-label">Balanced Diet Tip</div>
1821
+ <div class="cons-text">${escHtml(d.dietary_advice)}</div>
1822
+ </div>` : ''}
1823
+ </div>
1824
+ </div>`;
1825
  }
1826
 
1827
+ // 8. Product Analysis paragraphs
1828
  if (d.paragraph_benefits || d.paragraph_uniqueness) {
1829
+ html += `<div class="r-section">
1830
+ <div class="r-title">Product Analysis</div>
1831
  ${d.paragraph_benefits ? `<p class="para-text">${escHtml(d.paragraph_benefits)}</p>` : ''}
1832
  ${d.paragraph_uniqueness ? `<p class="para-text">${escHtml(d.paragraph_uniqueness)}</p>` : ''}
1833
  </div>`;
1834
  }
1835
 
1836
+ // 9. Age Group Warnings
1837
  if (ageW.length) {
1838
+ html += `<div class="r-section">
1839
+ <div class="r-title">By Age Group</div>
1840
  <div class="age-grid">
1841
+ ${ageW.map(w => `
1842
+ <div class="age-card ${w.status || 'caution'}">
1843
+ <div class="age-emoji">${w.emoji || 'πŸ‘€'}</div>
1844
  <div class="age-grp">${escHtml(w.group)}</div>
1845
  <div class="age-msg">${escHtml(w.message)}</div>
1846
  </div>`).join('')}
1847
+ </div>
1848
+ </div>`;
 
 
 
 
 
 
 
 
 
 
 
 
1849
  }
1850
 
1851
+ // 10. Curiosity Fact + Wellness Tip
1852
+ if (d.curiosity_fact || d.wellness_tip) {
1853
+ html += `<div class="r-section">
1854
+ <div class="r-title">Curiosity Corner</div>
1855
+ ${d.curiosity_fact ? `<div class="curiosity-box">πŸ’‘ ${escHtml(d.curiosity_fact)}</div>` : ''}
1856
+ ${d.wellness_tip ? `<div class="wellness-box">🌿 <strong>Tip for you:</strong> ${escHtml(d.wellness_tip)}</div>` : ''}
1857
+ </div>`;
1858
  }
1859
 
1860
+ // 11. Molecular Insight
1861
  if (d.molecular_insight) {
1862
+ html += `<div class="r-section">
1863
+ <div class="r-title">Molecular Insight</div>
1864
  <div class="mol-box">πŸ”¬ ${escHtml(d.molecular_insight)}</div>
1865
  </div>`;
1866
  }
1867
 
1868
+ // 12. Ingredients Spotlight (tap-to-learn)
1869
+ if (spotlight.length) {
1870
+ html += `<div class="r-section">
1871
+ <div class="r-title">Ingredients Spotlight</div>
1872
+ <div class="spotlight-list">
1873
+ ${spotlight.map(s => `
1874
+ <div class="spotlight-item" onclick="openIngSheet('${escHtml(s.name).replace(/'/g,"\\'")}')">
1875
+ <div class="spot-name">${escHtml(s.name)}</div>
1876
+ <div class="spot-role">${escHtml(s.role || '')}</div>
1877
+ <div class="spot-fact">${escHtml(s.curiosity || '')}</div>
1878
+ <div class="spot-tap">Tap to learn more β†’</div>
 
 
 
 
1879
  </div>`).join('')}
1880
+ </div>
1881
+ </div>`;
1882
+ }
1883
+
1884
+ // 13. Full Ingredients List
1885
+ if (ingList.length) {
1886
+ html += `<div class="r-section">
1887
+ <div class="r-title">Full Ingredients</div>
1888
+ <div class="ingredients-full">
1889
+ ${ingList.map(i => `<span class="ing-chip">${escHtml(i)}</span>`).join('')}
1890
+ </div>
1891
+ </div>`;
1892
  }
1893
 
1894
+ // 14. Better Alternative
1895
  if (d.better_alternative) {
1896
  html += `<div class="r-section" style="padding:0;overflow:hidden">
1897
  <div class="alt-box">
 
1900
  <div class="alt-lbl">Better Alternative</div>
1901
  <div class="alt-text">${escHtml(d.better_alternative)}</div>
1902
  </div>
1903
+ </div>
1904
+ </div>`;
1905
  }
1906
  return html;
1907
  }
 
 
 
 
 
 
 
 
 
 
 
 
1908
  function escHtml(str) {
1909
  // Only escape the four chars that break HTML structure.
1910
  // Do NOT encode quotes in text nodes β€” causes doubled encoding.
 
2032
  updateProfileStats();
2033
  }
2034
  // ===== SHARE =====
2035
+ // ── SHARE (replaced: now opens canvas share card) ──────────────────
2036
  async function shareReport() {
2037
  if (!lastResult) return;
2038
+ generateShareCard(lastResult);
2039
+ document.getElementById('share-overlay').classList.add('open');
2040
+ }
2041
+
2042
+ function closeShareOverlay(e) {
2043
+ if (!e || e.target === document.getElementById('share-overlay')) {
2044
+ document.getElementById('share-overlay').classList.remove('open');
2045
+ }
2046
+ }
2047
+
2048
+ function generateShareCard(d) {
2049
+ const canvas = document.getElementById('shareCanvas');
2050
+ const ctx = canvas.getContext('2d');
2051
+ const W = 680, H = 400;
2052
+ canvas.width = W; canvas.height = H;
2053
+
2054
+ // Background
2055
+ ctx.fillStyle = '#1E3A2F';
2056
+ ctx.fillRect(0, 0, W, H);
2057
+
2058
+ // Subtle grain texture overlay
2059
+ ctx.fillStyle = 'rgba(255,255,255,0.02)';
2060
+ for (let i = 0; i < 3000; i++) {
2061
+ ctx.fillRect(Math.random()*W, Math.random()*H, 1, 1);
2062
+ }
2063
+
2064
+ // Score ring
2065
+ const cx = 100, cy = 190, r = 66;
2066
+ ctx.beginPath();
2067
+ ctx.arc(cx, cy, r, 0, Math.PI*2);
2068
+ ctx.strokeStyle = 'rgba(255,255,255,0.12)';
2069
+ ctx.lineWidth = 10;
2070
+ ctx.stroke();
2071
+
2072
+ const score = Math.min(10, Math.max(1, d.score || 5));
2073
+ const angle = (score / 10) * Math.PI * 2 - Math.PI / 2;
2074
+ const scoreColor = score >= 7 ? '#7FB896' : score >= 5 ? '#C9A84C' : '#D96B47';
2075
+ ctx.beginPath();
2076
+ ctx.arc(cx, cy, r, -Math.PI/2, angle);
2077
+ ctx.strokeStyle = scoreColor;
2078
+ ctx.lineWidth = 10;
2079
+ ctx.lineCap = 'round';
2080
+ ctx.stroke();
2081
+
2082
+ // Score number
2083
+ ctx.fillStyle = '#FFFFFF';
2084
+ ctx.font = 'bold 42px DM Sans, Arial';
2085
+ ctx.textAlign = 'center';
2086
+ ctx.fillText(score, cx, cy + 8);
2087
+ ctx.font = '16px DM Sans, Arial';
2088
+ ctx.fillStyle = 'rgba(255,255,255,0.5)';
2089
+ ctx.fillText('/ 10', cx, cy + 28);
2090
+
2091
+ // Verdict pill
2092
+ const verdict = (d.verdict || 'Analyzed').toUpperCase();
2093
+ ctx.font = 'bold 11px DM Sans, Arial';
2094
+ const pillW = ctx.measureText(verdict).width + 24;
2095
+ const pillX = cx - pillW/2, pillY = cy + 46;
2096
+ ctx.fillStyle = scoreColor;
2097
+ roundRect(ctx, pillX, pillY, pillW, 22, 11);
2098
+ ctx.fillStyle = '#1E3A2F';
2099
+ ctx.textAlign = 'center';
2100
+ ctx.font = 'bold 11px DM Sans, Arial';
2101
+ ctx.fillText(verdict, cx, pillY + 15);
2102
+
2103
+ // Divider
2104
+ ctx.strokeStyle = 'rgba(255,255,255,0.1)';
2105
+ ctx.lineWidth = 1;
2106
+ ctx.beginPath();
2107
+ ctx.moveTo(185, 60); ctx.lineTo(185, 340);
2108
+ ctx.stroke();
2109
+
2110
+ // Product info (right column)
2111
+ const rx = 210;
2112
+ ctx.textAlign = 'left';
2113
+
2114
+ // Category tag
2115
+ ctx.font = '600 12px DM Sans, Arial';
2116
+ ctx.fillStyle = 'rgba(201,168,76,0.85)';
2117
+ ctx.fillText((d.product_category || 'Food Product').toUpperCase(), rx, 90);
2118
+
2119
+ // Product name
2120
+ const name = d.product_name || 'Unknown Product';
2121
+ ctx.font = 'bold 26px DM Sans, Arial';
2122
+ ctx.fillStyle = '#FFFFFF';
2123
+ const nameLines = wrapText(ctx, name, W - rx - 40, 26);
2124
+ nameLines.slice(0, 2).forEach((line, i) => {
2125
+ ctx.fillText(line, rx, 120 + i * 34);
2126
+ });
2127
+
2128
+ // Summary
2129
+ const summaryY = nameLines.length > 1 ? 196 : 168;
2130
+ ctx.font = '15px DM Sans, Arial';
2131
+ ctx.fillStyle = 'rgba(255,255,255,0.6)';
2132
+ const summaryLines = wrapText(ctx, d.summary || '', W - rx - 40, 15);
2133
+ summaryLines.slice(0, 3).forEach((line, i) => {
2134
+ ctx.fillText(line, rx, summaryY + i * 22);
2135
+ });
2136
+
2137
+ // Nutrient pills row
2138
+ const nutrients = (d.nutrient_breakdown || []).slice(0, 4);
2139
+ const pillsY = 296;
2140
+ let pillOffset = rx;
2141
+ nutrients.forEach(n => {
2142
+ const pillColor = n.rating === 'good' ? '#2D7A4F' :
2143
+ n.rating === 'moderate' ? '#7A5800' :
2144
+ n.rating === 'caution' ? '#8B2500' : '#5C1010';
2145
+ const label = `${n.name} ${n.value}${n.unit}`;
2146
+ ctx.font = '600 12px DM Sans, Arial';
2147
+ const tw = ctx.measureText(label).width + 20;
2148
+ ctx.fillStyle = pillColor;
2149
+ roundRect(ctx, pillOffset, pillsY, tw, 26, 8);
2150
+ ctx.fillStyle = 'rgba(255,255,255,0.85)';
2151
+ ctx.textAlign = 'left';
2152
+ ctx.font = '600 12px DM Sans, Arial';
2153
+ ctx.fillText(label, pillOffset + 10, pillsY + 17);
2154
+ pillOffset += tw + 8;
2155
+ if (pillOffset > W - 40) return;
2156
+ });
2157
+
2158
+ // Footer bar
2159
+ ctx.fillStyle = 'rgba(0,0,0,0.25)';
2160
+ ctx.fillRect(0, 356, W, 44);
2161
+ ctx.font = 'bold 15px DM Sans, Arial';
2162
+ ctx.fillStyle = '#C9A84C';
2163
+ ctx.textAlign = 'left';
2164
+ ctx.fillText('🌿 Eatlytic', 30, 382);
2165
+ ctx.font = '13px DM Sans, Arial';
2166
+ ctx.fillStyle = 'rgba(255,255,255,0.4)';
2167
+ ctx.textAlign = 'right';
2168
+ ctx.fillText('AI-Powered Food Intelligence β€’ eatlytic.app', W - 30, 382);
2169
+ }
2170
+
2171
+ function roundRect(ctx, x, y, w, h, r) {
2172
+ ctx.beginPath();
2173
+ ctx.moveTo(x + r, y);
2174
+ ctx.lineTo(x + w - r, y);
2175
+ ctx.quadraticCurveTo(x + w, y, x + w, y + r);
2176
+ ctx.lineTo(x + w, y + h - r);
2177
+ ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
2178
+ ctx.lineTo(x + r, y + h);
2179
+ ctx.quadraticCurveTo(x, y + h, x, y + h - r);
2180
+ ctx.lineTo(x, y + r);
2181
+ ctx.quadraticCurveTo(x, y, x + r, y);
2182
+ ctx.closePath();
2183
+ ctx.fill();
2184
+ }
2185
+
2186
+ function wrapText(ctx, text, maxWidth, fontSize) {
2187
+ const words = (text || '').split(' ');
2188
+ const lines = [];
2189
+ let line = '';
2190
+ words.forEach(word => {
2191
+ const test = line ? line + ' ' + word : word;
2192
+ if (ctx.measureText(test).width > maxWidth && line) {
2193
+ lines.push(line);
2194
+ line = word;
2195
+ } else {
2196
+ line = test;
2197
+ }
2198
+ });
2199
+ if (line) lines.push(line);
2200
+ return lines;
2201
+ }
2202
+
2203
+ function downloadShareCard() {
2204
+ const canvas = document.getElementById('shareCanvas');
2205
+ const link = document.createElement('a');
2206
+ link.download = `eatlytic-${(lastResult?.product_name || 'report').replace(/\s+/g,'-').toLowerCase()}.png`;
2207
+ link.href = canvas.toDataURL('image/png');
2208
+ link.click();
2209
+ showToast('πŸ“₯ Image saved!');
2210
+ }
2211
+
2212
+ async function nativeShare() {
2213
+ const canvas = document.getElementById('shareCanvas');
2214
+ try {
2215
+ canvas.toBlob(async blob => {
2216
+ const file = new File([blob], 'eatlytic-report.png', { type: 'image/png' });
2217
+ if (navigator.share && navigator.canShare({ files: [file] })) {
2218
+ await navigator.share({ files: [file], title: 'Eatlytic Food Report', text: `${lastResult?.product_name} scored ${lastResult?.score}/10 β€” analysed with Eatlytic` });
2219
+ } else {
2220
+ // Fallback: copy text
2221
+ await navigator.clipboard.writeText(`🌿 ${lastResult?.product_name} β€” Score: ${lastResult?.score}/10\n${lastResult?.verdict}\n\n${lastResult?.summary}\n\nAnalysed with Eatlytic`);
2222
+ showToast('πŸ“‹ Report text copied!');
2223
+ }
2224
+ });
2225
+ } catch { showToast('πŸ“‹ Share link copied!'); }
2226
+ }
2227
+
2228
+ // ── INGREDIENT BOTTOM SHEET ─────────────────────────────────────────
2229
+ async function openIngSheet(name) {
2230
+ const sheet = document.getElementById('ing-sheet');
2231
+ const content = document.getElementById('ing-content');
2232
+ content.innerHTML = `<div class="ing-loading"><div class="ing-spinner"></div>Loading info on <strong>${escHtml(name)}</strong>…</div>`;
2233
+ sheet.classList.add('open');
2234
+
2235
+ try {
2236
+ const res = await fetch(`${API}/ingredient-info?name=${encodeURIComponent(name)}&context=${encodeURIComponent(currentPersona)}&language=${currentLang}`);
2237
+ const d = await res.json();
2238
+ if (d.error) throw new Error(d.error);
2239
+
2240
+ const safetyClass = d.safety_level === 'safe' ? 'safe' : d.safety_level === 'caution' ? 'caution' : 'aware';
2241
+ const safetyLabel = d.safety_level === 'safe' ? 'βœ… Generally Safe' : d.safety_level === 'caution' ? '⚠️ Use Caution' : 'πŸ‘ Be Aware';
2242
+
2243
+ content.innerHTML = `
2244
+ <div class="ing-name">${escHtml(d.name || name)}</div>
2245
+ <div class="ing-verdict-pill ${safetyClass}">${safetyLabel}</div>
2246
+ <div class="ing-row">
2247
+ <div class="ing-row-label">What it is</div>
2248
+ <div class="ing-row-text">${escHtml(d.what_it_is || 'β€”')}</div>
2249
+ </div>
2250
+ <div class="ing-row">
2251
+ <div class="ing-row-label">Why it's used</div>
2252
+ <div class="ing-row-text">${escHtml(d.why_used || 'β€”')}</div>
2253
+ </div>
2254
+ <div class="ing-row">
2255
+ <div class="ing-row-label">Health impact</div>
2256
+ <div class="ing-row-text">${escHtml(d.health_impact || 'β€”')}</div>
2257
+ </div>
2258
+ ${d.persona_note ? `<div class="ing-row">
2259
+ <div class="ing-row-label">For ${escHtml(currentPersona)}</div>
2260
+ <div class="ing-row-text">${escHtml(d.persona_note)}</div>
2261
+ </div>` : ''}
2262
+ ${d.curiosity ? `<div class="ing-curiosity">πŸ’‘ ${escHtml(d.curiosity)}</div>` : ''}
2263
+ `;
2264
+ } catch(e) {
2265
+ content.innerHTML = `<div class="ing-loading" style="color:#C94B1E">Could not load ingredient info. Try again.</div>`;
2266
+ }
2267
+ }
2268
+
2269
+ function closeIngSheet(e) {
2270
+ if (!e || e.target.classList.contains('ing-backdrop') || e.target === document.getElementById('ing-sheet')) {
2271
+ document.getElementById('ing-sheet').classList.remove('open');
2272
+ }
2273
+ }
2274
+
2275
+ // ── ONBOARDING ──────────────────────────────────────────────────────
2276
+ const OB_SLIDES = [
2277
+ {
2278
+ icon: '🌿',
2279
+ title: 'Know exactly what you eat',
2280
+ desc: 'Scan any food label β€” ingredients, nutrients, additives β€” get a clear health report in seconds.',
2281
+ },
2282
+ {
2283
+ icon: 'πŸ“Έ',
2284
+ title: 'No barcode needed',
2285
+ desc: 'Even blurry photos work. Our AI reads and enhances the label automatically.',
2286
+ },
2287
+ {
2288
+ icon: '🎯',
2289
+ title: 'Personalised for you',
2290
+ desc: 'Pick your eating profile so every scan is filtered through what matters to your body.',
2291
+ personaPicker: true,
2292
+ },
2293
+ ];
2294
+ let obStep = 0;
2295
+
2296
+ function initOnboarding() {
2297
+ const seen = localStorage.getItem('eatlytic_ob_done');
2298
+ if (seen) return;
2299
+ obStep = 0;
2300
+ renderObSlide();
2301
+ document.getElementById('onboarding').classList.remove('hidden');
2302
+ }
2303
+
2304
+ function renderObSlide() {
2305
+ const slide = OB_SLIDES[obStep];
2306
+ const slidesEl = document.querySelector('.ob-slides');
2307
+ const dotsEl = document.getElementById('ob-dots');
2308
+ const nextBtn = document.getElementById('ob-next-btn');
2309
+
2310
+ // Dots
2311
+ dotsEl.innerHTML = OB_SLIDES.map((_, i) =>
2312
+ `<div class="ob-dot ${i === obStep ? 'active' : ''}"></div>`
2313
+ ).join('');
2314
+
2315
+ // Button label
2316
+ nextBtn.textContent = obStep < OB_SLIDES.length - 1 ? 'Next β†’' : 'Start Scanning 🌿';
2317
+
2318
+ // Slide content
2319
+ let personaHTML = '';
2320
+ if (slide.personaPicker) {
2321
+ const personas = ['🏠 General','πŸ’ͺ Gym','πŸ‘Ά Baby','✨ Skin','πŸ₯‘ Keto','❀️ Heart'];
2322
+ personaHTML = `<div class="ob-persona-row">${
2323
+ personas.map(p => {
2324
+ const val = p.split(' ')[1];
2325
+ return `<div class="ob-persona-chip ${currentPersona===val?'selected':''}"
2326
+ onclick="obPickPersona(this,'${val}')">${p}</div>`;
2327
+ }).join('')
2328
+ }</div>`;
2329
+ }
2330
+
2331
+ slidesEl.innerHTML = `
2332
+ <div class="ob-icon">${slide.icon}</div>
2333
+ <div class="ob-title">${slide.title}</div>
2334
+ <div class="ob-desc">${slide.desc}</div>
2335
+ ${personaHTML}
2336
+ `;
2337
+ }
2338
+
2339
+ function obPickPersona(el, val) {
2340
+ document.querySelectorAll('.ob-persona-chip').forEach(c => c.classList.remove('selected'));
2341
+ el.classList.add('selected');
2342
+ currentPersona = val;
2343
+ localStorage.setItem('eatlytic_persona', val);
2344
+ // Sync main persona chips
2345
+ document.querySelectorAll('#persona-row .chip').forEach(c => {
2346
+ c.classList.toggle('active', c.textContent.includes(val));
2347
+ });
2348
+ }
2349
+
2350
+ function nextOnboardStep() {
2351
+ if (obStep < OB_SLIDES.length - 1) {
2352
+ obStep++;
2353
+ renderObSlide();
2354
+ } else {
2355
+ closeOnboarding();
2356
+ }
2357
+ }
2358
+
2359
+ function closeOnboarding() {
2360
+ localStorage.setItem('eatlytic_ob_done', '1');
2361
+ document.getElementById('onboarding').classList.add('hidden');
2362
+ }
2363
+
2364
+ // ── BARCODE SCANNER ─────────────────────────────────────────────────
2365
+ let barcodeReader = null;
2366
+ let barcodeStream = null;
2367
+
2368
+ async function startBarcodeScanner() {
2369
+ const overlay = document.getElementById('barcode-overlay');
2370
+ overlay.classList.add('open');
2371
+
2372
  try {
2373
+ const ZXing = window.ZXingBrowser;
2374
+ if (!ZXing) { showToast('Barcode library loading, try again'); overlay.classList.remove('open'); return; }
2375
+
2376
+ barcodeReader = new ZXing.BrowserMultiFormatReader();
2377
+ const videoEl = document.getElementById('barcode-video');
2378
+
2379
+ barcodeStream = await barcodeReader.decodeFromConstraints(
2380
+ { video: { facingMode: 'environment' } },
2381
+ videoEl,
2382
+ async (result, err) => {
2383
+ if (result) {
2384
+ const code = result.getText();
2385
+ stopBarcodeScanner();
2386
+ await handleBarcodeResult(code);
2387
+ }
2388
+ }
2389
+ );
2390
+ } catch(e) {
2391
+ showToast('Camera not available for barcode scan');
2392
+ overlay.classList.remove('open');
2393
+ }
2394
  }
2395
+
2396
+ function stopBarcodeScanner() {
2397
+ try { if (barcodeReader) barcodeReader.reset(); } catch {}
2398
+ try { if (barcodeStream) barcodeStream.getTracks().forEach(t => t.stop()); } catch {}
2399
+ barcodeReader = null; barcodeStream = null;
2400
+ document.getElementById('barcode-overlay').classList.remove('open');
2401
+ }
2402
+
2403
+ async function handleBarcodeResult(barcode) {
2404
+ showToast(`πŸ“¦ Barcode: ${barcode} β€” fetching product info…`);
2405
+ // Query Open Food Facts API (free, no key required)
2406
+ try {
2407
+ const res = await fetch(`https://world.openfoodfacts.org/api/v0/product/${barcode}.json`);
2408
+ const data = await res.json();
2409
+ if (data.status !== 1) {
2410
+ showToast('Product not found β€” please scan the label instead');
2411
+ return;
2412
+ }
2413
+ const p = data.product;
2414
+ // Build a text blob that mimics a scanned label so the existing analyze pipeline works
2415
+ const labelText = [
2416
+ p.product_name || p.product_name_en || '',
2417
+ p.brands || '',
2418
+ p.ingredients_text_en || p.ingredients_text || '',
2419
+ p.nutriments ? `Calories: ${p.nutriments['energy-kcal_100g']||''} kcal, Protein: ${p.nutriments.proteins_100g||''}g, Fat: ${p.nutriments.fat_100g||''}g, Carbs: ${p.nutriments.carbohydrates_100g||''}g, Sugar: ${p.nutriments.sugars_100g||''}g, Sodium: ${p.nutriments.sodium_100g ? Math.round(p.nutriments.sodium_100g*1000) : ''}mg` : '',
2420
+ ].filter(Boolean).join('\n');
2421
+
2422
+ showToast(`βœ… Found: ${p.product_name || 'Product'} β€” analysing…`);
2423
+ // Create a minimal 1x1 placeholder image (the backend requires a file upload)
2424
+ const canvas = document.createElement('canvas');
2425
+ canvas.width = 1; canvas.height = 1;
2426
+ canvas.toBlob(async (imgBlob) => {
2427
+ const fd = new FormData();
2428
+ fd.append('image', imgBlob, 'barcode.jpg');
2429
+ fd.append('persona', currentPersona);
2430
+ fd.append('language', currentLang);
2431
+ fd.append('age_group', 'adult');
2432
+ fd.append('product_category', p.categories || 'general');
2433
+ fd.append('extracted_text', labelText); // bypass OCR with pre-filled text
2434
+
2435
+ document.getElementById('loader').style.display = 'flex';
2436
+ document.getElementById('loader-step').textContent = 'Processing barcode product…';
2437
+
2438
+ try {
2439
+ const analyzeRes = await fetch(`${API}/analyze`, { method:'POST', body: fd, signal: currentAbortCtrl?.signal });
2440
+ const result = await analyzeRes.json();
2441
+ document.getElementById('loader').style.display = 'none';
2442
+ if (result.error) { showError('⚠️', 'Analysis Failed', result.message || result.error); return; }
2443
+ lastResult = result;
2444
+ addToHistory(result);
2445
+ renderReport(result);
2446
+ } catch(e) {
2447
+ document.getElementById('loader').style.display = 'none';
2448
+ showError('❌', 'Failed', 'Could not analyse barcode product.');
2449
+ }
2450
+ }, 'image/jpeg');
2451
+ } catch(e) {
2452
+ showToast('Could not look up barcode β€” scan the label instead');
2453
+ }
2454
+ }
2455
+
2456
  function showToast(msg = 'βœ… Done!') {
2457
  const t = document.getElementById('toast');
2458
  t.textContent = msg;