|
|
| 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"<style>{cls.css_template}</style>{body}" |
|
|
| @classmethod |
| def _render_body(cls, state: dict[str, Any]) -> str: |
| |
| 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'<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; |
| } |
| """ |
|
|
| @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 '<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; } } |
| """ |
|
|
| @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'<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); } } |
| """ |
|
|
| @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'<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; |
| } |
| """ |
|
|
| @classmethod |
| 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 |
|
|
| |