"""Dating-app themed tournament UI for g-Harmony.""" import random from dash import dcc, html import dash_bootstrap_components as dbc import plotly.graph_objects as go from src.galaxy_profiles import get_display_name def get_app_theme() -> str: """Return the full HTML template with embedded CSS.""" return ''' {%metas%} Perihelion {%favicon%} {%css%} {%app_entry%} ''' def _create_star_field(n=80): """Generate CSS star-field background as inline-styled divs.""" stars = [] for i in range(n): x = random.random() * 100 y = random.random() * 100 size = random.random() * 2 + 0.5 delay = round(random.random() * 4, 1) duration = round(random.random() * 3 + 2, 1) stars.append(html.Div(style={ "position": "absolute", "left": f"{x:.1f}%", "top": f"{y:.1f}%", "width": f"{size:.1f}px", "height": f"{size:.1f}px", "borderRadius": "50%", "background": "#fff", "animation": f"twinkle {duration}s {delay}s infinite ease-in-out", })) return html.Div( stars, style={ "position": "fixed", "top": "0", "left": "0", "right": "0", "bottom": "0", "pointerEvents": "none", "zIndex": "0", }, ) def create_galaxy_card(row_index: int, side: str = "left"): """Build a single galaxy profile card — image + name.""" name = get_display_name(row_index) btn_id = f"{side}-card-btn" return html.Button( [ html.Img( src=f"/galaxy-images/{row_index}.jpg", className="galaxy-card-image", ), html.Div(name, className="galaxy-card-name"), ], className="galaxy-card", id=btn_id, n_clicks=0, style={ "border": "none", "padding": "0", "textAlign": "left", "width": "100%", }, ) def create_arena(left_idx, right_idx): """Build the two-card arena with VS divider.""" return dbc.Row( [ dbc.Col( create_galaxy_card(left_idx, side="left"), width=5, ), dbc.Col( html.Div("VS", className="vs-divider"), width=2, className="d-flex align-items-center justify-content-center", ), dbc.Col( create_galaxy_card(right_idx, side="right"), width=5, ), ], className="g-0 align-items-stretch", style={"animation": "fadeSlideUp 0.4s ease"}, ) def create_progress_dashboard(info: dict): """Build the ELO ranking progress dashboard.""" total_comps = info.get("total_comparisons", 0) elo_values = info.get("elo_values", []) stats_row = dbc.Row( [ dbc.Col(html.Div([ html.Div(str(total_comps), className="progress-stat-value"), html.Div("COMPARISONS", className="progress-stat-label"), ], className="progress-stat"), width=12), ], className="mb-3", ) if elo_values: fig = go.Figure(data=[go.Histogram( x=elo_values, nbinsx=30, marker_color="rgba(167,139,250,0.6)", marker_line_color="rgba(167,139,250,0.8)", marker_line_width=1, )]) fig.update_layout( paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)", font_color="rgba(255,255,255,0.5)", font_family="Outfit", font_size=10, margin=dict(l=30, r=10, t=10, b=30), height=120, xaxis=dict(gridcolor="rgba(255,255,255,0.05)", title_text="ELO Rating", title_font_size=9), yaxis=dict(gridcolor="rgba(255,255,255,0.05)", title_text="Count", title_font_size=9), ) histogram = dcc.Graph(figure=fig, config={"displayModeBar": False}, style={"height": "120px"}) else: histogram = html.Div() return html.Div([stats_row, histogram], className="progress-dashboard") def create_leaderboard_rows(leaderboard_data): """Build leaderboard row elements from sorted data.""" rows = [] for i, entry in enumerate(leaderboard_data): idx = entry["id"] name = get_display_name(idx) rank = i + 1 rank_color = {1: "#FFD700", 2: "#C0C0C0", 3: "#CD7F32"}.get(rank, "rgba(255,255,255,0.4)") rows.append( html.Div( [ html.Span(str(rank), className="leaderboard-rank", style={"color": rank_color}), html.Img(src=f"/galaxy-images/{idx}.jpg", className="leaderboard-thumb"), html.Span(name, className="leaderboard-name"), html.Span(f"{entry['elo']:.0f}", className="leaderboard-elo"), ], className="leaderboard-row", ) ) return rows def create_layout(): """Assemble the complete app layout.""" return dbc.Container( [ _create_star_field(80), # Header html.Div( [ html.Div("Perihelion", className="gharmony-title text-center"), html.Div("VOTE FOR THE MOST INTERESTING GALAXY", className="gharmony-tagline text-center mt-1"), html.Div( "Left/Right arrow keys to choose", style={ "fontFamily": "'Outfit', sans-serif", "fontSize": "0.65rem", "fontWeight": "400", "color": "rgba(255,255,255,0.2)", "letterSpacing": "1px", "marginTop": "8px", }, ), ], className="text-center pt-4 pb-3", style={"position": "relative", "zIndex": "10"}, ), # Arena html.Div(id="arena-container", style={"position": "relative", "zIndex": "10"}), # Spacer html.Div(style={"height": "24px"}), # Progress dashboard html.Div(id="progress-dashboard-container", style={"position": "relative", "zIndex": "10"}), # Spacer html.Div(style={"height": "24px"}), # Leaderboard html.Div( [ html.Div( [ html.Span("LEADERBOARD"), html.I(className="fas fa-chevron-down", id="leaderboard-arrow", style={"transition": "transform 0.3s", "fontSize": "0.65rem"}), ], className="leaderboard-header", id="leaderboard-toggle", n_clicks=0, ), html.Div(id="leaderboard-body", style={"display": "none"}), ], className="leaderboard-container mb-4", style={"position": "relative", "zIndex": "10"}, ), # Stores dcc.Store(id="current-pair", data=None), dcc.Store(id="comparison-count", data=0), dcc.Store(id="elo-info", data={}), dcc.Store(id="session-id", data=""), # Interval for progress updates dcc.Interval(id="progress-interval", interval=10000, n_intervals=0), ], fluid=True, className="py-0", )