Spaces:
Running on Zero
Running on Zero
| 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) | |
| def render(cls, state: dict[str, Any]) -> str: | |
| body = cls._render_body(state) | |
| return f"<style>{cls.css_template}</style>{body}" | |
| 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; } } | |
| """ | |
| 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'<img src="data:image/png;base64,{img_b64}" alt="final dish"/>' | |
| elif img_path: | |
| img_tag = f'<img src="/file={html.escape(img_path)}" alt="final dish"/>' | |
| else: | |
| img_tag = '<div style="background:#efe3c8;border-radius:12px;height:320px;display:flex;align-items:center;justify-content:center;color:#8a6a3a;font-family:\'Lora\',serif;font-style:italic;">Image will appear here</div>' | |
| return f""" | |
| <div class="cwm-hero"> | |
| <div>{img_tag}</div> | |
| <div> | |
| <div class="meta">{cuisine} � {servings} servings � {time} min</div> | |
| <h1>{name}</h1> | |
| <p class="visual">{visual}</p> | |
| </div> | |
| </div> | |
| """ | |
| 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; | |
| } | |
| """ | |
| 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 '<div class="cwm-chips-empty">No ingredients yet � upload a fridge photo.</div>' | |
| parts = ['<div class="cwm-chips">'] | |
| for ing in have: | |
| parts.append(f'<span class="cwm-chip">{html.escape(str(ing))}</span>') | |
| for ing in missing: | |
| parts.append(f'<span class="cwm-chip missing">missing: {html.escape(str(ing))}</span>') | |
| parts.append('</div>') | |
| 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; } } | |
| """ | |
| 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'<div class="cwm-option"><h3>{name}</h3><p>{why}</p></div>') | |
| return f'<div class="cwm-options">{"".join(cards)}</div>' | |
| 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); } } | |
| """ | |
| 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'<div class="cwm-nutri-cell"><span class="v">{html.escape(str(v))}</span>' | |
| f'<div class="l">{html.escape(label)}</div></div>' | |
| for v, label in cells | |
| ) | |
| return f""" | |
| <div class="cwm-nutri-wrap"> | |
| <h3 class="cwm-nutri-title">Per serving</h3> | |
| <div class="cwm-nutri">{cell_html}</div> | |
| </div> | |
| """ | |
| 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; | |
| } | |
| """ | |
| def _render_body(cls, state: dict[str, Any]) -> str: | |
| v = state.get("verdict") | |
| if not v: | |
| return '<div class="cwm-verdict-empty">Upload a progress photo to get a verdict.</div>' | |
| verdict = state.get("verdict", "wait") | |
| feedback = html.escape(state.get("feedback", "")) | |
| tip = state.get("tip") | |
| tip_html = f"<small>{html.escape(tip)}</small>" if tip else "" | |
| return f""" | |
| <div class="cwm-verdict {html.escape(verdict)}"> | |
| <div class="cwm-verdict-pill">{html.escape(verdict)}</div> | |
| <div class="cwm-verdict-text">{feedback}{tip_html}</div> | |
| </div> | |
| """ | |
| 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 | |