Spaces:
Sleeping
Sleeping
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; min-width: 200px; padding: 15px; border-right: 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" | |
} | |
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"<div class='method-results' style='{STYLES['method_container']}'>" | |
html += f"<h4 style='{STYLES['method_title']}; border-bottom: 2px solid {color_hex};'>{method_name}</h4>" | |
if results: | |
html += f"<ul style='{STYLES['item_list']}'>" | |
# 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"<strong>{id_val}</strong>: {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"<li style='display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; padding: 6px; border-radius: 4px; background-color: rgba(240, 240, 240, 0.4);'>" | |
html += f"<span style='font-weight: 500; flex: 1;'>{name}</span>" | |
html += f"<span style='background-color: {bg_color}; border: 1px solid {confidence_color}; color: #000; font-weight: 600; padding: 2px 6px; border-radius: 4px; min-width: 70px; text-align: center; margin-left: 8px;'>Confidence: {confidence_percent}%</span>" | |
html += "</li>" | |
html += "</ul>" | |
else: | |
html += f"<p style='{STYLES['empty_message']}'>No results found</p>" | |
html += "</div>" | |
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"<div class='result-card' style='{STYLES['card']}'>" | |
html += f"<div class='card-header' style='{STYLES['header']}; background-color: {header_bg_color};'>" | |
html += f"<h3 style='{STYLES['header_text']}'>{title}</h3>" | |
html += "</div>" | |
html += f"<div class='card-content'>{content}</div>" | |
html += "</div>" | |
return html | |
def format_comparison_html(product, method_results): | |
""" | |
Format the comparison results as HTML | |
Args: | |
product: Product name | |
method_results: Dictionary with results from different methods | |
Returns: | |
HTML string | |
""" | |
# Create the methods comparison content | |
methods_html = f"<div class='methods-comparison' style='{STYLES['flex_container']}'>" | |
# 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 += "</div>" | |
# 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 = "<div class='results-container' style='font-family: Arial, sans-serif;'>" | |
if header_text: | |
container += f"<p style='color: {COLORS['text_secondary']};'>{header_text}</p>" | |
container += ''.join(html_elements) | |
container += "</div>" | |
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"<div style='{STYLES['info_panel']}'>" | |
content += "<h4 style='margin-top: 0; border-bottom: 1px solid rgba(0,0,0,0.1); padding-bottom: 8px;'>Expanded Description</h4>" | |
content += f"<p style='margin-bottom: 8px;'>{expanded_description}</p>" | |
content += "</div>" | |
# Format the results section - create custom section | |
color_hex = METHOD_COLORS.get(match_type, "#1abc9c") | |
# Add results section with custom title | |
content += f"<div class='method-results' style='margin-top: 15px; border-left: 3px solid {color_hex}; padding-left: 15px;'>" | |
title_text = "Ingredients" if match_type == "ingredients" else "Categories" | |
content += f"<h4 style='margin-top: 0; color: {color_hex};'>{title_text}</h4>" | |
# Filter results by confidence threshold | |
filtered_results = filter_results_by_threshold(results, confidence_threshold) | |
if filtered_results: | |
content += "<ul style='margin-top: 5px; padding-left: 20px;'>" | |
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"<li style='display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px;'>" | |
content += f"<span style='font-weight: 500; flex: 1;'>{display_text}</span>" | |
content += f"<span style='background-color: {bg_color}; border: 1px solid {confidence_color}; color: #000; font-weight: 600; padding: 2px 6px; border-radius: 4px; min-width: 70px; text-align: center; margin-left: 8px;'>Confidence: {confidence_percent}%</span>" | |
content += "</li>" | |
content += "</ul>" | |
else: | |
content += "<p style='color: #777; font-style: italic; margin: 5px 0;'>No matches found above confidence threshold.</p>" | |
content += "</div>" | |
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"<div style='{STYLES['info_panel']}'>" | |
content += f"<p style='margin: 0;'>{summary}</p>" | |
content += "</div>" | |
# Add expanded description if provided | |
if expanded_description: | |
content += f"<div style='{STYLES['info_panel']}'>" | |
content += "<h4 style='margin-top: 0; margin-bottom: 8px; border-bottom: 1px solid rgba(0,0,0,0.1); padding-bottom: 5px;'>Expanded Description</h4>" | |
content += f"<p style='margin: 0;'>{expanded_description}</p>" | |
content += "</div>" | |
# Filter results by confidence threshold | |
filtered_results = filter_results_by_threshold(results, confidence_threshold) | |
# Format the results | |
if filtered_results: | |
content += "<div style='padding: 10px;'>" | |
content += "<table style='width: 100%; border-collapse: collapse;'>" | |
content += "<thead><tr>" | |
content += "<th style='text-align: left; padding: 8px; border-bottom: 2px solid #ddd;'>Match</th>" | |
content += "<th style='text-align: right; padding: 8px; border-bottom: 2px solid #ddd; width: 100px;'>Confidence</th>" | |
content += "</tr></thead>" | |
content += "<tbody>" | |
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 += "<tr>" | |
content += f"<td style='text-align: left; padding: 8px; border-bottom: 1px solid #ddd;'>{display_text}</td>" | |
content += f"<td style='text-align: center; padding: 8px; border-bottom: 1px solid #ddd;'>" | |
content += f"<span style='background-color: {bg_color}; border: 1px solid {confidence_color}; color: #000;" | |
content += f"font-weight: 600; padding: 2px 6px; border-radius: 4px; display: inline-block; width: 70px;'>" | |
content += f"{confidence_percent}%</span></td>" | |
content += "</tr>" | |
content += "</tbody></table>" | |
content += "</div>" | |
else: | |
content += "<p style='color: #777; font-style: italic; padding: 10px; margin: 0;'>No matches found above confidence threshold.</p>" | |
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; min-width: 200px; padding: 15px; border-right: 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=""): | |
""" | |
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 | |
Returns: | |
HTML string | |
""" | |
content = "" | |
# Add expanded description if available | |
if explanation: | |
content += f"<div style='{STYLES['info_panel']}'>" | |
content += "<h4 style='margin-top: 0; border-bottom: 1px solid rgba(0,0,0,0.1); padding-bottom: 8px;'>Expanded Description</h4>" | |
content += f"<p style='margin-bottom: 8px;'>{explanation}</p>" | |
content += "</div>" | |
# Add Chicory results if available | |
if chicory_result: | |
content += f"<div style='{STYLES['info_panel']}'>" | |
content += "<h4 style='margin-top: 0; border-bottom: 1px solid rgba(0,0,0,0.1); padding-bottom: 8px;'>Chicory Parser Results</h4>" | |
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"<div style='display: flex; justify-content: space-between; align-items: center; padding: 8px; border-radius: 4px;'>" | |
content += f"<span style='font-weight: bold;'>{ingredient}</span>" | |
content += f"<span style='background-color: {get_confidence_bg_color(confidence)}; border: 1px solid {get_confidence_color(confidence)}; color: #000; font-weight: 600; padding: 2px 6px; border-radius: 4px; min-width: 70px; text-align: center;'>Confidence: {confidence_percent}%</span>" | |
content += "</div>" | |
else: | |
content += f"<p style='{STYLES['empty_message']}'>No Chicory results available</p>" | |
content += "</div>" | |
# Add the category results | |
content += format_method_results( | |
method_key="categories", | |
results=categories, | |
color_hex=header_color or METHOD_COLORS.get("categories", "#1abc9c") | |
) | |
return format_result_card(title=product, content=content) | |