Spaces:
Sleeping
Sleeping
import streamlit as st | |
import pandas as pd | |
from comparison import compare_ingredient_methods_ui | |
from ui_core import embeddings, load_examples | |
from ui_ingredient_matching import categorize_products | |
from ui_category_matching import categorize_products_by_category | |
from ui_hybrid_matching import categorize_products_with_voyage_reranking | |
from ui_expanded_matching import categorize_products_with_openai_reranking | |
# Removed unused import: from ui_formatters import format_results_html | |
# Session state initialization moved into render_ui() | |
def render_ui(): | |
"""Render the Streamlit interface""" | |
# Initialize session state keys if they don't exist at the start of rendering | |
if 'ingredient_input' not in st.session_state: | |
st.session_state.ingredient_input = "" | |
if 'category_input' not in st.session_state: | |
st.session_state.category_input = "" | |
if 'voyage_input' not in st.session_state: | |
st.session_state.voyage_input = "" | |
if 'openai_input' not in st.session_state: | |
st.session_state.openai_input = "" | |
if 'compare_input' not in st.session_state: | |
st.session_state.compare_input = "" | |
# Add state for results persistence | |
if 'ingredient_results_html' not in st.session_state: | |
st.session_state.ingredient_results_html = None | |
if 'category_results_html' not in st.session_state: | |
st.session_state.category_results_html = None | |
if 'voyage_results_html' not in st.session_state: | |
st.session_state.voyage_results_html = None | |
if 'openai_results_html' not in st.session_state: | |
st.session_state.openai_results_html = None | |
if 'compare_results_html' not in st.session_state: | |
st.session_state.compare_results_html = None | |
# Page config is now set in app.py | |
st.title("Product Categorization Tool") | |
st.markdown("Analyze products by matching to ingredients or categories using AI embeddings.") | |
# Use st.tabs for the different sections | |
tab_ingredient, tab_category, tab_voyage, tab_openai, tab_compare = st.tabs([ | |
"Ingredient Embeddings", | |
"Category Embeddings", | |
"Voyage AI Reranking", | |
"OpenAI Reranking", | |
"Compare Methods" | |
]) | |
# --- Ingredient Matching Tab --- | |
with tab_ingredient: | |
st.header("Match Products to Ingredients") | |
col1, col2 = st.columns([1, 2]) # Give more space to results | |
with col1: | |
with st.container(border=True): | |
st.subheader("Input & Options") | |
# Handle button click *before* rendering the text area | |
if st.button("Load Examples", key="ingredient_examples", icon="π"): | |
st.session_state.ingredient_input = load_examples() # Update state for next rerun | |
# Input section - Use the session state value | |
text_input = st.text_area( | |
"Product Names (one per line)", | |
value=st.session_state.ingredient_input, # Use value from state | |
placeholder="Enter product names, one per line", | |
height=200, # Reduced height slightly | |
key="ingredient_input_widget" # Use a different key for the widget itself if needed, or manage via value | |
) | |
# Update session state if user types manually | |
st.session_state.ingredient_input = text_input | |
st.markdown("---") # Separator | |
st.caption("Matching Options") | |
use_expansion = st.checkbox( | |
"Use Description Expansion (AI)", | |
value=False, | |
key="ingredient_expansion", | |
help="Expand product descriptions using AI before matching" | |
) | |
top_n = st.slider("Top N Results", 1, 25, 10, step=1, key="ingredient_top_n") | |
confidence = st.slider("Similarity Threshold", 0.1, 0.9, 0.5, step=0.05, key="ingredient_confidence") | |
find_ingredients_btn = st.button("Find Similar Ingredients", type="primary", key="ingredient_find", use_container_width=True, icon="π") | |
with col2: | |
with st.container(border=True): | |
# Results section | |
st.subheader("Results") | |
results_placeholder_ingredient = st.container() # Use container for results area | |
# --- Results Display Logic (Ingredient Tab) --- | |
if find_ingredients_btn: | |
if st.session_state.ingredient_input: # Check state value | |
with st.spinner("Finding similar ingredients..."): | |
results_html = categorize_products( | |
st.session_state.ingredient_input, | |
False, | |
use_expansion, | |
top_n, | |
confidence | |
) | |
st.session_state.ingredient_results_html = results_html # Store results | |
results_placeholder_ingredient.markdown(results_html, unsafe_allow_html=True) # Display immediately | |
else: | |
st.session_state.ingredient_results_html = None # Clear results if input is empty | |
results_placeholder_ingredient.warning("Please enter product names.") | |
# Display stored results if button wasn't clicked but results exist | |
elif 'ingredient_results_html' in st.session_state and st.session_state.ingredient_results_html: | |
results_placeholder_ingredient.markdown(st.session_state.ingredient_results_html, unsafe_allow_html=True) | |
else: | |
# Initial state or after clearing | |
results_placeholder_ingredient.write("Results will appear here.") | |
# --- Category Matching Tab --- | |
with tab_category: | |
st.header("Match Products to Categories") | |
col1, col2 = st.columns([1, 2]) # Give more space to results | |
with col1: | |
with st.container(border=True): | |
st.subheader("Input & Options") | |
if st.button("Load Examples", key="category_examples", icon="π"): | |
st.session_state.category_input = load_examples() | |
category_text_input = st.text_area( | |
"Product Names (one per line)", | |
value=st.session_state.category_input, | |
placeholder="Enter product names, one per line", | |
height=200, # Reduced height | |
key="category_input_widget" | |
) | |
st.session_state.category_input = category_text_input | |
st.markdown("---") # Separator | |
st.caption("Matching Options") | |
category_use_expansion = st.checkbox( | |
"Use Description Expansion (AI)", | |
value=False, | |
key="category_expansion", | |
help="Expand product descriptions using AI before matching" | |
) | |
category_top_n = st.slider("Top N Categories", 1, 10, 5, step=1, key="category_top_n") | |
category_confidence = st.slider("Matching Threshold", 0.1, 0.9, 0.5, step=0.05, key="category_confidence") | |
match_categories_btn = st.button("Match to Categories", type="primary", key="category_match", use_container_width=True, icon="π") | |
with col2: | |
with st.container(border=True): | |
st.subheader("Results") | |
results_placeholder_category = st.container() # Use container | |
# --- Results Display Logic (Category Tab) --- | |
if match_categories_btn: | |
if st.session_state.category_input: | |
with st.spinner("Matching to categories..."): | |
results_html = categorize_products_by_category( | |
st.session_state.category_input, | |
False, | |
category_use_expansion, | |
category_top_n, | |
category_confidence | |
) | |
st.session_state.category_results_html = results_html # Store results | |
results_placeholder_category.markdown(results_html, unsafe_allow_html=True) # Display immediately | |
else: | |
st.session_state.category_results_html = None # Clear results if input is empty | |
results_placeholder_category.warning("Please enter product names.") | |
# Display stored results if button wasn't clicked but results exist | |
elif 'category_results_html' in st.session_state and st.session_state.category_results_html: | |
results_placeholder_category.markdown(st.session_state.category_results_html, unsafe_allow_html=True) | |
else: | |
# Initial state or after clearing | |
results_placeholder_category.write("Results will appear here.") | |
# --- Common function for Reranking Tabs --- | |
def create_reranking_ui(tab, tab_key_prefix, tab_name, backend_function, default_match="categories"): | |
with tab: | |
st.header(f"Match using {tab_name}") | |
col1, col2 = st.columns([1, 2]) # Give more space to results | |
with col1: | |
with st.container(border=True): | |
st.subheader("Input & Options") | |
if st.button("Load Examples", key=f"{tab_key_prefix}_examples", icon="π"): | |
st.session_state[f"{tab_key_prefix}_input"] = load_examples() | |
tab_input_value = st.text_area( | |
"Product Names (one per line)", | |
value=st.session_state[f"{tab_key_prefix}_input"], | |
placeholder="Enter product names, one per line", | |
height=200, # Reduced height | |
key=f"{tab_key_prefix}_input_widget" | |
) | |
st.session_state[f"{tab_key_prefix}_input"] = tab_input_value # Update state | |
st.markdown("---") # Separator | |
st.caption("Matching Options") | |
tab_match_type = st.radio( | |
"Match Type", | |
options=["categories", "ingredients"], | |
index=0 if default_match == "categories" else 1, | |
key=f"{tab_key_prefix}_match_type", | |
horizontal=True, | |
help="Choose whether to match against ingredients or categories" | |
) | |
tab_expansion = st.checkbox( | |
"Use Description Expansion (AI)", | |
value=False, | |
key=f"{tab_key_prefix}_expansion", | |
help="Expand product descriptions using AI before matching" | |
) | |
tab_emb_top_n = st.slider("Embedding Top N Results", 1, 50, 20, step=1, key=f"{tab_key_prefix}_emb_top_n", help="How many candidates to fetch initially using embeddings.") | |
tab_top_n = st.slider("Final Top N Results", 1, 10, 5, step=1, key=f"{tab_key_prefix}_final_top_n", help="How many final results to show after reranking.") | |
tab_confidence = st.slider("Matching Threshold", 0.1, 0.9, 0.5, step=0.05, key=f"{tab_key_prefix}_confidence") | |
tab_match_btn = st.button(f"Match using {tab_name}", type="primary", key=f"{tab_key_prefix}_match", use_container_width=True, icon="π") | |
with col2: | |
with st.container(border=True): | |
st.subheader("Results") | |
results_placeholder_rerank = st.container() # Use container | |
# --- Results Display Logic (Reranking Tabs) --- | |
results_key = f"{tab_key_prefix}_results_html" | |
input_key = f"{tab_key_prefix}_input" | |
if tab_match_btn: | |
if st.session_state[input_key]: | |
with st.spinner(f"Matching using {tab_name}..."): | |
results_html = backend_function( | |
st.session_state[input_key], | |
False, | |
tab_expansion, | |
tab_emb_top_n, | |
tab_top_n, | |
tab_confidence, | |
tab_match_type | |
) | |
st.session_state[results_key] = results_html # Store results | |
results_placeholder_rerank.markdown(results_html, unsafe_allow_html=True) # Display immediately | |
else: | |
st.session_state[results_key] = None # Clear results if input is empty | |
results_placeholder_rerank.warning("Please enter product names.") | |
# Display stored results if button wasn't clicked but results exist | |
elif results_key in st.session_state and st.session_state[results_key]: | |
results_placeholder_rerank.markdown(st.session_state[results_key], unsafe_allow_html=True) | |
else: | |
# Initial state or after clearing | |
results_placeholder_rerank.write("Results will appear here.") | |
# Create the reranking tabs | |
create_reranking_ui(tab_voyage, "voyage", "Voyage AI Reranking", categorize_products_with_voyage_reranking, "categories") | |
create_reranking_ui(tab_openai, "openai", "OpenAI Reranking", categorize_products_with_openai_reranking, "categories") | |
# --- Compare Methods Tab --- | |
with tab_compare: | |
st.header("Compare Matching Methods") | |
col1, col2 = st.columns([1, 2]) # Give more space to results | |
with col1: | |
with st.container(border=True): | |
st.subheader("Input & Options") | |
if st.button("Load Examples", key="compare_examples", icon="π"): | |
st.session_state.compare_input = load_examples() | |
compare_product_input_value = st.text_area( | |
"Product Names (one per line)", | |
value=st.session_state.compare_input, | |
placeholder="4 Tbsp sweet pickle relish\nchocolate chips\nfresh parsley", | |
height=200, # Consistent height | |
key="compare_input_widget" | |
) | |
st.session_state.compare_input = compare_product_input_value # Update state | |
st.markdown("---") # Separator | |
st.caption("Comparison Options") | |
compare_match_type = st.radio( | |
"Match Type", | |
options=["categories", "ingredients"], | |
index=0, | |
key="compare_match_type", | |
horizontal=True, | |
help="Choose whether to match against ingredients or categories" | |
) | |
compare_expansion = st.checkbox( | |
"Use Description Expansion (AI)", | |
value=False, | |
key="compare_expansion", | |
help="Expand product descriptions using AI before matching" | |
) | |
compare_embedding_top_n = st.slider( | |
"Initial embedding candidates", | |
min_value=5, max_value=50, value=20, step=5, | |
key="compare_emb_top_n", | |
help="How many candidates to fetch initially using embeddings for reranking methods." | |
) | |
compare_final_top_n = st.slider( | |
"Final results per method", | |
min_value=1, max_value=10, value=3, step=1, | |
key="compare_final_top_n", | |
help="How many final results to show per method." | |
) | |
compare_confidence_threshold = st.slider( | |
"Confidence threshold", | |
min_value=0.0, max_value=1.0, value=0.5, step=0.05, | |
key="compare_confidence" | |
) | |
compare_btn = st.button("Compare Methods", type="primary", key="compare_run", use_container_width=True, icon="π") | |
with col2: | |
with st.container(border=True): | |
st.subheader("Comparison Results") | |
results_placeholder_compare = st.container() # Use container | |
# --- Results Display Logic (Compare Tab) --- | |
if compare_btn: | |
if st.session_state.compare_input: | |
with st.spinner("Comparing methods..."): | |
results_html = compare_ingredient_methods_ui( | |
st.session_state.compare_input, | |
compare_embedding_top_n, | |
compare_final_top_n, | |
compare_confidence_threshold, | |
compare_match_type, | |
compare_expansion | |
) | |
st.session_state.compare_results_html = results_html # Store results | |
results_placeholder_compare.markdown(results_html, unsafe_allow_html=True) # Display immediately | |
else: | |
st.session_state.compare_results_html = None # Clear results if input is empty | |
results_placeholder_compare.warning("Please enter product names.") | |
# Display stored results if button wasn't clicked but results exist | |
elif 'compare_results_html' in st.session_state and st.session_state.compare_results_html: | |
results_placeholder_compare.markdown(st.session_state.compare_results_html, unsafe_allow_html=True) | |
else: | |
# Initial state or after clearing | |
results_placeholder_compare.write("Results will appear here.") | |
st.markdown("---") | |
st.markdown("Powered by Voyage AI embeddings β’ Built with Streamlit") | |