class TemplatedHTML(gr.HTML): """A gr.HTML wrapper that re-renders a template against a state dict.""" html_template: ClassVar[str] = "" css_template: ClassVar[str] = "" def __init__(self, value: dict[str, Any] | None = None, **kwargs): self._state: dict[str, Any] = value or {} super().__init__(value=self.render(self._state), **kwargs) @classmethod def render(cls, state: dict[str, Any]) -> str: body = cls._render_body(state) return f"{body}" @classmethod def _render_body(cls, state: dict[str, Any]) -> str: # Default: simple ${value.X} substitution against the state dict. return substitute(cls.html_template, state) from typing import Callable, Literal, Sequence, Any, TYPE_CHECKING from gradio.blocks import Block if TYPE_CHECKING: from gradio.components import Timer from gradio.components.base import Component class RecipeHero(TemplatedHTML): css_template = """ .cwm-hero { background: #fffbf0; border: 1px solid #d8c9ad; border-radius: 16px; padding: 32px; display: grid; grid-template-columns: 1fr 1fr; gap: 28px; box-shadow: 0 10px 28px rgba(107, 74, 42, 0.10); } .cwm-hero img { width: 100%; height: 320px; object-fit: cover; border-radius: 12px; background: #efe3c8; } .cwm-hero h1 { font-family: 'Lora', serif; font-size: 38px; color: #6b4a2a; margin: 0 0 8px; } .cwm-hero .meta { color: #8a6a3a; font-size: 14px; letter-spacing: 0.04em; text-transform: uppercase; margin-bottom: 18px; } .cwm-hero .visual { font-family: 'Lora', serif; font-style: italic; color: #6b4a2a; font-size: 17px; line-height: 1.55; } @media (max-width: 720px) { .cwm-hero { grid-template-columns: 1fr; } } """ @classmethod def _render_body(cls, state: dict[str, Any]) -> str: name = html.escape(state.get("name") or "Pick a dish to get started") cuisine = html.escape(state.get("cuisine") or "") servings = state.get("servings") or 0 time = state.get("total_time_minutes") or 0 visual = html.escape(state.get("final_dish_visual") or "") img_b64 = state.get("final_dish_image_b64") or "" img_path = state.get("final_dish_image_path") or "" if img_b64: img_tag = f'final dish' elif img_path: img_tag = f'final dish' else: img_tag = '
Image will appear here
' return f"""
{img_tag}
{cuisine} · {servings} servings · {time} min

{name}

{visual}

""" from typing import Callable, Literal, Sequence, Any, TYPE_CHECKING from gradio.blocks import Block if TYPE_CHECKING: from gradio.components import Timer from gradio.components.base import Component class IngredientChips(TemplatedHTML): css_template = """ .cwm-chips { display: flex; flex-wrap: wrap; gap: 8px; padding: 14px 0; } /* Forzamos el color del texto y nos aseguramos de que no se herede un color claro */ .cwm-chips .cwm-chip { background: #fbe2d2 !important; color: #6b4a2a !important; border: 1px solid #d8c9ad !important; border-radius: 999px; padding: 6px 14px; font-size: 14px; font-family: 'Inter', sans-serif; display: inline-block; } /* Forzamos el color para los chips de ingredientes faltantes */ .cwm-chips .cwm-chip.missing { background: #fbe2d2 !important; border-color: #c47a52 !important; } .cwm-chips-empty { color: #6b4a2a !important; font-style: italic; padding: 14px 0; } """ @classmethod def _render_body(cls, state: dict[str, Any]) -> str: have = state.get("have") or [] missing = state.get("missing") or [] if not have and not missing: return '
No ingredients yet — upload a fridge photo.
' parts = ['
'] for ing in have: parts.append(f'{html.escape(str(ing))}') for ing in missing: parts.append(f'missing: {html.escape(str(ing))}') parts.append('
') return "".join(parts) from typing import Callable, Literal, Sequence, Any, TYPE_CHECKING from gradio.blocks import Block if TYPE_CHECKING: from gradio.components import Timer from gradio.components.base import Component class DishOptions(TemplatedHTML): css_template = """ .cwm-options { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; } .cwm-option { background: #fffbf0; border: 1px solid #d8c9ad; border-radius: 12px; padding: 18px; text-align: left; } .cwm-option h3 { font-family: 'Lora', serif; font-size: 19px; color: #6b4a2a; margin: 0 0 6px; } .cwm-option p { color: #7a5a35; font-size: 14px; line-height: 1.45; margin: 0; } @media (max-width: 720px) { .cwm-options { grid-template-columns: 1fr; } } """ @classmethod def _render_body(cls, state: dict[str, Any]) -> str: opts = state.get("options") or [] if not opts: return "" cards = [] for o in opts[:3]: name = html.escape(str(o.get("name", ""))) why = html.escape(str(o.get("why", ""))) cards.append(f'

{name}

{why}

') return f'
{"".join(cards)}
' from typing import Callable, Literal, Sequence, Any, TYPE_CHECKING from gradio.blocks import Block if TYPE_CHECKING: from gradio.components import Timer from gradio.components.base import Component class NutritionGrid(TemplatedHTML): css_template = """ .cwm-nutri-wrap { margin-top: 10px; } .cwm-nutri-title { font-family: 'Lora', serif; color: #6b4a2a; font-size: 22px; margin: 0 0 14px; } .cwm-nutri { display: grid; grid-template-columns: repeat(5, 1fr); gap: 12px; } .cwm-nutri-cell { background: #fffbf0; border: 1px solid #d8c9ad; border-radius: 10px; padding: 14px 10px; text-align: center; } .cwm-nutri-cell .v { font-family: 'Lora', serif; font-size: 24px; font-weight: 700; color: #6b4a2a; display: block; } .cwm-nutri-cell .l { font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase; color: #8a6a3a; margin-top: 4px; } @media (max-width: 720px) { .cwm-nutri { grid-template-columns: repeat(2, 1fr); } } """ @classmethod def _render_body(cls, state: dict[str, Any]) -> str: nutri = state.get("nutrition") or {} cells = [ (nutri.get("calories", 0), "kcal"), (nutri.get("protein_g", 0), "protein"), (nutri.get("carbs_g", 0), "carbs"), (nutri.get("fat_g", 0), "fat"), (nutri.get("fiber_g", 0), "fiber"), ] cell_html = "".join( f'
{html.escape(str(v))}' f'
{html.escape(label)}
' for v, label in cells ) return f"""

Per serving

{cell_html}
""" from typing import Callable, Literal, Sequence, Any, TYPE_CHECKING from gradio.blocks import Block if TYPE_CHECKING: from gradio.components import Timer from gradio.components.base import Component class VerdictBadge(TemplatedHTML): css_template = """ .cwm-verdict { display: flex; align-items: center; gap: 18px; background: #fffbf0; border-radius: 12px; padding: 18px 22px; border: 1px solid #d8c9ad; } .cwm-verdict.go { border-left: 6px solid #4f8b4a; } .cwm-verdict.wait { border-left: 6px solid #d4a23c; } .cwm-verdict.fix { border-left: 6px solid #b94a3a; } .cwm-verdict-pill { font-family: 'Lora', serif; font-weight: 700; font-size: 16px; text-transform: uppercase; letter-spacing: 0.08em; padding: 8px 16px; border-radius: 999px; color: #fffbf0; } .cwm-verdict.go .cwm-verdict-pill { background: #4f8b4a; } .cwm-verdict.wait .cwm-verdict-pill { background: #d4a23c; } .cwm-verdict.fix .cwm-verdict-pill { background: #b94a3a; } .cwm-verdict-text { font-size: 16px; color: #4a3722; line-height: 1.5; } .cwm-verdict-text small { color: #8a6a3a; display: block; margin-top: 4px; } .cwm-verdict-empty { color: #b39870; font-style: italic; padding: 14px 0; } """ @classmethod def _render_body(cls, state: dict[str, Any]) -> str: v = state.get("verdict") if not v: return '
Upload a progress photo to get a verdict.
' verdict = state.get("verdict", "wait") feedback = html.escape(state.get("feedback", "")) tip = state.get("tip") tip_html = f"{html.escape(tip)}" if tip else "" return f"""
{html.escape(verdict)}
{feedback}{tip_html}
""" from typing import Callable, Literal, Sequence, Any, TYPE_CHECKING from gradio.blocks import Block if TYPE_CHECKING: from gradio.components import Timer from gradio.components.base import Component