Spaces:
Sleeping
Sleeping
| 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()) | |