Spaces:
Running
Running
| """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") | |