File size: 4,438 Bytes
89d9642 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 | """Product cards and the verdict panel."""
from __future__ import annotations
import streamlit as st
from components import badges
from nutriweb.reco.engine import Recommendation
from nutriweb.reco.filters import Verdict
def product_card(product: dict, *, key: str, on_open, cta: str = "View details") -> None:
"""A search-result card with an action button beneath it."""
st.markdown(
f"""<div class="nw-card">
{badges.thumb(product)}
<h4>{badges.title_of(product)}</h4>
<div class="nw-brand">{badges.brand_of(product) or " "}</div>
{badges.badge_row(product)}
{badges.health_meter(product.get("health_score"))}
</div>""",
unsafe_allow_html=True,
)
if st.button(cta, key=key, width='stretch'):
on_open(product["code"])
def recommendation_card(rec: Recommendation, *, key: str, on_open) -> None:
"""A recommendation card. Always states why this specific swap was chosen."""
product = rec.product
warning = ""
if rec.warnings:
warning = (
'<div class="nw-why" style="color:#8A6D00">⚠︎ '
+ badges.esc("; ".join(rec.warnings))
+ "</div>"
)
st.markdown(
f"""<div class="nw-card">
{badges.thumb(product)}
<h4>{badges.title_of(product)}</h4>
<div class="nw-brand">{badges.brand_of(product) or " "}</div>
<span class="nw-gain">+{rec.health_gain:.0f} health</span>
{badges.badge_row(product)}
<div class="nw-why">{badges.esc(rec.why)}</div>
{warning}
</div>""",
unsafe_allow_html=True,
)
if st.button("View details", key=key, width='stretch'):
on_open(product["code"])
def verdict_panel(verdict: Verdict, profile_is_empty: bool = False) -> None:
"""The "is this for you" panel.
Driven by the same `filters.evaluate` the engine uses, so what the user is
told here always matches what the recommender actually did.
"""
if profile_is_empty:
st.markdown(
'<div class="nw-verdict v-warn"><h5>No profile set</h5>'
"<div>Add your allergens and dietary preferences to see whether this "
"product suits you.</div></div>",
unsafe_allow_html=True,
)
return
tone = "v-block" if verdict.blockers else ("v-warn" if verdict.warnings else "v-ok")
icon = "✕" if verdict.blockers else ("!" if verdict.warnings else "✓")
items = "".join(
f"<li>{badges.esc(m)}</li>" for m in (*verdict.blockers, *verdict.warnings)
)
body = f"<ul>{items}</ul>" if items else "<div>Nothing in your profile conflicts with this product.</div>"
st.markdown(
f'<div class="nw-verdict {tone}"><h5>{icon} {badges.esc(verdict.summary)}</h5>{body}</div>',
unsafe_allow_html=True,
)
def nutrient_table(product: dict, reference: dict | None = None) -> str:
"""Per-100g nutrient table with bars scaled to a reference intake.
Bars are relative to a rough per-100g share of an adult reference intake,
so they answer "is this a lot?" rather than just restating the number.
"""
rows = [
("Energy", "energy_kcal_100g", "kcal", 200.0),
("Fat", "fat_100g", "g", 17.5),
(" of which saturates", "saturated_fat_100g", "g", 5.0),
("Carbohydrates", "carbohydrates_100g", "g", 45.0),
(" of which sugars", "sugars_100g", "g", 22.5),
("Fibre", "fiber_100g", "g", 7.5),
("Protein", "proteins_100g", "g", 25.0),
("Salt", "salt_derived", "g", 1.5),
]
from nutriweb.util import num
out = ['<table class="nw-nutri">']
for label, col, unit, high in rows:
value = num(product.get(col))
if value is None:
out.append(
f'<tr><td class="n">{badges.esc(label)}</td>'
f'<td class="v nw-sub">—</td><td></td></tr>'
)
continue
pct = max(2.0, min(100.0, value / high * 100.0))
over = " over" if value > high else ""
out.append(
f'<tr><td class="n">{badges.esc(label)}</td>'
f'<td class="v">{value:.1f} {unit}</td>'
f'<td><div class="nw-bar"><span class="{over.strip()}" '
f'style="width:{pct:.0f}%"></span></div></td></tr>'
)
out.append("</table>")
return "".join(out)
|