AION-Search / src /components.py
astronolan's picture
improved fonts
4fbb2ab
"""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 '''
<!DOCTYPE html>
<html>
<head>
{%metas%}
<title>galaxy semantic search</title>
{%favicon%}
{%css%}
<style>
@import url('https://fonts.googleapis.com/css2?family=SF+Pro+Display:wght@200;300;400;500;600&display=swap');
* {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Inter', sans-serif;
background: #000000;
color: #F5F5F7;
min-height: 100vh;
margin: 0;
overflow-x: hidden;
}
body::before {
content: '';
position: fixed;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle at 20% 80%, #1C1C1E 0%, transparent 50%),
radial-gradient(circle at 80% 20%, #161618 0%, transparent 50%),
radial-gradient(circle at 40% 40%, #0A0A0B 0%, transparent 50%);
z-index: -1;
}
.container-fluid {
background-color: transparent !important;
padding-top: 2rem !important;
}
.hover-shadow {
transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
border: 0.5px solid rgba(255, 255, 255, 0.1);
background: #0A0A0B;
overflow: hidden;
position: relative;
}
.hover-shadow::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(255,255,255,0.05) 0%, transparent 100%);
opacity: 0;
transition: opacity 0.4s ease;
}
.hover-shadow:hover {
transform: translateY(-4px) scale(1.02);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.8),
0 0 60px rgba(255, 255, 255, 0.05) !important;
border-color: rgba(255, 255, 255, 0.2);
}
.hover-shadow:hover::before {
opacity: 1;
}
.search-container {
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(40px) saturate(180%);
-webkit-backdrop-filter: blur(40px) saturate(180%);
border-radius: 16px;
padding: 1.25rem;
border: 0.5px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
}
.example-button {
background: #E5E5E7 !important;
background-color: #E5E5E7 !important;
border: 0.5px solid #D1D1D3 !important;
color: #1A1A1A !important;
font-weight: 500;
font-size: 0.75rem !important;
padding: 0.4rem 0.9rem !important;
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
letter-spacing: 0.01em;
}
.example-button:hover {
background: #F0F0F2 !important;
background-color: #F0F0F2 !important;
border-color: #C0C0C2 !important;
color: #000000 !important;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.example-button i {
color: #2A2A2A !important;
}
.galaxy-title {
color: #F5F5F7;
font-weight: 200;
font-size: 1.75rem;
letter-spacing: -0.03em;
background: linear-gradient(180deg, #F5F5F7 0%, rgba(245, 245, 247, 0.6) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
animation: float 6s ease-in-out infinite;
}
.modal-content {
background: #1C1C1E;
border: 0.5px solid rgba(255, 255, 255, 0.1);
border-radius: 16px;
backdrop-filter: blur(20px);
}
.modal-header, .modal-footer {
border-color: rgba(255, 255, 255, 0.05);
}
.form-control:focus, .form-control:active {
background-color: rgba(255, 255, 255, 0.05) !important;
border-color: rgba(255, 255, 255, 0.3) !important;
box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.05) !important;
color: #F5F5F7 !important;
}
.form-control {
background-color: rgba(255, 255, 255, 0.03) !important;
border: 0.5px solid rgba(255, 255, 255, 0.1) !important;
color: #F5F5F7 !important;
font-size: 0.95rem !important;
font-weight: 300;
letter-spacing: 0.01em;
}
.form-control::placeholder {
color: rgba(245, 245, 247, 0.4) !important;
}
.btn-primary {
background: rgba(255, 255, 255, 0.8);
color: #000;
border: none;
font-weight: 600;
font-size: 0.9rem;
padding: 0.6rem 1.8rem;
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
letter-spacing: 0.02em;
}
.btn-primary:hover {
background: rgba(255, 255, 255, 0.95);
color: #000;
transform: translateY(-1px);
box-shadow: 0 8px 24px rgba(255, 255, 255, 0.15);
}
.btn-primary:hover i {
color: #000 !important;
}
.results-header {
color: rgba(245, 245, 247, 0.6);
font-weight: 300;
font-size: 0.85rem !important;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.time-breakdown {
color: rgba(245, 245, 247, 0.4);
font-size: 0.7rem;
font-weight: 300;
letter-spacing: 0.02em;
}
.galaxy-count {
color: rgba(245, 245, 247, 0.5);
font-weight: 300;
font-size: 0.85rem;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.score-badge {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
color: rgba(245, 245, 247, 0.9);
font-size: 0.65rem !important;
padding: 3px 8px !important;
border-radius: 6px;
font-weight: 500;
letter-spacing: 0.02em;
border: 0.5px solid rgba(255, 255, 255, 0.1);
}
.info-button {
color: rgba(245, 245, 247, 0.5) !important;
font-size: 0.75rem !important;
opacity: 0.8;
transition: all 0.3s ease;
letter-spacing: 0.02em;
}
.info-button:hover {
opacity: 1;
color: #F5F5F7 !important;
}
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.02);
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.2);
}
.btn-link {
text-decoration: none !important;
}
.input-group-text {
background: rgba(255, 255, 255, 0.03) !important;
border: 0.5px solid rgba(255, 255, 255, 0.1) !important;
color: rgba(245, 245, 247, 0.5) !important;
}
.spinner-border {
color: rgba(245, 245, 247, 0.5) !important;
}
@supports (backdrop-filter: blur(40px)) {
.search-container {
background: rgba(255, 255, 255, 0.03);
}
}
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-3px); }
}
.download-button {
background: rgba(255, 255, 255, 0.05);
border: 0.5px solid rgba(255, 255, 255, 0.1) !important;
color: rgba(245, 245, 247, 0.6) !important;
font-size: 0.75rem !important;
padding: 0.4rem 0.8rem !important;
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
letter-spacing: 0.01em;
margin-left: 0.5rem;
}
.download-button:hover {
background: rgba(255, 255, 255, 0.08) !important;
border-color: rgba(255, 255, 255, 0.15) !important;
color: rgba(245, 245, 247, 0.8) !important;
transform: translateY(-1px);
}
.download-button i {
color: rgba(245, 245, 247, 0.6) !important;
}
.download-button:hover i {
color: rgba(245, 245, 247, 0.8) !important;
}
.refinement-toggle {
background: rgba(255, 255, 255, 0.03);
border: 0.5px solid rgba(255, 255, 255, 0.1);
color: rgba(245, 245, 247, 0.5);
font-size: 0.7rem;
padding: 0.5rem 0.75rem;
transition: all 0.3s ease;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 0.4rem;
margin: 0;
letter-spacing: 0.05em;
text-transform: uppercase;
border-radius: 8px;
height: 100%;
}
.refinement-toggle:hover {
background: rgba(255, 255, 255, 0.05);
border-color: rgba(255, 255, 255, 0.15);
color: rgba(245, 245, 247, 0.8);
transform: translateY(-1px);
}
.refinement-toggle i {
transition: transform 0.3s ease;
font-size: 0.65rem;
}
.refinement-toggle.expanded i {
transform: rotate(180deg);
}
.refinement-container {
background: rgba(255, 255, 255, 0.03);
border: 0.5px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 1.5rem;
margin-top: 1rem;
backdrop-filter: blur(20px);
}
.refinement-label {
color: rgba(245, 245, 247, 0.6);
font-size: 0.85rem;
font-weight: 300;
margin-bottom: 0.5rem;
}
.vector-operation-select {
background-color: rgba(255, 255, 255, 0.03) !important;
border: 0.5px solid rgba(255, 255, 255, 0.1) !important;
color: #F5F5F7 !important;
font-size: 0.9rem !important;
font-weight: 500;
text-align: center;
}
.vector-operation-select option {
background-color: #1C1C1E;
color: #F5F5F7;
}
.vector-query-type-select {
background-color: rgba(255, 255, 255, 0.03) !important;
border: 0.5px solid rgba(255, 255, 255, 0.1) !important;
color: #F5F5F7 !important;
font-size: 0.9rem !important;
font-weight: 500;
}
.vector-query-type-select option {
background-color: #1C1C1E;
color: #F5F5F7;
}
.btn-add-vector {
background: rgba(255, 255, 255, 0.05);
border: 0.5px solid rgba(255, 255, 255, 0.1) !important;
color: rgba(245, 245, 247, 0.6) !important;
font-size: 0.8rem !important;
padding: 0.4rem 0.8rem !important;
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
letter-spacing: 0.01em;
}
.btn-add-vector:hover {
background: rgba(255, 255, 255, 0.08) !important;
border-color: rgba(255, 255, 255, 0.15) !important;
color: rgba(245, 245, 247, 0.8) !important;
transform: translateY(-1px);
}
.btn-add-vector i {
color: rgba(245, 245, 247, 0.6) !important;
}
.btn-add-vector:hover i {
color: rgba(245, 245, 247, 0.8) !important;
}
.vector-delete-btn {
opacity: 0.5;
transition: opacity 0.2s ease;
padding: 0.25rem 0.5rem !important;
border: none !important;
background: none !important;
}
.vector-delete-btn:hover {
opacity: 1;
background: rgba(220, 53, 69, 0.1) !important;
border-radius: 4px;
}
.vector-delete-btn i {
font-size: 0.9rem;
}
/* Range slider styling */
.rmag-slider .rc-slider-rail {
background-color: rgba(255, 255, 255, 0.1);
height: 4px;
}
.rmag-slider .rc-slider-track {
background: linear-gradient(90deg, rgba(255, 255, 255, 0.6) 0%, rgba(255, 255, 255, 0.8) 100%);
height: 4px;
}
.rmag-slider .rc-slider-handle {
border: 2px solid rgba(255, 255, 255, 0.8);
background-color: #F5F5F7;
opacity: 1;
width: 16px;
height: 16px;
margin-top: -6px;
}
.rmag-slider .rc-slider-handle:hover,
.rmag-slider .rc-slider-handle:active,
.rmag-slider .rc-slider-handle:focus {
border-color: rgba(255, 255, 255, 0.95);
box-shadow: 0 0 0 5px rgba(255, 255, 255, 0.1);
}
.rmag-slider .rc-slider-mark-text {
color: rgba(245, 245, 247, 0.5);
font-size: 0.75rem;
font-weight: 300;
}
.rmag-slider .rc-slider-tooltip-inner {
background-color: rgba(255, 255, 255, 0.9);
color: #000;
font-size: 0.75rem;
font-weight: 500;
padding: 4px 8px;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
.rmag-slider .rc-slider-tooltip-arrow {
border-top-color: rgba(255, 255, 255, 0.9);
}
</style>
</head>
<body>
{%app_entry%}
<footer>
{%config%}
{%scripts%}
{%renderer%}
</footer>
</body>
</html>
'''
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")