Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| from src.search.service import SearchService | |
| from src.utils.constants import Constants | |
| from src.utils.logging import get_logger | |
| class UIComponents: | |
| """ | |
| Class to handle the Gradio UI components for the application. | |
| """ | |
| def __init__(self): | |
| """ | |
| Initialize the UI components. | |
| """ | |
| self.logger = get_logger() | |
| self.logger.info("Initializing UI components") | |
| try: | |
| self.search_service = SearchService() | |
| self.logger.info("UI components initialized successfully") | |
| except Exception as e: | |
| self.logger.error(f"Failed to initialize UI components: {str(e)}") | |
| raise | |
| def create_customer_tab(self): | |
| """ | |
| Create the customer search tab. | |
| Returns: | |
| gradio.Tab: The customer search tab. | |
| """ | |
| with gr.Tab(Constants.UI_CUSTOMER_TAB) as tab: | |
| gr.Markdown(f"## {Constants.UI_CUSTOMER_DESCRIPTION}") | |
| with gr.Row(): | |
| text_input = gr.Textbox( | |
| label=Constants.UI_TEXT_INPUT_LABEL, | |
| placeholder="Ex: Une grille décorative pour mon jardin avec des motifs floraux", | |
| lines=3 | |
| ) | |
| with gr.Row(): | |
| image_input = gr.Image( | |
| label=Constants.UI_IMAGE_INPUT_LABEL, | |
| type="pil" | |
| ) | |
| with gr.Row(): | |
| search_button = gr.Button(Constants.UI_SEARCH_BUTTON, variant="primary") | |
| with gr.Row(visible=False) as loading_indicator: | |
| gr.Markdown(f"### {Constants.UI_LOADING_TEXT}") | |
| results_container = gr.HTML(visible=False) | |
| def search(text, image): | |
| self.logger.info("Customer UI search initiated") | |
| self.logger.debug(f"Search parameters - Text: '{text}', Image provided: {image is not None}") | |
| if not text and image is None: | |
| self.logger.info("No search parameters provided, returning empty results") | |
| return None, gr.update(visible=False), gr.update(visible=False) | |
| try: | |
| self.logger.info("Calling customer search service") | |
| results = self.search_service.customer_search(text, image) | |
| if not results: | |
| self.logger.info("No results found for customer search") | |
| return None, gr.update(visible=False), gr.HTML( | |
| f"<div class='no-results'>{Constants.UI_NO_RESULTS}</div>", visible=True) | |
| # Prepare gallery images | |
| self.logger.debug(f"Preparing gallery with {len(results)} images") | |
| gallery_images = [] | |
| for result in results: | |
| gallery_images.append((result["thumbnail"], result["title"])) | |
| # Prepare HTML for results | |
| self.logger.debug("Creating HTML for search results") | |
| html = self._create_results_html(results) | |
| self.logger.info(f"Customer search completed successfully with {len(results)} results") | |
| return gr.HTML(html, visible=True) | |
| except Exception as e: | |
| self.logger.error(f"Error during customer UI search: {str(e)}") | |
| # Return a user-friendly error message | |
| error_html = f"<div class='no-results'>Une erreur s'est produite lors de la recherche. Veuillez réessayer.</div>" | |
| return None, gr.update(visible=False), gr.HTML(error_html, visible=True) | |
| search_button.click( | |
| fn=search, | |
| inputs=[text_input, image_input], | |
| outputs=[results_container], | |
| # js=""" | |
| # function(text, image) { | |
| # // Disable search button and show loading indicator | |
| # document.querySelector("button[variant='primary']").disabled = true; | |
| # return [text, image]; | |
| # } | |
| # """, | |
| preprocess=True | |
| ).then( | |
| # js=""" | |
| # function() { | |
| # // Re-enable search button and hide loading indicator | |
| # document.querySelector("button[variant='primary']").disabled = false; | |
| # return []; | |
| # } | |
| # """ | |
| ) | |
| # Show/hide loading indicator | |
| search_button.click( | |
| fn=lambda: gr.update(visible=True), | |
| inputs=None, | |
| outputs=loading_indicator | |
| ).then( | |
| fn=lambda: gr.update(visible=False), | |
| inputs=None, | |
| outputs=loading_indicator | |
| ) | |
| return tab | |
| def create_staff_tab(self): | |
| """ | |
| Create the staff search tab. | |
| Returns: | |
| gradio.Tab: The staff search tab. | |
| """ | |
| with gr.Tab(Constants.UI_STAFF_TAB) as tab: | |
| gr.Markdown(f"## {Constants.UI_STAFF_DESCRIPTION}") | |
| with gr.Row(): | |
| image_input = gr.Image( | |
| label=Constants.UI_IMAGE_INPUT_LABEL, | |
| type="pil" | |
| ) | |
| with gr.Row(): | |
| search_button = gr.Button(Constants.UI_SEARCH_BUTTON, variant="primary") | |
| with gr.Row(visible=False) as loading_indicator: | |
| gr.Markdown(f"### {Constants.UI_LOADING_TEXT}") | |
| result_container = gr.HTML(visible=False) | |
| def search(image): | |
| self.logger.info("Staff UI search initiated") | |
| self.logger.debug(f"Search parameters - Image provided: {image is not None}") | |
| if image is None: | |
| self.logger.info("No image provided for staff search, returning empty results") | |
| return gr.update(visible=False) | |
| try: | |
| self.logger.info("Calling staff search service") | |
| result = self.search_service.staff_search(image) | |
| if not result: | |
| self.logger.info("No results found for staff search") | |
| return gr.HTML(f"<div class='no-results'>{Constants.UI_NO_RESULTS}</div>", visible=True) | |
| # Prepare HTML for result | |
| self.logger.debug("Creating HTML for search result") | |
| html = self._create_results_html([result]) | |
| self.logger.info("Staff search completed successfully with a match") | |
| return gr.HTML(html, visible=True) | |
| except Exception as e: | |
| self.logger.error(f"Error during staff UI search: {str(e)}") | |
| # Return a user-friendly error message | |
| error_html = f"<div class='no-results'>Une erreur s'est produite lors de la recherche. Veuillez réessayer.</div>" | |
| return gr.HTML(error_html, visible=True) | |
| search_button.click( | |
| fn=search, | |
| inputs=[image_input], | |
| outputs=[result_container], | |
| # js=""" | |
| # function(image) { | |
| # // Disable search button and show loading indicator | |
| # document.querySelector("button[variant='primary']").disabled = true; | |
| # return [image]; | |
| # } | |
| # """, | |
| preprocess=True | |
| ).then( | |
| # js=""" | |
| # function() { | |
| # // Re-enable search button and hide loading indicator | |
| # document.querySelector("button[variant='primary']").disabled = false; | |
| # return []; | |
| # } | |
| # """ | |
| ) | |
| # Show/hide loading indicator | |
| search_button.click( | |
| fn=lambda: gr.update(visible=True), | |
| inputs=None, | |
| outputs=loading_indicator | |
| ).then( | |
| fn=lambda: gr.update(visible=False), | |
| inputs=None, | |
| outputs=loading_indicator | |
| ) | |
| return tab | |
| def _create_results_html(self, results): | |
| """ | |
| Create HTML for displaying search results. | |
| Args: | |
| results (list): List of search results. | |
| Returns: | |
| str: HTML string for displaying results. | |
| """ | |
| self.logger.debug(f"Creating HTML for {len(results)} results") | |
| try: | |
| html = "<div class='results-container'>" | |
| for i, result in enumerate(results): | |
| self.logger.debug(f"Adding result {i + 1}: {result['title']}") | |
| html += f""" | |
| <div class='result-card'> | |
| <div class='result-image'> | |
| <img src='{result["thumbnail"]}' alt='{result["title"]}'> | |
| </div> | |
| <div class='result-content'> | |
| <h3>{result["title"]}</h3> | |
| <p>{result["description"]}</p> | |
| <p class='price'><strong>{Constants.UI_PRICE_LABEL}</strong> {result["price"]}</p> | |
| <a href='{result["product_page_url"]}' target='_blank' class='product-button'>{Constants.UI_PRODUCT_BUTTON}</a> | |
| </div> | |
| </div> | |
| """ | |
| html += "</div>" | |
| self.logger.debug("HTML for results created successfully") | |
| except Exception as e: | |
| self.logger.error(f"Error creating HTML for results: {str(e)}") | |
| # Create a simple fallback HTML in case of error | |
| html = "<div class='results-container'><p>Résultats disponibles mais impossible de les afficher correctement.</p></div>" | |
| # Add CSS for styling | |
| html += """ | |
| <style> | |
| .results-container { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 20px; | |
| justify-content: center; | |
| } | |
| .result-card { | |
| border: 1px solid #ddd; | |
| border-radius: 8px; | |
| overflow: hidden; | |
| width: 300px; | |
| box-shadow: 0 2px 5px rgba(0,0,0,0.1); | |
| transition: transform 0.3s ease; | |
| background-color: white; | |
| } | |
| .result-card:hover { | |
| transform: translateY(-5px); | |
| box-shadow: 0 5px 15px rgba(0,0,0,0.1); | |
| } | |
| .result-image { | |
| height: 200px; | |
| overflow: hidden; | |
| } | |
| .result-image img { | |
| width: 100%; | |
| height: 100%; | |
| object-fit: cover; | |
| } | |
| .result-content { | |
| padding: 15px; | |
| } | |
| .result-content h3 { | |
| margin-top: 0; | |
| color: #333; | |
| } | |
| .price { | |
| font-size: 1.1em; | |
| color: #e63946; | |
| } | |
| .product-button { | |
| display: inline-block; | |
| background-color: #457b9d; | |
| color: white; | |
| padding: 8px 16px; | |
| text-decoration: none; | |
| border-radius: 4px; | |
| margin-top: 10px; | |
| transition: background-color 0.3s ease; | |
| } | |
| .product-button:hover { | |
| background-color: #1d3557; | |
| } | |
| .no-results { | |
| text-align: center; | |
| padding: 20px; | |
| font-size: 1.2em; | |
| color: #666; | |
| } | |
| /* Responsive design */ | |
| @media (max-width: 768px) { | |
| .results-container { | |
| flex-direction: column; | |
| align-items: center; | |
| } | |
| .result-card { | |
| width: 100%; | |
| max-width: 350px; | |
| } | |
| } | |
| </style> | |
| """ | |
| return html | |
| def create_interface(self): | |
| """ | |
| Create the complete Gradio interface. | |
| Returns: | |
| gradio.Blocks: The Gradio interface. | |
| """ | |
| with gr.Blocks(title=Constants.UI_TITLE, theme=gr.themes.Soft()) as interface: | |
| gr.Markdown(f"# {Constants.UI_TITLE}") | |
| with gr.Tabs(): | |
| self.create_customer_tab() | |
| self.create_staff_tab() | |
| # Add CSS for responsive design | |
| gr.HTML(""" | |
| <style> | |
| /* Responsive design */ | |
| @media (max-width: 768px) { | |
| .gradio-container { | |
| padding: 10px !important; | |
| } | |
| .gradio-row { | |
| flex-direction: column !important; | |
| } | |
| } | |
| </style> | |
| """) | |
| return interface | |
| def close(self): | |
| """ | |
| Close connections and resources. | |
| """ | |
| self.logger.info("Closing UI components resources") | |
| try: | |
| self.search_service.close() | |
| self.logger.info("UI components resources closed successfully") | |
| except Exception as e: | |
| self.logger.error(f"Error closing UI components resources: {str(e)}") | |
| # We don't re-raise the exception here to ensure cleanup continues even if there's an error | |