"""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")