"""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%}
{%config%}
{%scripts%}
{%renderer%}
'''
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",
)