from typing import List, Dict, Tuple, Any from utils import get_confidence_color, get_confidence_bg_color # Theme configuration (can be easily switched between light/dark) THEME = "dark" # Options: "light", "dark" # Theme-specific colors THEMES = { "light": { "background": "#ffffff", "card_bg": "#ffffff", "card_border": "#ddd", "header_bg": "#2c3e50", "header_text": "#ffffff", "text_primary": "#333333", "text_secondary": "#555555", "section_bg": "#f8f9fa", }, "dark": { "background": "#121212", "card_bg": "#1e1e1e", "card_border": "#333", "header_bg": "#37474f", "header_text": "#ffffff", "text_primary": "#e0e0e0", "text_secondary": "#b0bec5", "section_bg": "#263238", } } # Get current theme colors COLORS = THEMES[THEME] # Base styling constants (adjusted based on theme) STYLES = { "card": f"margin-bottom: 20px; border: 1px solid {COLORS['card_border']}; border-radius: 8px; overflow: hidden; background-color: {COLORS['card_bg']};", "header": f"background-color: {COLORS['header_bg']}; padding: 12px 15px; border-bottom: 1px solid {COLORS['card_border']};", "header_text": f"margin: 0; font-size: 18px; color: {COLORS['header_text']};", "flex_container": "display: flex; flex-wrap: wrap;", "method_container": f"flex: 1; width: 100%; padding: 15px; border-bottom: 1px solid {COLORS['card_border']};", "method_title": f"margin-top: 0; color: {COLORS['text_primary']}; padding-bottom: 8px;", "item_list": "list-style-type: none; padding-left: 0;", "item": "margin-bottom: 8px; padding: 8px; border-radius: 4px;", "empty_message": "color: #7f8c8d; font-style: italic;", "info_panel": f"padding: 10px; background-color: {COLORS['section_bg']}; margin-bottom: 10px; border-radius: 4px;" } # Method colors (consistent across themes) METHOD_COLORS = { "base": "#f39c12", # Orange "voyage": "#3498db", # Blue "chicory": "#9b59b6", # Purple "openai": "#2ecc71", # Green "expanded": "#e74c3c", # Red "hybrid": "#1abc9c", # Turquoise "categories": "#1abc9c" # Same as hybrid } # Method display names METHOD_NAMES = { "base": "Base Embeddings", "voyage": "Voyage AI Reranker", "chicory": "Chicory Parser", "openai": "OpenAI", "expanded": "Expanded Description", "hybrid": "Hybrid Matching", "categories": "Category Matches", "ingredients": "Ingredient Matches" } def format_method_results(method_key, results, color_hex=None): """ Format results for a single method section Args: method_key: Key identifying the method (base, voyage, etc.) results: List of (name, score) tuples or format-specific data structure color_hex: Optional color override (otherwise uses METHOD_COLORS) Returns: HTML string for the method section """ # Get color from METHOD_COLORS if not provided if color_hex is None: color_hex = METHOD_COLORS.get(method_key, "#777777") # Get method name from METHOD_NAMES or use the key with capitalization method_name = METHOD_NAMES.get(method_key, method_key.replace('_', ' ').title()) html = f"
" html += f"

{method_name}

" if results: html += f"" else: html += f"

No results found

" html += "
" return html def format_result_card(title, content, header_bg_color=None): """ Create a styled card with a header and content Args: title: Card title content: HTML content for the card body header_bg_color: Optional header background color Returns: HTML string for the card """ if header_bg_color is None: header_bg_color = COLORS['header_bg'] # Default header background color html = f"
" html += f"
" html += f"

{title}

" html += "
" html += f"
{content}
" html += "
" return html def format_comparison_html(product, method_results, expanded_description=""): """ Format the comparison results as HTML Args: product: Product name method_results: Dictionary with results from different methods expanded_description: Optional expanded product description Returns: HTML string """ # Create the methods comparison content with column direction methods_html = f"
" # Add expanded description if available if expanded_description: methods_html += f"
" methods_html += "

Expanded Description

" methods_html += f"

{expanded_description}

" methods_html += "
" # Add results for each method for method_key in ["base", "voyage", "chicory", "openai"]: methods_html += format_method_results( method_key=method_key, results=method_results.get(method_key, []) ) methods_html += "
" # Create the full card with the methods content return format_result_card(title=product, content=methods_html) def format_reranking_results_html(results, match_type="ingredients", show_scores=True, include_explanation=False, method="voyage", confidence_threshold=0.0): """ Unified formatter that works for both Voyage and OpenAI results, using the individual elements approach with the original visual style. Args: results: List of result dictionaries match_type: Either "ingredients" or "categories" show_scores: Whether to show confidence scores include_explanation: Whether to include expanded descriptions method: Method used for ranking ("voyage" or "openai") confidence_threshold: Threshold for filtering individual items (default 0.0 shows all) Returns: HTML string for displaying results """ if not results or len(results) == 0: return f"No {match_type.lower()} matches found." # Method-specific styling method_color = METHOD_COLORS.get(method, "#777777") method_name = METHOD_NAMES.get(method, method.capitalize()) # Create a header text header_text = f"Matched {len(results)} products to {match_type} using {method_name}" # Generate individual HTML elements for each result - using the old style approach html_elements = [] for result in results: product_name = result.get("product_name", "") matching_items = result.get("matching_items", []) item_scores = result.get("item_scores", []) explanation = result.get("explanation", "") if include_explanation else "" # Convert matching items into tuples with scores for format_expanded_results_html formatted_matches = [] # Make sure we have scores for all items if len(item_scores) != len(matching_items): # If scores are missing, use overall confidence for all result_confidence = result.get("confidence", 0.5) item_scores = [result_confidence] * len(matching_items) for i, item in enumerate(matching_items): score = item_scores[i] if ":" in item and match_type == "categories": # Handle category format "id: description" parts = item.split(":", 1) cat_id = parts[0].strip() cat_text = parts[1].strip() if len(parts) > 1 else "" formatted_matches.append((cat_id, cat_text, score)) else: # Handle ingredient format (just name and score) formatted_matches.append((item, score)) # Only skip if there are no matches at all if not formatted_matches: continue # Use the older style formatter with threshold if include_explanation: # Use expanded_results_html for the old style with expanded descriptions element_html = format_expanded_results_html( product=product_name, results=formatted_matches, expanded_description=explanation, match_type=match_type, confidence_threshold=confidence_threshold ) else: # Use hybrid_results_html when no expanded description is available summary_text = f"{match_type.capitalize()} matches using {method_name}." element_html = format_hybrid_results_html( product=product_name, results=formatted_matches, summary=summary_text, expanded_description="", confidence_threshold=confidence_threshold ) html_elements.append(element_html) # Combine all elements into a container return create_results_container(html_elements, header_text=header_text) def create_results_container(html_elements, header_text=None): """ Create a container for multiple results Args: html_elements: List of HTML strings to include header_text: Optional header text Returns: HTML string for the container """ container = "
" if header_text: container += f"

{header_text}

" container += ''.join(html_elements) container += "
" return container def filter_results_by_threshold(results, confidence_threshold=0.0): """Helper function to filter results by confidence threshold""" filtered_results = [] for item in results: # Handle both 2-value (match, score) and 3-value (id, text, score) tuples score = item[-1] if isinstance(item, tuple) and len(item) >= 2 else 0.0 # Only include results above the threshold if score >= confidence_threshold: filtered_results.append(item) return filtered_results def parse_result_item(item): """Helper function to parse result items into display text and score""" # Handle both 2-value (match, score) and 3-value (id, text, score) tuples if isinstance(item, tuple): if len(item) == 2: match, score = item display_text = match elif len(item) == 3: cat_id, cat_text, score = item display_text = f"{cat_id}: {cat_text}" if cat_text else cat_id else: display_text = str(item) score = 0.0 else: display_text = str(item) score = 0.0 return display_text, score def format_expanded_results_html(product, results, expanded_description, match_type="ingredients", confidence_threshold=0.0): """Format results using expanded descriptions""" content = "" # Add expanded description section content += f"
" content += "

Expanded Description

" content += f"

{expanded_description}

" content += "
" # Format the results section - create custom section color_hex = METHOD_COLORS.get(match_type, "#1abc9c") # Add results section with custom title content += f"
" title_text = "Ingredients" if match_type == "ingredients" else "Categories" content += f"

{title_text}

" # Filter results by confidence threshold filtered_results = filter_results_by_threshold(results, confidence_threshold) if filtered_results: content += "" else: content += "

No matches found above confidence threshold.

" content += "
" return format_result_card(title=product, content=content) def format_hybrid_results_html(product, results, summary, expanded_description="", confidence_threshold=0.0): """ Format results for hybrid matching Args: product: Product name results: List of result tuples (name, score) or (id, name, score) summary: Summary text to display expanded_description: Optional expanded description confidence_threshold: Threshold for filtering individual items Returns: HTML string for displaying results """ content = "" # Add summary text if summary: content += f"
" content += f"

{summary}

" content += "
" # Add expanded description if provided if expanded_description: content += f"
" content += "

Expanded Description

" content += f"

{expanded_description}

" content += "
" # Filter results by confidence threshold filtered_results = filter_results_by_threshold(results, confidence_threshold) # Format the results if filtered_results: content += "
" content += "" content += "" content += "" content += "" content += "" content += "" for item in filtered_results: display_text, score = parse_result_item(item) confidence_percent = int(score * 100) confidence_color = get_confidence_color(score) bg_color = get_confidence_bg_color(score) content += "" content += f"" content += f"" content += "" content += "
MatchConfidence
{display_text}" content += f"" content += f"{confidence_percent}%
" content += "
" else: content += "

No matches found above confidence threshold.

" return format_result_card(title=product, content=content) def get_formatted_css(): """ Generate CSS for the UI based on current theme Returns: CSS string for styling the UI """ return f""" .gradio-container .prose {{ max-width: 100%; }} #results-container {{ height: 600px !important; overflow-y: auto !important; overflow-x: hidden !important; padding: 15px !important; border: 1px solid {COLORS['card_border']} !important; background-color: {COLORS['background']} !important; color: {COLORS['text_primary']} !important; }} /* Style for method columns */ .methods-comparison {{ display: flex; flex-wrap: wrap; }} .method-results {{ flex: 1; min-width: 200px; padding: 15px; border-right: 1px solid {COLORS['card_border']}; }} /* Make the product header more visible */ .product-header {{ background-color: {COLORS['header_bg']} !important; padding: 12px 15px !important; border-bottom: 1px solid {COLORS['card_border']} !important; }} .product-header h3 {{ margin: 0 !important; font-size: 18px !important; color: {COLORS['header_text']} !important; background-color: transparent !important; }} /* Remove all nested scrollbars */ #results-container * {{ overflow: visible !important; height: auto !important; max-height: none !important; }} """ def set_theme(theme_name): """ Update the global theme setting Args: theme_name: Theme name to set ("light" or "dark") Returns: Boolean indicating success """ global THEME, COLORS, STYLES if theme_name in THEMES: THEME = theme_name COLORS = THEMES[THEME] # Update styles with new colors STYLES = { "card": f"margin-bottom: 20px; border: 1px solid {COLORS['card_border']}; border-radius: 8px; overflow: hidden; background-color: {COLORS['card_bg']};", "header": f"background-color: {COLORS['header_bg']}; padding: 12px 15px; border-bottom: 1px solid {COLORS['card_border']};", "header_text": f"margin: 0; font-size: 18px; color: {COLORS['header_text']};", "flex_container": "display: flex; flex-wrap: wrap;", "method_container": f"flex: 1; width: 100%; padding: 15px; border-bottom: 1px solid {COLORS['card_border']};", "method_title": f"margin-top: 0; color: {COLORS['text_primary']}; padding-bottom: 8px;", "item_list": "list-style-type: none; padding-left: 0;", "item": "margin-bottom: 8px; padding: 8px; border-radius: 4px;", "empty_message": "color: #7f8c8d; font-style: italic;", "info_panel": f"padding: 10px; background-color: {COLORS['section_bg']}; margin-bottom: 10px; border-radius: 4px;" } return True return False def format_categories_html(product, categories, chicory_result=None, header_color=None, explanation="", match_type="categories"): """ Format category matching results as HTML Args: product: Product name categories: List of (category, score) tuples chicory_result: Optional chicory parser result for the product header_color: Optional header background color explanation: Optional expanded description text match_type: Either "ingredients" or "categories" Returns: HTML string """ content = "" # Add expanded description if available if explanation: content += f"
" content += "

Expanded Description

" content += f"

{explanation}

" content += "
" # Add Chicory results if available if chicory_result: content += f"
" content += "

Chicory Parser Results

" if isinstance(chicory_result, dict): ingredient = chicory_result.get("ingredient", "Not found") confidence = chicory_result.get("confidence", 0) confidence_percent = int(confidence * 100) content += f"
" content += f"{ingredient}" content += f"Confidence: {confidence_percent}%" content += "
" else: content += f"

No Chicory results available

" content += "
" # Add the category results content += format_method_results( method_key=match_type, results=categories, color_hex=header_color or METHOD_COLORS.get(match_type, "#1abc9c") ) return format_result_card(title=product, content=content)