Nekochu's picture
fix: light yellow text for Used by label, better contrast on dark cards
b344d6a
import gradio as gr
import requests
import itertools
from collections import Counter
# HuggingFace valid colors
VALID_COLORS = ['red', 'yellow', 'green', 'blue', 'indigo', 'purple', 'pink', 'gray']
# Color CSS mappings
COLOR_MAP = {
'red': 'rgb(220 38 38)',
'yellow': 'rgb(234 179 8)',
'green': 'rgb(22 163 74)',
'blue': 'rgb(37 99 235)',
'indigo': 'rgb(79 70 229)',
'purple': 'rgb(147 51 234)',
'pink': 'rgb(236 72 153)',
'gray': 'rgb(107 114 128)'
}
def fetch_space_colors(orgs_str="Luminia"):
"""Fetch all color combinations from multiple orgs/users (comma-separated)"""
used_colors = []
color_to_spaces = {}
orgs = [o.strip() for o in orgs_str.split(",") if o.strip()]
if not orgs:
orgs = ["Luminia"]
from huggingface_hub import list_spaces
for org in orgs:
print(f"πŸ” Fetching spaces for: {org}...")
try:
spaces_list = list(list_spaces(author=org))
spaces = [(space.id.split('/')[-1], org) for space in spaces_list]
print(f"πŸ“¦ {org}: {len(spaces)} spaces")
except Exception as e:
print(f"❌ {org}: {e}")
continue
for space_name, owner in spaces:
try:
url = f"https://huggingface.co/spaces/{owner}/{space_name}/raw/main/README.md"
response = requests.get(url, timeout=5)
if response.status_code == 200:
content = response.text
color_from = None
color_to = None
for line in content.split('\n'):
if line.startswith('colorFrom:'):
color_from = line.split(':')[1].strip()
elif line.startswith('colorTo:'):
color_to = line.split(':')[1].strip()
if color_from and color_to:
label = f"{owner}/{space_name}"
print(f"βœ… {label}: {color_from} β†’ {color_to}")
used_colors.append((color_from, color_to))
key = (color_from, color_to)
if key not in color_to_spaces:
color_to_spaces[key] = []
color_to_spaces[key].append(label)
else:
print(f"⚠️ {owner}/{space_name}: Missing color metadata")
else:
print(f"❌ {owner}/{space_name}: HTTP {response.status_code}")
except Exception as e:
print(f"❌ {owner}/{space_name}: {e}")
print(f"πŸ“Š Total colors found across {len(orgs)} org(s): {len(used_colors)}")
return used_colors, color_to_spaces
def get_unique_combinations(orgs_str="Luminia"):
"""Get all possible unique combinations across multiple orgs"""
used_colors, color_to_spaces = fetch_space_colors(orgs_str)
used_set = set(used_colors)
all_combos = list(itertools.product(VALID_COLORS, repeat=2))
available = [combo for combo in all_combos if combo not in used_set]
return list(used_set), available, color_to_spaces
def create_card_html(color_from, color_to, title="Preview Space", emoji="🎨", status="Available"):
"""Create HF space card preview HTML matching exact HuggingFace styling"""
from_rgb = COLOR_MAP[color_from]
to_rgb = COLOR_MAP[color_to]
return f"""
<article style="position: relative; width: 100%;">
<a href="#" style="
background: linear-gradient(to bottom right, {from_rgb}, {to_rgb});
position: relative;
z-index: 0;
display: flex;
flex-direction: column;
justify-content: space-between;
overflow: hidden;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
height: 120px;
border-radius: 0.75rem;
transition: filter 0.2s;
text-decoration: none;
width: 100%;
" onmouseover="this.style.boxShadow='inset 0 2px 4px 0 rgba(0, 0, 0, 0.06); this.style.filter=brightness(1.1)'" onmouseout="this.style.boxShadow='0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)'; this.style.filter='brightness(1)'">
<div style="
background: linear-gradient(to bottom right, rgba(0, 0, 0, 0.2), transparent);
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 50%;
"></div>
<header style="
background: linear-gradient(to top, rgba(0, 0, 0, 0.04), transparent 10%);
display: flex;
gap: 0.25rem;
overflow: hidden;
border-radius: 0 0 1rem 1rem;
background-color: rgba(0, 0, 0, 0.04);
font-size: 0.725rem;
padding-left: 0.875rem;
padding-right: 0.875rem;
height: 34px;
">
<div style="margin-top: 8.5px; display: flex; height: 16px; flex-shrink: 0; flex-grow: 0; flex-basis: auto; flex-wrap: wrap; gap: 0.375rem;">
<div style="
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.25rem;
border-radius: 0.125rem;
border: 1px solid rgba(255, 255, 255, 0.05);
background-color: rgba(255, 255, 255, 0.1);
padding-left: 0.125rem;
padding-right: 0.125rem;
font-family: ui-monospace, monospace;
line-height: 1.25;
color: white;
opacity: 0.9;
">
<span>{status}</span>
</div>
</div>
</header>
<main style="padding-left: 1rem; padding-right: 1rem; position: relative;">
<div style="color: white; font-size: 1.125rem;">
<div style="display: flex; align-items: center; justify-content: space-between; gap: 0.75rem;">
<div style="margin-bottom: 0.125rem; display: flex; align-items: center; justify-content: flex-start; gap: 0.375rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; line-height: 1.25;">
<h4 style="
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
text-align: left;
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.4));
margin: 0;
font-size: 1rem;
" title="{title}">{title}</h4>
<div style="line-height: 1; filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.4));">{emoji}</div>
</div>
</div>
<p style="
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
text-align: left;
font-size: 0.8375rem;
line-height: 1.15rem;
color: rgb(229 231 235);
opacity: 0.85;
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.2));
margin: 0;
">
{color_from} β†’ {color_to}
</p>
</div>
</main>
<footer style="
display: flex;
align-items: center;
justify-content: space-between;
font-size: 0.875rem;
padding-left: 0.875rem;
padding-right: 0.875rem;
height: 34px;
background: linear-gradient(to bottom, rgba(0, 0, 0, 0.04), transparent 20%);
border-radius: 1rem;
background-color: rgba(0, 0, 0, 0.04);
padding-bottom: 0.75rem;
padding-top: 0.625rem;
font-size: 0.75rem;
">
<span style="font-family: ui-monospace, monospace; font-size: 0.725rem; color: rgb(209 213 219);">Preview</span>
</footer>
</a>
</article>
"""
def show_colors(orgs_str="Luminia"):
"""Show used and available color combinations across orgs"""
if not orgs_str:
orgs_str = "Luminia"
used, available, color_to_spaces = get_unique_combinations(orgs_str)
# Style tag for 2x2 grid and collapsible section
html = """
<style>
.card-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1.25rem;
}
h2.section-title {
font-size: 1.25rem;
font-weight: 700;
margin-bottom: 1rem;
margin-top: 0;
}
.container {
padding: 20px;
}
details {
margin-bottom: 2.5rem;
border: 1px solid #e5e7eb;
border-radius: 0.5rem;
padding: 1rem;
}
summary {
cursor: pointer;
font-size: 1.25rem;
font-weight: 700;
padding: 0.5rem;
user-select: none;
}
summary:hover {
background-color: #f9fafb;
}
.space-names {
font-size: 0.75rem;
color: #6b7280;
margin-top: 0.25rem;
}
</style>
<div class="container">
"""
# Used combinations (collapsible, show all unique)
used_unique = list(dict.fromkeys(used)) # Remove duplicates while preserving order
html += '<details open style="margin-bottom: 2.5rem; border: 1px solid #e5e7eb; border-radius: 0.5rem; padding: 1rem;">'
html += '<summary style="cursor: pointer; font-size: 1.25rem; font-weight: 700; padding: 0.5rem; user-select: none;">πŸ“‹ Currently Used Combinations (click to collapse)</summary>'
html += '<div class="card-grid" style="margin-top: 1rem;">'
for cf, ct in used_unique:
spaces = color_to_spaces.get((cf, ct), [])
spaces_text = ", ".join(spaces) if spaces else ""
title = f"{cf}β†’{ct}<br><small style='font-size: 0.75rem; color: #fde68a; text-shadow: 0 1px 2px rgba(0,0,0,0.6);'>Used by: {spaces_text}</small>"
html += create_card_html(cf, ct, title, "", "Used")
html += '</div></details>'
# Available combinations (2x2 grid)
html += '<div>'
html += '<h2 class="section-title">✨ Available Unique Combinations</h2>'
html += '<div class="card-grid">'
for cf, ct in available[:20]:
html += create_card_html(cf, ct, f"{cf}β†’{ct}", "", "Available")
html += '</div></div>'
html += '</div>'
return html
def get_available_colors_api(orgs_str="Luminia,WeReCooking"):
"""API endpoint: returns available (unused) color combinations as JSON-friendly list.
Input: comma-separated org/usernames (default: Luminia,WeReCooking).
Output: dict with 'used' and 'available' color pairs.
"""
if not orgs_str:
orgs_str = "Luminia,WeReCooking"
used, available, color_to_spaces = get_unique_combinations(orgs_str)
used_list = [{"colorFrom": cf, "colorTo": ct, "spaces": color_to_spaces.get((cf, ct), [])} for cf, ct in dict.fromkeys(used)]
available_list = [{"colorFrom": cf, "colorTo": ct} for cf, ct in available]
return {
"orgs": orgs_str,
"used_count": len(used_list),
"available_count": len(available_list),
"used": used_list,
"available": available_list,
}
# Gradio Interface
with gr.Blocks(title="HF Space Color Picker") as demo:
gr.Markdown("# 🎨 HuggingFace Space Color Picker\nPreview and discover unique color combinations for your HF Spaces.\n\nAPI: call `get_available_colors` with `orgs` = comma-separated usernames (default: `Luminia,WeReCooking`).")
with gr.Row():
orgs_input = gr.Textbox(value="Luminia,WeReCooking", label="Orgs / Usernames (comma-separated)", scale=3)
refresh_btn = gr.Button("πŸ”„ Scan Colors", scale=1, variant="primary")
output_html = gr.HTML()
refresh_btn.click(show_colors, inputs=[orgs_input], outputs=[output_html])
# API endpoint for programmatic access
api_output = gr.JSON(visible=False)
api_btn = gr.Button(visible=False)
api_btn.click(
fn=get_available_colors_api,
inputs=[orgs_input],
outputs=[api_output],
api_name="get_available_colors",
)
demo.load(fn=lambda: show_colors("Luminia,WeReCooking"), outputs=[output_html])
if __name__ == "__main__":
demo.launch(mcp_server=True, show_error=True, theme=gr.themes.Soft())