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 "&nbsp;"}</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 "&nbsp;"}</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)