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"
"
# Handle different result formats
for item in results:
# Handle tuple with 2 elements (name, score)
if isinstance(item, tuple) and len(item) == 2:
name, score = item
# Handle tuple with 3 elements (common in category results)
elif isinstance(item, tuple) and len(item) == 3:
id_val, text, score = item
name = f"{id_val}: {text}" if text else id_val
# Handle dictionary format
elif isinstance(item, dict) and "name" in item and "score" in item:
name = item["name"]
score = item["score"]
# Handle dictionary format with different keys
elif isinstance(item, dict) and "category" in item and "confidence" in item:
name = item["category"]
score = item["confidence"]
# Handle dictionary format for ingredients
elif isinstance(item, dict) and "ingredient" in item and "relevance_score" in item:
name = item["ingredient"]
score = item["relevance_score"]
# Default case - just convert to string
else:
name = str(item)
score = 0.0
# Ensure score is a float
try:
score = float(score)
except (ValueError, TypeError):
score = 0.0
confidence_percent = int(score * 100)
confidence_color = get_confidence_color(score)
bg_color = get_confidence_bg_color(score)
# Improved layout with better contrast and labeled confidence
html += f"- "
html += f"{name}"
html += f"Confidence: {confidence_percent}%"
html += "
"
html += "
"
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"
{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 += "
"
for item in filtered_results:
display_text, score = parse_result_item(item)
confidence_percent = int(score * 100)
# Improved styling for confidence percentage - using black text for better contrast
confidence_color = get_confidence_color(score)
bg_color = get_confidence_bg_color(score)
content += f"- "
content += f"{display_text}"
content += f"Confidence: {confidence_percent}%"
content += "
"
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 += "Match | "
content += "Confidence | "
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"{display_text} | "
content += f""
content += f""
content += f"{confidence_percent}% | "
content += "
"
content += "
"
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)