"""UI components for AION Search."""
from dash import dcc, html
import dash_bootstrap_components as dbc
from src.config import TOTAL_GALAXIES
def get_app_theme() -> str:
"""Get the custom CSS theme for the app.
Returns:
HTML string with embedded CSS
"""
return '''
{%metas%}
galaxy semantic search
{%favicon%}
{%css%}
{%app_entry%}
'''
def create_header():
"""Create the app header with title and galaxy count."""
return dbc.Row([
dbc.Col([
html.Div([
html.H1("galaxy semantic search", className="galaxy-title text-center mb-1"),
html.P([
"powered by ",
html.A(
"AION-Search",
href="https://aion-search.github.io/",
target="_blank",
rel="noopener noreferrer",
style={
"color": "rgba(245, 245, 247, 0.5)",
"textDecoration": "underline"
}
)
], className="text-center mb-2",
style={"color": "rgba(245, 245, 247, 0.5)", "font-weight": "300",
"font-size": "0.8rem", "letter-spacing": "0.05em"}),
html.Div(id="galaxy-count", className="galaxy-count text-center")
], className="text-center mb-3")
])
])
def create_rmag_filter_panel():
"""Create the r_mag filter panel."""
return dbc.Row([
dbc.Col([
html.Div([
dbc.Row([
dbc.Col([
html.Div("r-mag",
style={"color": "rgba(245, 245, 247, 0.6)",
"font-size": "0.85rem",
"font-weight": "300",
"text-align": "center"})
], width=1, className="d-flex align-items-center justify-content-center", style={"padding": "0"}),
dbc.Col([
dcc.RangeSlider(
id="rmag-slider",
min=13.0,
max=20.0,
step=0.1,
value=[13.0, 20.0],
marks={13: '13', 15: '15', 17: '17', 19: '19', 20: '20'},
tooltip={"placement": "bottom", "always_visible": True},
className="rmag-slider"
)
], width=11)
], className="align-items-center", style={"margin": "0"})
], style={"padding": "0.5rem 0"})
], width=12)
], className="mt-2")
def create_search_container():
"""Create the main search input container with examples and input."""
return dbc.Row([
dbc.Col([
html.Div([
# Info button and tutorial link in top right
html.Div([
html.A(
"Tutorial",
href="https://blog.nolank.ca/aion-search/#best-practices",
target="_blank",
rel="noopener noreferrer",
style={
"textDecoration": "underline",
"marginRight": "0.1rem",
"color": "rgba(245, 245, 247, 0.6)",
"fontSize": "0.9rem",
"fontWeight": "400",
"letterSpacing": "0.03em",
"transition": "all 0.3s ease"
}
),
dbc.Button([
html.I(className="fas fa-info-circle")
], id="info-button", color="link", size="sm",
className="info-button")
], style={"position": "absolute", "top": "8px", "right": "8px", "z-index": "1000",
"display": "flex", "alignItems": "center"}),
# Example search buttons
html.Div([
html.P("Try these examples:", className="text-center mb-2",
style={"color": "rgba(245, 245, 247, 0.5)", "font-weight": "300",
"font-size": "0.75rem", "letter-spacing": "0.02em"}),
html.Div([
dbc.Button([html.I(className="fas fa-clone me-2"), "Two edge-on galaxies"],
id="example-1", className="example-button me-2 mb-2", size="sm", color="light"),
dbc.Button([html.I(className="fas fa-water me-2"), "Tidal"],
id="example-2", className="example-button me-2 mb-2", size="sm", color="light"),
dbc.Button([html.I(className="fas fa-stream me-2"), "Stream"],
id="example-3", className="example-button me-2 mb-2", size="sm", color="light"),
dbc.Button([html.I(className="fas fa-explosion me-2"), "A violent merger"],
id="example-5", className="example-button me-2 mb-2", size="sm", color="light"),
dbc.Button([html.I(className="fas fa-moon me-2"), "Low surface brightness"],
id="example-6", className="example-button me-2 mb-2", size="sm", color="light"),
dbc.Button([html.I(className="fas fa-ring me-2"), "Ring-like structure"],
id="example-7", className="example-button me-2 mb-2", size="sm", color="light"),
dbc.Button([html.I(className="fas fa-star me-2"), "A bursty, star forming galaxy"],
id="example-8", className="example-button mb-2", size="sm", color="light")
], className="text-center")
], className="mb-3"),
# Basic search input (shown when NOT in advanced mode)
html.Div([
dbc.InputGroup([
dbc.InputGroupText(html.I(className="fas fa-search")),
dbc.Input(
id="search-input",
placeholder="Describe the galaxy you're looking for...",
type="text",
n_submit=0
),
dbc.Button("Search",
id="search-button", color="primary", n_clicks=0),
dbc.Button([
html.I(className="fas fa-download")
], id="download-button", color="secondary", n_clicks=0,
className="download-button", size="sm",
disabled=True)
])
], id="basic-search-bar"),
# Advanced search interface (shown when in advanced mode)
html.Div([
html.P("Combine multiple text and/or image queries using vector addition/subtraction:",
className="refinement-label mb-3"),
# Main query row
dbc.Row([
# Operation selector
dbc.Col([
dbc.Select(
id="main-vector-operation",
options=[
{"label": "+10", "value": "+10"},
{"label": "+5", "value": "+5"},
{"label": "+2", "value": "+2"},
{"label": "+", "value": "+"},
{"label": "-", "value": "-"},
{"label": "-2", "value": "-2"},
{"label": "-5", "value": "-5"},
{"label": "-10", "value": "-10"}
],
value="+",
style={"width": "70px"},
className="d-inline-block vector-operation-select"
)
], width=1),
# Query type selector
dbc.Col([
dbc.Select(
id="main-query-type",
options=[
{"label": "Text", "value": "text"},
{"label": "Image", "value": "image"}
],
value="text",
style={"width": "100px"},
className="d-inline-block vector-query-type-select"
)
], width=2),
# Input area
dbc.Col([
# Text input (shown when type is "text")
html.Div([
dcc.Input(
id="search-input-advanced",
placeholder="Enter text query...",
type="text",
value="",
n_submit=0,
className="form-control"
)
], id="main-text-input-container", style={"display": "block"}),
# Image inputs (shown when type is "image")
html.Div([
dbc.Row([
dbc.Col([
dbc.Input(
id="main-vector-ra",
placeholder="RA:",
type="number",
step="any"
)
], width=6),
dbc.Col([
dbc.Input(
id="main-vector-dec",
placeholder="Dec:",
type="number",
step="any"
)
], width=6)
])
], id="main-image-input-container", style={"display": "none"})
], width=9)
], className="mb-2"),
# Additional queries container
html.Div(id="vector-inputs", children=[]),
# Buttons row
dbc.Row([
dbc.Col([
dbc.Button(
[html.I(className="fas fa-plus me-2"), "Add Query"],
id="add-vector-input",
color="secondary",
size="sm",
className="btn-add-vector"
)
], width=6),
dbc.Col([
dbc.Button(
"Search",
id="search-button-advanced",
className="btn-primary",
n_clicks=0,
style={"margin-right": "0.5rem"}
),
dbc.Button([
html.I(className="fas fa-download")
], id="download-button-advanced", color="secondary", n_clicks=0,
className="download-button", size="sm",
disabled=True)
], width=6, className="text-end")
], className="mt-3")
], id="advanced-search-interface", style={"display": "none"}),
# Toggle button (below search bar)
html.Div([
dbc.Button([
html.I(className="fas fa-chevron-down me-2", id="vector-arrow"),
"Text + Image search with vector addition"
], id="vector-toggle", color="link", className="refinement-toggle",
style={"white-space": "nowrap", "padding": "0.5rem 0.75rem", "margin-top": "0.5rem"})
], className="d-flex")
], className="search-container", style={"position": "relative"}),
# r_mag filter
create_rmag_filter_panel()
], width=12, lg=11, className="mx-auto")
], className="mb-3")
def create_vector_input_row(index: int, query_type: str = "text", ra: float = None, dec: float = None, fov: float = 0.025,
text_value: str = None, operation: str = "+"):
"""Create a single vector input row with operation selector, query type toggle, and conditional inputs.
Args:
index: Index of the vector input row
query_type: Type of query - "text" or "image" (default: "text")
ra: Initial RA value for image queries (default: None)
dec: Initial Dec value for image queries (default: None)
fov: Initial FoV value for image queries (default: 0.025)
text_value: Initial text value for text queries (default: None)
operation: Initial operation value (default: "+")
Returns:
Dash Bootstrap Row component with text/image mode toggle
"""
# Determine display styles based on query type
text_display = {"display": "block"} if query_type == "text" else {"display": "none"}
image_display = {"display": "none"} if query_type == "text" else {"display": "block"}
return dbc.Row([
# Operation column with magnitude support
dbc.Col([
dbc.Select(
id={"type": "vector-operation", "index": index},
options=[
{"label": "+10", "value": "+10"},
{"label": "+5", "value": "+5"},
{"label": "+2", "value": "+2"},
{"label": "+", "value": "+"},
{"label": "-", "value": "-"},
{"label": "-2", "value": "-2"},
{"label": "-5", "value": "-5"},
{"label": "-10", "value": "-10"}
],
value=operation,
style={"width": "70px"},
className="d-inline-block vector-operation-select"
)
], width=1),
# Query type toggle (Text/Image)
dbc.Col([
dbc.Select(
id={"type": "vector-query-type", "index": index},
options=[
{"label": "Text", "value": "text"},
{"label": "Image", "value": "image"}
],
value=query_type,
style={"width": "100px"},
className="d-inline-block vector-query-type-select"
)
], width=2),
# Input area (text or image fields)
dbc.Col([
# Text input (shown when type is "text")
html.Div([
dcc.Input(
id={"type": "vector-text", "index": index},
placeholder="Enter text query...",
type="text",
value=text_value,
n_submit=0,
className="form-control"
)
], id={"type": "text-input-container", "index": index}, style=text_display),
# Image inputs (shown when type is "image")
html.Div([
dbc.Row([
dbc.Col([
dbc.Input(
id={"type": "vector-ra", "index": index},
placeholder="RA:",
type="number",
step="any",
value=ra
)
], width=6),
dbc.Col([
dbc.Input(
id={"type": "vector-dec", "index": index},
placeholder="Dec:",
type="number",
step="any",
value=dec
)
], width=6)
])
], id={"type": "image-input-container", "index": index}, style=image_display)
], width=8),
# Delete button
dbc.Col([
dbc.Button(
html.I(className="fas fa-times"),
id={"type": "vector-delete", "index": index},
color="link",
size="sm",
className="text-danger vector-delete-btn",
style={"padding": "0.25rem 0.5rem"}
)
], width=1, className="d-flex align-items-center justify-content-end")
], className="mb-2", id={"type": "vector-row", "index": index})
def create_results_container():
"""Create the search results display container."""
return dbc.Row([
dbc.Col([
html.Div(id="search-time", className="time-breakdown text-center mb-2"),
html.Div(id="search-results")
])
])
def create_stores():
"""Create Dash Store components for data persistence."""
return [
dcc.Location(id='url', refresh=False),
dcc.Store(id="search-data"),
dcc.Store(id="current-galaxy-data"),
dcc.Store(id="vector-inputs-count", data=1),
dcc.Store(id="current-search-params"), # Stores current search params for URL generation
dcc.Store(id="pending-expand-galaxy"), # Stores galaxy to expand after URL-based search
dcc.Store(id="url-search-trigger", data=0), # Triggers search after URL state restore
dcc.Download(id="download-csv")
]
def create_galaxy_modal():
"""Create the modal for displaying galaxy details."""
return dbc.Modal([
dbc.ModalHeader(dbc.ModalTitle(id="modal-title")),
dbc.ModalBody([
html.Div(id="modal-image", className="text-center mb-3"),
html.Div(id="modal-description")
]),
dbc.ModalFooter([
dbc.Button(
[html.I(className="fas fa-plus me-2"), "Search for similar"],
id="add-to-advanced-search",
color="primary",
className="me-2"
),
html.A(
dbc.Button(
[html.I(className="fas fa-telescope me-2"), "View in Legacy Survey"],
color="primary",
className="me-2"
),
id="legacy-survey-link",
href="#",
target="_blank",
rel="noopener noreferrer"
),
dbc.Button(
[html.I(className="fas fa-link me-2"), "Copy link"],
id="copy-galaxy-link",
color="secondary",
className="me-2",
n_clicks=0
),
html.Span(id="copy-galaxy-feedback", style={"color": "#28a745", "fontSize": "0.8rem"}),
dbc.Button("Close", id="close-modal", className="ms-auto")
])
], id="galaxy-modal", size="lg", is_open=False)
def create_info_modal():
"""Create the info modal explaining the app."""
return dbc.Modal([
dbc.ModalHeader(dbc.ModalTitle([html.I(className="fas fa-info-circle me-2"), "About Galaxy Search"])),
dbc.ModalBody([
html.P("This app searches ~19 million Legacy Survey galaxies with r-mag brighter than 20 magnitude. "
"These galaxy images were converted to a text-searchable space using AION-Search, which was trained on "
"~275k text descriptions of galaxy images generated by GPT-4.1-mini.",
style={"color": "rgba(245, 245, 247, 0.8)", "margin-bottom": "1rem", "font-size": "0.9rem"}),
html.P("Images are from DESI Legacy Surveys DR10 via the hips2fits service provided by the Strasbourg Astronomical Data Centre (CDS).",
style={"color": "rgba(245, 245, 247, 0.6)", "margin-bottom": "0", "font-size": "0.75rem"})
]),
dbc.ModalFooter([
html.A(
dbc.Button([html.I(className="fas fa-file-alt me-2"), "Paper"], color="secondary"),
href="https://arxiv.org/abs/2512.11982",
target="_blank",
rel="noopener noreferrer",
className="me-2"
),
html.A(
dbc.Button([html.I(className="fas fa-book me-2"), "Tutorial"], color="secondary"),
href="https://blog.nolank.ca/aion-search/#best-practices",
target="_blank",
rel="noopener noreferrer",
className="me-2"
),
dbc.Button("Close", id="close-info-modal", className="ms-auto")
])
], id="info-modal", size="lg", is_open=False)
def create_layout():
"""Create the complete app layout.
Returns:
Dash Container with the full app layout
"""
return dbc.Container([
create_header(),
create_search_container(),
create_results_container(),
*create_stores(),
create_galaxy_modal(),
create_info_modal()
], fluid=True, className="py-2")