product_ingredient_demo / ui_formatters.py
esilver's picture
Added expansion for embeddings
1e737a1
raw
history blame
24 kB
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)