huggimon / src /card_html.py
ImTamsi's picture
feat: initial HuggiMon Space MVP
f4e4f7c
Raw
History Blame Contribute Delete
6.96 kB
"""Generate the HTML/CSS card preview for the Gradio UI."""
from src.scoring import CardData
STYLE_THEMES = {
"Starter": {
"bg": "linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%)",
"accent": "#6366f1",
"accent2": "#818cf8",
"text": "#1f2937",
"subtext": "#4b5563",
"card": "#ffffff",
"shine": "rgba(255,255,255,0.6)",
},
"Legendary": {
"bg": "linear-gradient(135deg, #fff1c1 0%, #ff8a00 100%)",
"accent": "#b45309",
"accent2": "#f59e0b",
"text": "#451a03",
"subtext": "#78350f",
"card": "#fffbeb",
"shine": "rgba(255,255,255,0.7)",
},
"Dark Mode": {
"bg": "linear-gradient(135deg, #0f172a 0%, #4c1d95 100%)",
"accent": "#22d3ee",
"accent2": "#a855f7",
"text": "#f8fafc",
"subtext": "#cbd5e1",
"card": "#1e293b",
"shine": "rgba(255,255,255,0.15)",
},
"Researcher": {
"bg": "linear-gradient(135deg, #e0f2fe 0%, #0ea5e9 100%)",
"accent": "#0369a1",
"accent2": "#38bdf8",
"text": "#082f49",
"subtext": "#0c4a6e",
"card": "#f0f9ff",
"shine": "rgba(255,255,255,0.6)",
},
"Builder": {
"bg": "linear-gradient(135deg, #ffedd5 0%, #f97316 100%)",
"accent": "#c2410c",
"accent2": "#fb923c",
"text": "#431407",
"subtext": "#7c2d12",
"card": "#fff7ed",
"shine": "rgba(255,255,255,0.6)",
},
"Esport": {
"bg": "linear-gradient(135deg, #1a0505 0%, #dc2626 100%)",
"accent": "#facc15",
"accent2": "#ef4444",
"text": "#fef2f2",
"subtext": "#fecaca",
"card": "#450a0a",
"shine": "rgba(255,255,255,0.2)",
},
}
RARITY_GLYPHS = {
"Common": "★",
"Rare": "★★",
"Epic": "★★★",
"Legendary": "★★★★",
}
def _stat_bar(label: str, value: int, theme: dict) -> str:
return f"""
<div class="hstat" style="margin: 6px 0;">
<div style="display:flex;justify-content:space-between;font-size:12px;font-weight:700;color:{theme['text']};">
<span>{label}</span><span>{value}</span>
</div>
<div style="background:{theme['shine']};border-radius:999px;height:8px;overflow:hidden;margin-top:3px;">
<div style="width:{value}%;background:{theme['accent']};height:100%;border-radius:999px;transition:width 0.8s ease;"></div>
</div>
</div>
"""
def render_card_html(card: CardData, style: str = "Starter", share_url: str = "") -> str:
theme = STYLE_THEMES.get(style, STYLE_THEMES["Starter"])
rarity_glyph = RARITY_GLYPHS.get(card.rarity, "★")
attacks = " · ".join(card.attacks) if card.attacks else "Learning"
stats_html = "".join(
[
_stat_bar("MODEL", card.stats.model, theme),
_stat_bar("DATA", card.stats.data, theme),
_stat_bar("SPACE", card.stats.space, theme),
_stat_bar("IMPACT", card.stats.impact, theme),
_stat_bar("COMMUNITY", card.stats.community, theme),
_stat_bar("DOCS", card.stats.docs, theme),
]
)
share_hint = f"""
<div style="margin-top:14px;font-size:11px;color:{theme['subtext']};text-align:center;">
Share: <code style="background:{theme['shine']};padding:2px 6px;border-radius:4px;">{share_url or f'/api/card/{card.username}.png'}</code>
</div>
"""
return f"""
<div class="hcard" style="
width:380px;
border-radius:20px;
padding:22px;
background:{theme['bg']};
color:{theme['text']};
font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;
box-shadow:0 25px 50px -12px rgba(0,0,0,0.25);
border:1px solid {theme['shine']};
position:relative;
overflow:hidden;
">
<div style="position:absolute;top:0;left:0;right:0;height:6px;background:{theme['accent']};"></div>
<div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:12px;">
<div>
<div style="font-size:11px;letter-spacing:1.5px;font-weight:800;color:{theme['accent']};text-transform:uppercase;">AI Trainer Card</div>
<h2 style="margin:4px 0 0;font-size:28px;letter-spacing:-0.5px;">{card.display_name}</h2>
</div>
<div style="text-align:right;">
<div style="font-size:22px;color:{theme['accent']};">{rarity_glyph}</div>
<div style="font-size:11px;font-weight:700;color:{theme['subtext']};">{card.rarity}</div>
</div>
</div>
<div style="display:flex;gap:12px;margin-bottom:14px;">
<div style="flex:1;background:{theme['card']};border-radius:14px;padding:12px;text-align:center;box-shadow:0 4px 6px -1px rgba(0,0,0,0.05);">
<div style="font-size:10px;font-weight:700;color:{theme['accent']};text-transform:uppercase;">Type</div>
<div style="font-size:16px;font-weight:800;">{card.type}</div>
</div>
<div style="flex:1;background:{theme['card']};border-radius:14px;padding:12px;text-align:center;box-shadow:0 4px 6px -1px rgba(0,0,0,0.05);">
<div style="font-size:10px;font-weight:700;color:{theme['accent']};text-transform:uppercase;">Level</div>
<div style="font-size:22px;font-weight:900;color:{theme['accent']};">{card.level}</div>
</div>
</div>
<div style="background:{theme['card']};border-radius:16px;padding:16px;box-shadow:0 4px 6px -1px rgba(0,0,0,0.05);">
{stats_html}
</div>
<div style="margin-top:14px;display:grid;grid-template-columns:1fr 1fr;gap:10px;">
<div style="background:{theme['card']};border-radius:12px;padding:10px;">
<div style="font-size:9px;font-weight:700;color:{theme['accent']};text-transform:uppercase;">Attacks</div>
<div style="font-size:12px;font-weight:700;">{attacks}</div>
</div>
<div style="background:{theme['card']};border-radius:12px;padding:10px;">
<div style="font-size:9px;font-weight:700;color:{theme['accent']};text-transform:uppercase;">Passive</div>
<div style="font-size:12px;font-weight:700;">{card.passive}</div>
</div>
</div>
<div style="margin-top:12px;background:{theme['card']};border-radius:12px;padding:10px;text-align:center;">
<div style="font-size:9px;font-weight:700;color:{theme['accent']};text-transform:uppercase;">Evolution</div>
<div style="font-size:12px;font-weight:700;">{card.evolution}</div>
</div>
<div style="margin-top:14px;display:flex;justify-content:space-between;font-size:10px;color:{theme['subtext']};">
<span>🤗 {card.total_models} models · {card.total_datasets} datasets · {card.total_spaces} spaces</span>
<span>❤️ {card.total_likes} · ⬇️ {card.total_downloads}</span>
</div>
{share_hint}
</div>
"""