Spaces:
Sleeping
Sleeping
feat: Update UI components
Browse files- comparison.py +4 -5
- ui.py +205 -169
- ui_core.py +2 -5
- ui_formatters.py +211 -529
- ui_hybrid_matching.py +1 -1
- ui_ingredient_matching.py +2 -2
- utils.py +0 -1
comparison.py
CHANGED
@@ -6,7 +6,7 @@ from category_matching import hybrid_category_matching
|
|
6 |
from similarity import hybrid_ingredient_matching
|
7 |
|
8 |
from api_utils import process_in_parallel, rank_ingredients_openai
|
9 |
-
from ui_formatters import format_comparison_html
|
10 |
|
11 |
# from utils import SafeProgress # Removed SafeProgress import
|
12 |
from chicory_api import call_chicory_parser
|
@@ -391,10 +391,9 @@ def compare_ingredient_methods_ui(product_input, embedding_top_n=20,
|
|
391 |
expanded_description=expanded_text
|
392 |
))
|
393 |
|
394 |
-
|
395 |
-
|
396 |
-
|
397 |
-
)
|
398 |
|
399 |
# progress_tracker(1.0, desc="Complete") # Removed progress
|
400 |
return output_html
|
|
|
6 |
from similarity import hybrid_ingredient_matching
|
7 |
|
8 |
from api_utils import process_in_parallel, rank_ingredients_openai
|
9 |
+
from ui_formatters import format_comparison_html
|
10 |
|
11 |
# from utils import SafeProgress # Removed SafeProgress import
|
12 |
from chicory_api import call_chicory_parser
|
|
|
391 |
expanded_description=expanded_text
|
392 |
))
|
393 |
|
394 |
+
# Join the HTML elements for each product directly
|
395 |
+
# Add the header text manually before joining
|
396 |
+
output_html = f"<p>{header_text}</p>" + "".join(result_elements)
|
|
|
397 |
|
398 |
# progress_tracker(1.0, desc="Complete") # Removed progress
|
399 |
return output_html
|
ui.py
CHANGED
@@ -42,150 +42,176 @@ def render_ui():
|
|
42 |
# --- Ingredient Matching Tab ---
|
43 |
with tab_ingredient:
|
44 |
st.header("Match Products to Ingredients")
|
45 |
-
col1, col2 = st.columns(2)
|
46 |
with col1:
|
47 |
-
|
48 |
-
|
49 |
-
|
|
|
|
|
50 |
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
|
62 |
-
|
63 |
-
"
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
|
|
|
|
|
70 |
|
71 |
-
|
72 |
|
73 |
with col2:
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
if
|
79 |
-
|
80 |
-
st.
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
|
|
|
|
|
|
|
|
|
87 |
else:
|
88 |
-
|
|
|
89 |
|
90 |
# --- Category Matching Tab ---
|
91 |
with tab_category:
|
92 |
st.header("Match Products to Categories")
|
93 |
-
col1, col2 = st.columns(2)
|
94 |
with col1:
|
95 |
-
|
96 |
-
st.
|
|
|
|
|
97 |
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
|
107 |
-
|
108 |
-
"
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
|
114 |
-
|
|
|
|
|
115 |
|
116 |
-
|
117 |
|
118 |
with col2:
|
119 |
-
st.
|
120 |
-
|
121 |
-
|
122 |
-
if
|
123 |
-
|
124 |
-
st.
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
|
130 |
-
|
|
|
|
|
|
|
|
|
131 |
else:
|
132 |
-
results_placeholder_category.
|
133 |
|
134 |
# --- Common function for Reranking Tabs ---
|
135 |
def create_reranking_ui(tab, tab_key_prefix, tab_name, backend_function, default_match="categories"):
|
136 |
with tab:
|
137 |
st.header(f"Match using {tab_name}")
|
138 |
-
col1, col2 = st.columns(2)
|
139 |
with col1:
|
140 |
-
|
141 |
-
st.
|
|
|
|
|
142 |
|
143 |
-
|
144 |
-
|
145 |
-
|
146 |
-
|
147 |
-
|
148 |
-
|
149 |
-
|
150 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
151 |
|
152 |
-
tab_expansion = st.checkbox(
|
153 |
-
"Use Description Expansion (AI)",
|
154 |
-
value=False,
|
155 |
-
key=f"{tab_key_prefix}_expansion",
|
156 |
-
help="Expand product descriptions using AI before matching"
|
157 |
-
)
|
158 |
-
tab_emb_top_n = st.slider("Embedding Top N Results", 1, 50, 20, step=1, key=f"{tab_key_prefix}_emb_top_n")
|
159 |
-
tab_top_n = st.slider("Final Top N Results", 1, 10, 5, step=1, key=f"{tab_key_prefix}_final_top_n")
|
160 |
-
tab_confidence = st.slider("Matching Threshold", 0.1, 0.9, 0.5, step=0.05, key=f"{tab_key_prefix}_confidence")
|
161 |
-
tab_match_type = st.radio(
|
162 |
-
"Match Type",
|
163 |
-
options=["categories", "ingredients"],
|
164 |
-
index=0 if default_match == "categories" else 1,
|
165 |
-
key=f"{tab_key_prefix}_match_type",
|
166 |
-
horizontal=True,
|
167 |
-
help="Choose whether to match against ingredients or categories"
|
168 |
-
)
|
169 |
|
170 |
-
|
171 |
|
172 |
with col2:
|
173 |
-
st.
|
174 |
-
|
175 |
-
|
176 |
-
if
|
177 |
-
|
178 |
-
st.
|
179 |
-
|
180 |
-
|
181 |
-
|
182 |
-
|
183 |
-
|
184 |
-
|
185 |
-
|
186 |
-
|
|
|
|
|
|
|
|
|
187 |
else:
|
188 |
-
results_placeholder_rerank.
|
189 |
|
190 |
# Create the reranking tabs
|
191 |
create_reranking_ui(tab_voyage, "voyage", "Voyage AI Reranking", categorize_products_with_voyage_reranking, "categories")
|
@@ -194,68 +220,78 @@ def render_ui():
|
|
194 |
# --- Compare Methods Tab ---
|
195 |
with tab_compare:
|
196 |
st.header("Compare Matching Methods")
|
197 |
-
col1, col2 = st.columns(2)
|
198 |
with col1:
|
199 |
-
|
200 |
-
st.
|
|
|
|
|
201 |
|
202 |
-
|
203 |
-
|
204 |
-
|
205 |
-
|
206 |
-
|
207 |
-
|
208 |
-
|
209 |
-
|
210 |
|
211 |
-
|
212 |
-
"
|
213 |
-
|
214 |
-
|
215 |
-
|
216 |
-
|
217 |
-
|
218 |
-
|
219 |
-
|
220 |
-
|
221 |
-
|
222 |
-
|
223 |
-
|
224 |
-
|
225 |
-
|
226 |
-
|
227 |
-
|
228 |
-
|
229 |
-
|
230 |
-
|
231 |
-
|
232 |
-
|
233 |
-
|
234 |
-
|
235 |
-
|
236 |
-
|
237 |
-
|
238 |
-
|
239 |
-
|
|
|
|
|
|
|
|
|
240 |
|
241 |
-
|
242 |
|
243 |
with col2:
|
244 |
-
st.
|
245 |
-
|
246 |
-
|
247 |
-
if
|
248 |
-
|
249 |
-
st.
|
250 |
-
|
251 |
-
|
252 |
-
|
253 |
-
|
254 |
-
|
255 |
-
|
256 |
-
|
|
|
|
|
|
|
|
|
257 |
else:
|
258 |
-
results_placeholder_compare.
|
259 |
|
260 |
st.markdown("---")
|
261 |
st.markdown("Powered by Voyage AI embeddings • Built with Streamlit")
|
|
|
42 |
# --- Ingredient Matching Tab ---
|
43 |
with tab_ingredient:
|
44 |
st.header("Match Products to Ingredients")
|
45 |
+
col1, col2 = st.columns([1, 2]) # Give more space to results
|
46 |
with col1:
|
47 |
+
with st.container(border=True):
|
48 |
+
st.subheader("Input & Options")
|
49 |
+
# Handle button click *before* rendering the text area
|
50 |
+
if st.button("Load Examples", key="ingredient_examples"):
|
51 |
+
st.session_state.ingredient_input = load_examples() # Update state for next rerun
|
52 |
|
53 |
+
# Input section - Use the session state value
|
54 |
+
text_input = st.text_area(
|
55 |
+
"Product Names (one per line)",
|
56 |
+
value=st.session_state.ingredient_input, # Use value from state
|
57 |
+
placeholder="Enter product names, one per line",
|
58 |
+
height=200, # Reduced height slightly
|
59 |
+
key="ingredient_input_widget" # Use a different key for the widget itself if needed, or manage via value
|
60 |
+
)
|
61 |
+
# Update session state if user types manually
|
62 |
+
st.session_state.ingredient_input = text_input
|
63 |
|
64 |
+
st.markdown("---") # Separator
|
65 |
+
st.caption("Matching Options")
|
66 |
+
use_expansion = st.checkbox(
|
67 |
+
"Use Description Expansion (AI)",
|
68 |
+
value=False,
|
69 |
+
key="ingredient_expansion",
|
70 |
+
help="Expand product descriptions using AI before matching"
|
71 |
+
)
|
72 |
+
top_n = st.slider("Top N Results", 1, 25, 10, step=1, key="ingredient_top_n")
|
73 |
+
confidence = st.slider("Similarity Threshold", 0.1, 0.9, 0.5, step=0.05, key="ingredient_confidence")
|
74 |
|
75 |
+
find_ingredients_btn = st.button("Find Similar Ingredients", type="primary", key="ingredient_find", use_container_width=True)
|
76 |
|
77 |
with col2:
|
78 |
+
with st.container(border=True):
|
79 |
+
# Results section
|
80 |
+
st.subheader("Results")
|
81 |
+
results_placeholder_ingredient = st.container() # Use container for results area
|
82 |
+
if find_ingredients_btn:
|
83 |
+
if st.session_state.ingredient_input: # Check state value
|
84 |
+
with st.spinner("Finding similar ingredients..."):
|
85 |
+
results_html = categorize_products(
|
86 |
+
st.session_state.ingredient_input,
|
87 |
+
False,
|
88 |
+
use_expansion,
|
89 |
+
top_n,
|
90 |
+
confidence
|
91 |
+
)
|
92 |
+
results_placeholder_ingredient.markdown(results_html, unsafe_allow_html=True)
|
93 |
+
else:
|
94 |
+
results_placeholder_ingredient.warning("Please enter product names.")
|
95 |
else:
|
96 |
+
# Keep the placeholder visible even when no button is clicked yet
|
97 |
+
results_placeholder_ingredient.write("Results will appear here.")
|
98 |
|
99 |
# --- Category Matching Tab ---
|
100 |
with tab_category:
|
101 |
st.header("Match Products to Categories")
|
102 |
+
col1, col2 = st.columns([1, 2]) # Give more space to results
|
103 |
with col1:
|
104 |
+
with st.container(border=True):
|
105 |
+
st.subheader("Input & Options")
|
106 |
+
if st.button("Load Examples", key="category_examples"):
|
107 |
+
st.session_state.category_input = load_examples()
|
108 |
|
109 |
+
category_text_input = st.text_area(
|
110 |
+
"Product Names (one per line)",
|
111 |
+
value=st.session_state.category_input,
|
112 |
+
placeholder="Enter product names, one per line",
|
113 |
+
height=200, # Reduced height
|
114 |
+
key="category_input_widget"
|
115 |
+
)
|
116 |
+
st.session_state.category_input = category_text_input
|
117 |
|
118 |
+
st.markdown("---") # Separator
|
119 |
+
st.caption("Matching Options")
|
120 |
+
category_use_expansion = st.checkbox(
|
121 |
+
"Use Description Expansion (AI)",
|
122 |
+
value=False,
|
123 |
+
key="category_expansion",
|
124 |
+
help="Expand product descriptions using AI before matching"
|
125 |
+
)
|
126 |
+
category_top_n = st.slider("Top N Categories", 1, 10, 5, step=1, key="category_top_n")
|
127 |
+
category_confidence = st.slider("Matching Threshold", 0.1, 0.9, 0.5, step=0.05, key="category_confidence")
|
128 |
|
129 |
+
match_categories_btn = st.button("Match to Categories", type="primary", key="category_match", use_container_width=True)
|
130 |
|
131 |
with col2:
|
132 |
+
with st.container(border=True):
|
133 |
+
st.subheader("Results")
|
134 |
+
results_placeholder_category = st.container() # Use container
|
135 |
+
if match_categories_btn:
|
136 |
+
if st.session_state.category_input:
|
137 |
+
with st.spinner("Matching to categories..."):
|
138 |
+
results_html = categorize_products_by_category(
|
139 |
+
st.session_state.category_input,
|
140 |
+
False,
|
141 |
+
category_use_expansion,
|
142 |
+
category_top_n,
|
143 |
+
category_confidence
|
144 |
+
)
|
145 |
+
results_placeholder_category.markdown(results_html, unsafe_allow_html=True)
|
146 |
+
else:
|
147 |
+
results_placeholder_category.warning("Please enter product names.")
|
148 |
else:
|
149 |
+
results_placeholder_category.write("Results will appear here.")
|
150 |
|
151 |
# --- Common function for Reranking Tabs ---
|
152 |
def create_reranking_ui(tab, tab_key_prefix, tab_name, backend_function, default_match="categories"):
|
153 |
with tab:
|
154 |
st.header(f"Match using {tab_name}")
|
155 |
+
col1, col2 = st.columns([1, 2]) # Give more space to results
|
156 |
with col1:
|
157 |
+
with st.container(border=True):
|
158 |
+
st.subheader("Input & Options")
|
159 |
+
if st.button("Load Examples", key=f"{tab_key_prefix}_examples"):
|
160 |
+
st.session_state[f"{tab_key_prefix}_input"] = load_examples()
|
161 |
|
162 |
+
tab_input_value = st.text_area(
|
163 |
+
"Product Names (one per line)",
|
164 |
+
value=st.session_state[f"{tab_key_prefix}_input"],
|
165 |
+
placeholder="Enter product names, one per line",
|
166 |
+
height=200, # Reduced height
|
167 |
+
key=f"{tab_key_prefix}_input_widget"
|
168 |
+
)
|
169 |
+
st.session_state[f"{tab_key_prefix}_input"] = tab_input_value # Update state
|
170 |
+
|
171 |
+
st.markdown("---") # Separator
|
172 |
+
st.caption("Matching Options")
|
173 |
+
tab_match_type = st.radio(
|
174 |
+
"Match Type",
|
175 |
+
options=["categories", "ingredients"],
|
176 |
+
index=0 if default_match == "categories" else 1,
|
177 |
+
key=f"{tab_key_prefix}_match_type",
|
178 |
+
horizontal=True,
|
179 |
+
help="Choose whether to match against ingredients or categories"
|
180 |
+
)
|
181 |
+
tab_expansion = st.checkbox(
|
182 |
+
"Use Description Expansion (AI)",
|
183 |
+
value=False,
|
184 |
+
key=f"{tab_key_prefix}_expansion",
|
185 |
+
help="Expand product descriptions using AI before matching"
|
186 |
+
)
|
187 |
+
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.")
|
188 |
+
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.")
|
189 |
+
tab_confidence = st.slider("Matching Threshold", 0.1, 0.9, 0.5, step=0.05, key=f"{tab_key_prefix}_confidence")
|
190 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
191 |
|
192 |
+
tab_match_btn = st.button(f"Match using {tab_name}", type="primary", key=f"{tab_key_prefix}_match", use_container_width=True)
|
193 |
|
194 |
with col2:
|
195 |
+
with st.container(border=True):
|
196 |
+
st.subheader("Results")
|
197 |
+
results_placeholder_rerank = st.container() # Use container
|
198 |
+
if tab_match_btn:
|
199 |
+
if st.session_state[f"{tab_key_prefix}_input"]:
|
200 |
+
with st.spinner(f"Matching using {tab_name}..."):
|
201 |
+
results_html = backend_function(
|
202 |
+
st.session_state[f"{tab_key_prefix}_input"],
|
203 |
+
False,
|
204 |
+
tab_expansion,
|
205 |
+
tab_emb_top_n,
|
206 |
+
tab_top_n,
|
207 |
+
tab_confidence,
|
208 |
+
tab_match_type
|
209 |
+
)
|
210 |
+
results_placeholder_rerank.markdown(results_html, unsafe_allow_html=True)
|
211 |
+
else:
|
212 |
+
results_placeholder_rerank.warning("Please enter product names.")
|
213 |
else:
|
214 |
+
results_placeholder_rerank.write("Results will appear here.")
|
215 |
|
216 |
# Create the reranking tabs
|
217 |
create_reranking_ui(tab_voyage, "voyage", "Voyage AI Reranking", categorize_products_with_voyage_reranking, "categories")
|
|
|
220 |
# --- Compare Methods Tab ---
|
221 |
with tab_compare:
|
222 |
st.header("Compare Matching Methods")
|
223 |
+
col1, col2 = st.columns([1, 2]) # Give more space to results
|
224 |
with col1:
|
225 |
+
with st.container(border=True):
|
226 |
+
st.subheader("Input & Options")
|
227 |
+
if st.button("Load Examples", key="compare_examples"):
|
228 |
+
st.session_state.compare_input = load_examples()
|
229 |
|
230 |
+
compare_product_input_value = st.text_area(
|
231 |
+
"Product Names (one per line)",
|
232 |
+
value=st.session_state.compare_input,
|
233 |
+
placeholder="4 Tbsp sweet pickle relish\nchocolate chips\nfresh parsley",
|
234 |
+
height=150, # Reduced height
|
235 |
+
key="compare_input_widget"
|
236 |
+
)
|
237 |
+
st.session_state.compare_input = compare_product_input_value # Update state
|
238 |
|
239 |
+
st.markdown("---") # Separator
|
240 |
+
st.caption("Comparison Options")
|
241 |
+
compare_match_type = st.radio(
|
242 |
+
"Match Type",
|
243 |
+
options=["categories", "ingredients"],
|
244 |
+
index=0,
|
245 |
+
key="compare_match_type",
|
246 |
+
horizontal=True,
|
247 |
+
help="Choose whether to match against ingredients or categories"
|
248 |
+
)
|
249 |
+
compare_expansion = st.checkbox(
|
250 |
+
"Use Description Expansion (AI)",
|
251 |
+
value=False,
|
252 |
+
key="compare_expansion",
|
253 |
+
help="Expand product descriptions using AI before matching"
|
254 |
+
)
|
255 |
+
compare_embedding_top_n = st.slider(
|
256 |
+
"Initial embedding candidates",
|
257 |
+
min_value=5, max_value=50, value=20, step=5,
|
258 |
+
key="compare_emb_top_n",
|
259 |
+
help="How many candidates to fetch initially using embeddings for reranking methods."
|
260 |
+
)
|
261 |
+
compare_final_top_n = st.slider(
|
262 |
+
"Final results per method",
|
263 |
+
min_value=1, max_value=10, value=3, step=1,
|
264 |
+
key="compare_final_top_n",
|
265 |
+
help="How many final results to show per method."
|
266 |
+
)
|
267 |
+
compare_confidence_threshold = st.slider(
|
268 |
+
"Confidence threshold",
|
269 |
+
min_value=0.0, max_value=1.0, value=0.5, step=0.05,
|
270 |
+
key="compare_confidence"
|
271 |
+
)
|
272 |
|
273 |
+
compare_btn = st.button("Compare Methods", type="primary", key="compare_run", use_container_width=True)
|
274 |
|
275 |
with col2:
|
276 |
+
with st.container(border=True):
|
277 |
+
st.subheader("Comparison Results")
|
278 |
+
results_placeholder_compare = st.container() # Use container
|
279 |
+
if compare_btn:
|
280 |
+
if st.session_state.compare_input:
|
281 |
+
with st.spinner("Comparing methods..."):
|
282 |
+
results_html = compare_ingredient_methods_ui(
|
283 |
+
st.session_state.compare_input,
|
284 |
+
compare_embedding_top_n,
|
285 |
+
compare_final_top_n,
|
286 |
+
compare_confidence_threshold,
|
287 |
+
compare_match_type,
|
288 |
+
compare_expansion
|
289 |
+
)
|
290 |
+
results_placeholder_compare.markdown(results_html, unsafe_allow_html=True)
|
291 |
+
else:
|
292 |
+
results_placeholder_compare.warning("Please enter product names.")
|
293 |
else:
|
294 |
+
results_placeholder_compare.write("Results will appear here.")
|
295 |
|
296 |
st.markdown("---")
|
297 |
st.markdown("Powered by Voyage AI embeddings • Built with Streamlit")
|
ui_core.py
CHANGED
@@ -114,11 +114,8 @@ def load_examples():
|
|
114 |
"""Load example product names into the text input"""
|
115 |
return EXAMPLE_PRODUCTS
|
116 |
|
117 |
-
|
118 |
-
|
119 |
-
def get_css():
|
120 |
-
"""Return the CSS for the Gradio interface"""
|
121 |
-
return get_formatted_css()
|
122 |
|
123 |
def parse_input(input_text, is_file=False) -> Tuple[List[str], Optional[str]]:
|
124 |
"""Parse user input into a list of product names"""
|
|
|
114 |
"""Load example product names into the text input"""
|
115 |
return EXAMPLE_PRODUCTS
|
116 |
|
117 |
+
# Removed obsolete theme/CSS imports and functions previously here.
|
118 |
+
# Theme is now handled by Streamlit via .streamlit/config.toml
|
|
|
|
|
|
|
119 |
|
120 |
def parse_input(input_text, is_file=False) -> Tuple[List[str], Optional[str]]:
|
121 |
"""Parse user input into a list of product names"""
|
ui_formatters.py
CHANGED
@@ -1,50 +1,6 @@
|
|
1 |
from typing import List, Dict, Tuple, Any
|
2 |
from utils import get_confidence_color, get_confidence_bg_color
|
3 |
|
4 |
-
# Theme configuration (can be easily switched between light/dark)
|
5 |
-
THEME = "dark" # Options: "light", "dark"
|
6 |
-
|
7 |
-
# Theme-specific colors
|
8 |
-
THEMES = {
|
9 |
-
"light": {
|
10 |
-
"background": "#ffffff",
|
11 |
-
"card_bg": "#ffffff",
|
12 |
-
"card_border": "#ddd",
|
13 |
-
"header_bg": "#2c3e50",
|
14 |
-
"header_text": "#ffffff",
|
15 |
-
"text_primary": "#333333",
|
16 |
-
"text_secondary": "#555555",
|
17 |
-
"section_bg": "#f8f9fa",
|
18 |
-
},
|
19 |
-
"dark": {
|
20 |
-
"background": "#121212",
|
21 |
-
"card_bg": "#1e1e1e",
|
22 |
-
"card_border": "#333",
|
23 |
-
"header_bg": "#37474f",
|
24 |
-
"header_text": "#ffffff",
|
25 |
-
"text_primary": "#e0e0e0",
|
26 |
-
"text_secondary": "#b0bec5",
|
27 |
-
"section_bg": "#263238",
|
28 |
-
}
|
29 |
-
}
|
30 |
-
|
31 |
-
# Get current theme colors
|
32 |
-
COLORS = THEMES[THEME]
|
33 |
-
|
34 |
-
# Base styling constants (adjusted based on theme)
|
35 |
-
STYLES = {
|
36 |
-
"card": f"margin-bottom: 20px; border: 1px solid {COLORS['card_border']}; border-radius: 8px; overflow: hidden; background-color: {COLORS['card_bg']};",
|
37 |
-
"header": f"background-color: {COLORS['header_bg']}; padding: 12px 15px; border-bottom: 1px solid {COLORS['card_border']};",
|
38 |
-
"header_text": f"margin: 0; font-size: 18px; color: {COLORS['header_text']};",
|
39 |
-
"flex_container": "display: flex; flex-wrap: wrap;",
|
40 |
-
"method_container": f"flex: 1; width: 100%; padding: 15px; border-bottom: 1px solid {COLORS['card_border']};",
|
41 |
-
"method_title": f"margin-top: 0; color: {COLORS['text_primary']}; padding-bottom: 8px;",
|
42 |
-
"item_list": "list-style-type: none; padding-left: 0;",
|
43 |
-
"item": "margin-bottom: 8px; padding: 8px; border-radius: 4px;",
|
44 |
-
"empty_message": "color: #7f8c8d; font-style: italic;",
|
45 |
-
"info_panel": f"padding: 10px; background-color: {COLORS['section_bg']}; margin-bottom: 10px; border-radius: 4px;"
|
46 |
-
}
|
47 |
-
|
48 |
# Method colors (consistent across themes)
|
49 |
METHOD_COLORS = {
|
50 |
"base": "#f39c12", # Orange
|
@@ -53,7 +9,8 @@ METHOD_COLORS = {
|
|
53 |
"openai": "#2ecc71", # Green
|
54 |
"expanded": "#e74c3c", # Red
|
55 |
"hybrid": "#1abc9c", # Turquoise
|
56 |
-
"categories": "#1abc9c" # Same as hybrid
|
|
|
57 |
}
|
58 |
|
59 |
# Method display names
|
@@ -61,515 +18,240 @@ METHOD_NAMES = {
|
|
61 |
"base": "Base Embeddings",
|
62 |
"voyage": "Voyage AI Reranker",
|
63 |
"chicory": "Chicory Parser",
|
64 |
-
"openai": "OpenAI",
|
65 |
"expanded": "Expanded Description",
|
66 |
"hybrid": "Hybrid Matching",
|
67 |
"categories": "Category Matches",
|
68 |
"ingredients": "Ingredient Matches"
|
69 |
}
|
70 |
|
71 |
-
def
|
72 |
-
"""
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
#
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
|
121 |
-
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
confidence_color = get_confidence_color(score)
|
130 |
-
bg_color = get_confidence_bg_color(score)
|
131 |
-
|
132 |
-
# Improved layout with better contrast and labeled confidence
|
133 |
-
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);'>"
|
134 |
-
html += f"<span style='font-weight: 500; flex: 1;'>{name}</span>"
|
135 |
-
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>"
|
136 |
-
html += "</li>"
|
137 |
-
|
138 |
-
html += "</ul>"
|
139 |
-
else:
|
140 |
-
html += f"<p style='{STYLES['empty_message']}'>No results found</p>"
|
141 |
-
|
142 |
-
html += "</div>"
|
143 |
-
return html
|
144 |
|
145 |
-
def
|
146 |
-
"""
|
147 |
-
|
148 |
-
|
149 |
-
|
150 |
-
|
151 |
-
|
152 |
-
|
153 |
-
|
154 |
-
|
155 |
-
|
156 |
-
|
157 |
-
|
158 |
-
|
159 |
-
|
160 |
-
|
161 |
-
|
162 |
-
|
163 |
-
|
164 |
-
|
165 |
-
|
|
|
166 |
return html
|
167 |
|
168 |
-
def
|
169 |
-
"""
|
170 |
-
|
171 |
-
|
172 |
-
|
173 |
-
|
174 |
-
|
175 |
-
|
176 |
-
|
177 |
-
|
178 |
-
|
179 |
-
"""
|
180 |
-
#
|
181 |
-
|
182 |
-
|
183 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
184 |
if expanded_description:
|
185 |
-
|
186 |
-
|
187 |
-
|
188 |
-
|
189 |
-
|
190 |
-
|
191 |
-
|
192 |
-
|
193 |
-
|
194 |
-
|
195 |
-
|
196 |
-
|
197 |
-
|
198 |
-
|
199 |
-
|
200 |
-
|
|
|
|
|
201 |
|
202 |
-
|
|
|
|
|
|
|
|
|
|
|
203 |
method="voyage", confidence_threshold=0.0):
|
204 |
-
"""
|
205 |
-
|
206 |
-
|
207 |
-
|
208 |
-
Args:
|
209 |
-
results: List of result dictionaries
|
210 |
-
match_type: Either "ingredients" or "categories"
|
211 |
-
show_scores: Whether to show confidence scores
|
212 |
-
include_explanation: Whether to include expanded descriptions
|
213 |
-
method: Method used for ranking ("voyage" or "openai")
|
214 |
-
confidence_threshold: Threshold for filtering individual items (default 0.0 shows all)
|
215 |
-
|
216 |
-
Returns:
|
217 |
-
HTML string for displaying results
|
218 |
-
"""
|
219 |
-
if not results or len(results) == 0:
|
220 |
-
return f"No {match_type.lower()} matches found."
|
221 |
-
|
222 |
-
# Method-specific styling
|
223 |
-
method_color = METHOD_COLORS.get(method, "#777777")
|
224 |
-
method_name = METHOD_NAMES.get(method, method.capitalize())
|
225 |
-
|
226 |
-
# Create a header text
|
227 |
-
header_text = f"Matched {len(results)} products to {match_type} using {method_name}"
|
228 |
-
|
229 |
-
# Generate individual HTML elements for each result - using the old style approach
|
230 |
html_elements = []
|
231 |
-
|
232 |
for result in results:
|
233 |
-
product_name = result.get("product_name", "")
|
234 |
matching_items = result.get("matching_items", [])
|
235 |
item_scores = result.get("item_scores", [])
|
236 |
explanation = result.get("explanation", "") if include_explanation else ""
|
237 |
-
|
238 |
-
# Convert matching items into tuples with scores for format_expanded_results_html
|
239 |
-
formatted_matches = []
|
240 |
-
|
241 |
-
# Make sure we have scores for all items
|
242 |
if len(item_scores) != len(matching_items):
|
243 |
-
|
244 |
-
|
245 |
-
|
246 |
-
|
247 |
for i, item in enumerate(matching_items):
|
248 |
-
|
249 |
-
|
250 |
-
|
251 |
-
|
252 |
-
|
253 |
-
cat_text = parts[1].strip() if len(parts) > 1 else ""
|
254 |
-
formatted_matches.append((cat_id, cat_text, score))
|
255 |
else:
|
256 |
-
|
257 |
-
formatted_matches.append((item, score))
|
258 |
-
|
259 |
-
# Only skip if there are no matches at all
|
260 |
-
if not formatted_matches:
|
261 |
-
continue
|
262 |
-
|
263 |
-
# Use the older style formatter with threshold
|
264 |
-
if include_explanation:
|
265 |
-
# Use expanded_results_html for the old style with expanded descriptions
|
266 |
-
element_html = format_expanded_results_html(
|
267 |
-
product=product_name,
|
268 |
-
results=formatted_matches,
|
269 |
-
expanded_description=explanation,
|
270 |
-
match_type=match_type,
|
271 |
-
confidence_threshold=confidence_threshold
|
272 |
-
)
|
273 |
-
else:
|
274 |
-
# Use hybrid_results_html when no expanded description is available
|
275 |
-
summary_text = f"{match_type.capitalize()} matches using {method_name}."
|
276 |
-
element_html = format_hybrid_results_html(
|
277 |
-
product=product_name,
|
278 |
-
results=formatted_matches,
|
279 |
-
summary=summary_text,
|
280 |
-
expanded_description="",
|
281 |
-
confidence_threshold=confidence_threshold
|
282 |
-
)
|
283 |
-
|
284 |
-
html_elements.append(element_html)
|
285 |
-
|
286 |
-
# Combine all elements into a container
|
287 |
-
return create_results_container(html_elements, header_text=header_text)
|
288 |
-
|
289 |
-
def create_results_container(html_elements, header_text=None):
|
290 |
-
"""
|
291 |
-
Create a container for multiple results
|
292 |
-
|
293 |
-
Args:
|
294 |
-
html_elements: List of HTML strings to include
|
295 |
-
header_text: Optional header text
|
296 |
-
|
297 |
-
Returns:
|
298 |
-
HTML string for the container
|
299 |
-
"""
|
300 |
-
container = "<div class='results-container' style='font-family: Arial, sans-serif;'>"
|
301 |
-
|
302 |
-
if header_text:
|
303 |
-
container += f"<p style='color: {COLORS['text_secondary']};'>{header_text}</p>"
|
304 |
-
|
305 |
-
container += ''.join(html_elements)
|
306 |
-
container += "</div>"
|
307 |
-
|
308 |
-
return container
|
309 |
|
310 |
-
|
311 |
-
|
312 |
-
|
313 |
-
for item in results:
|
314 |
-
# Handle both 2-value (match, score) and 3-value (id, text, score) tuples
|
315 |
-
score = item[-1] if isinstance(item, tuple) and len(item) >= 2 else 0.0
|
316 |
-
# Only include results above the threshold
|
317 |
-
if score >= confidence_threshold:
|
318 |
-
filtered_results.append(item)
|
319 |
-
return filtered_results
|
320 |
|
321 |
-
|
322 |
-
|
323 |
-
|
324 |
-
|
325 |
-
if
|
326 |
-
|
327 |
-
|
328 |
-
|
329 |
-
|
330 |
-
|
331 |
-
|
332 |
-
|
333 |
-
|
334 |
-
|
335 |
-
display_text = str(item)
|
336 |
-
score = 0.0
|
337 |
-
return display_text, score
|
338 |
|
339 |
-
def format_expanded_results_html(product, results, expanded_description, match_type="ingredients", confidence_threshold=0.0):
|
340 |
-
"""Format results using expanded descriptions"""
|
341 |
-
content = ""
|
342 |
-
|
343 |
-
# Add expanded description section
|
344 |
-
content += f"<div style='{STYLES['info_panel']}'>"
|
345 |
-
content += "<h4 style='margin-top: 0; border-bottom: 1px solid rgba(0,0,0,0.1); padding-bottom: 8px;'>Expanded Description</h4>"
|
346 |
-
content += f"<p style='margin-bottom: 8px;'>{expanded_description}</p>"
|
347 |
-
content += "</div>"
|
348 |
-
|
349 |
-
# Format the results section - create custom section
|
350 |
-
color_hex = METHOD_COLORS.get(match_type, "#1abc9c")
|
351 |
-
|
352 |
-
# Add results section with custom title
|
353 |
-
content += f"<div class='method-results' style='margin-top: 15px; border-left: 3px solid {color_hex}; padding-left: 15px;'>"
|
354 |
-
title_text = "Ingredients" if match_type == "ingredients" else "Categories"
|
355 |
-
content += f"<h4 style='margin-top: 0; color: {color_hex};'>{title_text}</h4>"
|
356 |
-
|
357 |
-
# Filter results by confidence threshold
|
358 |
-
filtered_results = filter_results_by_threshold(results, confidence_threshold)
|
359 |
-
|
360 |
-
if filtered_results:
|
361 |
-
content += "<ul style='margin-top: 5px; padding-left: 20px;'>"
|
362 |
-
for item in filtered_results:
|
363 |
-
display_text, score = parse_result_item(item)
|
364 |
-
confidence_percent = int(score * 100)
|
365 |
-
# Improved styling for confidence percentage - using black text for better contrast
|
366 |
-
confidence_color = get_confidence_color(score)
|
367 |
-
bg_color = get_confidence_bg_color(score)
|
368 |
-
content += f"<li style='display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px;'>"
|
369 |
-
content += f"<span style='font-weight: 500; flex: 1;'>{display_text}</span>"
|
370 |
-
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>"
|
371 |
-
content += "</li>"
|
372 |
-
content += "</ul>"
|
373 |
-
else:
|
374 |
-
content += "<p style='color: #777; font-style: italic; margin: 5px 0;'>No matches found above confidence threshold.</p>"
|
375 |
-
|
376 |
-
content += "</div>"
|
377 |
-
|
378 |
-
return format_result_card(title=product, content=content)
|
379 |
-
|
380 |
-
def format_hybrid_results_html(product, results, summary, expanded_description="", confidence_threshold=0.0):
|
381 |
-
"""
|
382 |
-
Format results for hybrid matching
|
383 |
-
|
384 |
-
Args:
|
385 |
-
product: Product name
|
386 |
-
results: List of result tuples (name, score) or (id, name, score)
|
387 |
-
summary: Summary text to display
|
388 |
-
expanded_description: Optional expanded description
|
389 |
-
confidence_threshold: Threshold for filtering individual items
|
390 |
-
|
391 |
-
Returns:
|
392 |
-
HTML string for displaying results
|
393 |
-
"""
|
394 |
-
content = ""
|
395 |
-
|
396 |
-
# Add summary text
|
397 |
-
if summary:
|
398 |
-
content += f"<div style='{STYLES['info_panel']}'>"
|
399 |
-
content += f"<p style='margin: 0;'>{summary}</p>"
|
400 |
-
content += "</div>"
|
401 |
-
|
402 |
-
# Add expanded description if provided
|
403 |
-
if expanded_description:
|
404 |
-
content += f"<div style='{STYLES['info_panel']}'>"
|
405 |
-
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>"
|
406 |
-
content += f"<p style='margin: 0;'>{expanded_description}</p>"
|
407 |
-
content += "</div>"
|
408 |
-
|
409 |
-
# Filter results by confidence threshold
|
410 |
-
filtered_results = filter_results_by_threshold(results, confidence_threshold)
|
411 |
-
|
412 |
-
# Format the results
|
413 |
-
if filtered_results:
|
414 |
-
content += "<div style='padding: 10px;'>"
|
415 |
-
content += "<table style='width: 100%; border-collapse: collapse;'>"
|
416 |
-
content += "<thead><tr>"
|
417 |
-
content += "<th style='text-align: left; padding: 8px; border-bottom: 2px solid #ddd;'>Match</th>"
|
418 |
-
content += "<th style='text-align: right; padding: 8px; border-bottom: 2px solid #ddd; width: 100px;'>Confidence</th>"
|
419 |
-
content += "</tr></thead>"
|
420 |
-
content += "<tbody>"
|
421 |
-
|
422 |
-
for item in filtered_results:
|
423 |
-
display_text, score = parse_result_item(item)
|
424 |
-
confidence_percent = int(score * 100)
|
425 |
-
confidence_color = get_confidence_color(score)
|
426 |
-
bg_color = get_confidence_bg_color(score)
|
427 |
-
|
428 |
-
content += "<tr>"
|
429 |
-
content += f"<td style='text-align: left; padding: 8px; border-bottom: 1px solid #ddd;'>{display_text}</td>"
|
430 |
-
content += f"<td style='text-align: center; padding: 8px; border-bottom: 1px solid #ddd;'>"
|
431 |
-
content += f"<span style='background-color: {bg_color}; border: 1px solid {confidence_color}; color: #000;"
|
432 |
-
content += f"font-weight: 600; padding: 2px 6px; border-radius: 4px; display: inline-block; width: 70px;'>"
|
433 |
-
content += f"{confidence_percent}%</span></td>"
|
434 |
-
content += "</tr>"
|
435 |
-
|
436 |
-
content += "</tbody></table>"
|
437 |
-
content += "</div>"
|
438 |
-
else:
|
439 |
-
content += "<p style='color: #777; font-style: italic; padding: 10px; margin: 0;'>No matches found above confidence threshold.</p>"
|
440 |
-
|
441 |
-
return format_result_card(title=product, content=content)
|
442 |
-
|
443 |
-
def get_formatted_css():
|
444 |
-
"""
|
445 |
-
Generate CSS for the UI based on current theme
|
446 |
-
|
447 |
-
Returns:
|
448 |
-
CSS string for styling the UI
|
449 |
-
"""
|
450 |
-
return f"""
|
451 |
-
.gradio-container .prose {{
|
452 |
-
max-width: 100%;
|
453 |
-
}}
|
454 |
-
#results-container {{
|
455 |
-
height: 600px !important;
|
456 |
-
overflow-y: auto !important;
|
457 |
-
overflow-x: hidden !important;
|
458 |
-
padding: 15px !important;
|
459 |
-
border: 1px solid {COLORS['card_border']} !important;
|
460 |
-
background-color: {COLORS['background']} !important;
|
461 |
-
color: {COLORS['text_primary']} !important;
|
462 |
-
}}
|
463 |
-
/* Style for method columns */
|
464 |
-
.methods-comparison {{
|
465 |
-
display: flex;
|
466 |
-
flex-wrap: wrap;
|
467 |
-
}}
|
468 |
-
.method-results {{
|
469 |
-
flex: 1;
|
470 |
-
min-width: 200px;
|
471 |
-
padding: 15px;
|
472 |
-
border-right: 1px solid {COLORS['card_border']};
|
473 |
-
}}
|
474 |
-
/* Make the product header more visible */
|
475 |
-
.product-header {{
|
476 |
-
background-color: {COLORS['header_bg']} !important;
|
477 |
-
padding: 12px 15px !important;
|
478 |
-
border-bottom: 1px solid {COLORS['card_border']} !important;
|
479 |
-
}}
|
480 |
-
.product-header h3 {{
|
481 |
-
margin: 0 !important;
|
482 |
-
font-size: 18px !important;
|
483 |
-
color: {COLORS['header_text']} !important;
|
484 |
-
background-color: transparent !important;
|
485 |
-
}}
|
486 |
-
/* Remove all nested scrollbars */
|
487 |
-
#results-container * {{
|
488 |
-
overflow: visible !important;
|
489 |
-
height: auto !important;
|
490 |
-
max-height: none !important;
|
491 |
-
}}
|
492 |
-
"""
|
493 |
-
|
494 |
-
def set_theme(theme_name):
|
495 |
-
"""
|
496 |
-
Update the global theme setting
|
497 |
-
|
498 |
-
Args:
|
499 |
-
theme_name: Theme name to set ("light" or "dark")
|
500 |
-
|
501 |
-
Returns:
|
502 |
-
Boolean indicating success
|
503 |
-
"""
|
504 |
-
global THEME, COLORS, STYLES
|
505 |
-
if theme_name in THEMES:
|
506 |
-
THEME = theme_name
|
507 |
-
COLORS = THEMES[THEME]
|
508 |
-
# Update styles with new colors
|
509 |
-
STYLES = {
|
510 |
-
"card": f"margin-bottom: 20px; border: 1px solid {COLORS['card_border']}; border-radius: 8px; overflow: hidden; background-color: {COLORS['card_bg']};",
|
511 |
-
"header": f"background-color: {COLORS['header_bg']}; padding: 12px 15px; border-bottom: 1px solid {COLORS['card_border']};",
|
512 |
-
"header_text": f"margin: 0; font-size: 18px; color: {COLORS['header_text']};",
|
513 |
-
"flex_container": "display: flex; flex-wrap: wrap;",
|
514 |
-
"method_container": f"flex: 1; width: 100%; padding: 15px; border-bottom: 1px solid {COLORS['card_border']};",
|
515 |
-
"method_title": f"margin-top: 0; color: {COLORS['text_primary']}; padding-bottom: 8px;",
|
516 |
-
"item_list": "list-style-type: none; padding-left: 0;",
|
517 |
-
"item": "margin-bottom: 8px; padding: 8px; border-radius: 4px;",
|
518 |
-
"empty_message": "color: #7f8c8d; font-style: italic;",
|
519 |
-
"info_panel": f"padding: 10px; background-color: {COLORS['section_bg']}; margin-bottom: 10px; border-radius: 4px;"
|
520 |
-
}
|
521 |
-
return True
|
522 |
-
return False
|
523 |
-
|
524 |
-
def format_categories_html(product, categories, chicory_result=None, header_color=None, explanation="", match_type="categories"):
|
525 |
-
"""
|
526 |
-
Format category matching results as HTML
|
527 |
-
|
528 |
-
Args:
|
529 |
-
product: Product name
|
530 |
-
categories: List of (category, score) tuples
|
531 |
-
chicory_result: Optional chicory parser result for the product
|
532 |
-
header_color: Optional header background color
|
533 |
-
explanation: Optional expanded description text
|
534 |
-
match_type: Either "ingredients" or "categories"
|
535 |
-
|
536 |
-
Returns:
|
537 |
-
HTML string
|
538 |
-
"""
|
539 |
-
content = ""
|
540 |
-
|
541 |
-
# Add expanded description if available
|
542 |
if explanation:
|
543 |
-
|
544 |
-
|
545 |
-
|
546 |
-
|
547 |
-
|
548 |
-
|
549 |
-
|
550 |
-
content += f"<div style='{STYLES['info_panel']}'>"
|
551 |
-
content += "<h4 style='margin-top: 0; border-bottom: 1px solid rgba(0,0,0,0.1); padding-bottom: 8px;'>Chicory Parser Results</h4>"
|
552 |
-
|
553 |
if isinstance(chicory_result, dict):
|
554 |
ingredient = chicory_result.get("ingredient", "Not found")
|
555 |
-
|
556 |
-
|
557 |
-
|
558 |
-
|
559 |
-
|
560 |
-
|
561 |
-
|
|
|
|
|
562 |
else:
|
563 |
-
|
564 |
-
|
565 |
-
content += "</div>"
|
566 |
-
|
567 |
-
# Add the category results
|
568 |
-
content += format_method_results(
|
569 |
-
method_key=match_type,
|
570 |
-
results=categories,
|
571 |
-
color_hex=header_color or METHOD_COLORS.get(match_type, "#1abc9c")
|
572 |
-
)
|
573 |
-
|
574 |
-
return format_result_card(title=product, content=content)
|
575 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
from typing import List, Dict, Tuple, Any
|
2 |
from utils import get_confidence_color, get_confidence_bg_color
|
3 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
4 |
# Method colors (consistent across themes)
|
5 |
METHOD_COLORS = {
|
6 |
"base": "#f39c12", # Orange
|
|
|
9 |
"openai": "#2ecc71", # Green
|
10 |
"expanded": "#e74c3c", # Red
|
11 |
"hybrid": "#1abc9c", # Turquoise
|
12 |
+
"categories": "#1abc9c", # Same as hybrid
|
13 |
+
"ingredients": "#f39c12" # Same as base
|
14 |
}
|
15 |
|
16 |
# Method display names
|
|
|
18 |
"base": "Base Embeddings",
|
19 |
"voyage": "Voyage AI Reranker",
|
20 |
"chicory": "Chicory Parser",
|
21 |
+
"openai": "OpenAI Reranker",
|
22 |
"expanded": "Expanded Description",
|
23 |
"hybrid": "Hybrid Matching",
|
24 |
"categories": "Category Matches",
|
25 |
"ingredients": "Ingredient Matches"
|
26 |
}
|
27 |
|
28 |
+
def parse_result_item(item):
|
29 |
+
"""Helper function to parse result items into display text and score"""
|
30 |
+
display_text = str(item) # Default
|
31 |
+
score = 0.0
|
32 |
+
# Handle tuple formats
|
33 |
+
if isinstance(item, tuple):
|
34 |
+
if len(item) == 2:
|
35 |
+
match, score = item
|
36 |
+
display_text = str(match)
|
37 |
+
elif len(item) == 3:
|
38 |
+
id_val, text, score = item
|
39 |
+
display_text = f"<strong>{id_val}</strong>: {text}" if text else str(id_val)
|
40 |
+
# Handle dictionary formats
|
41 |
+
elif isinstance(item, dict):
|
42 |
+
if "name" in item and "score" in item:
|
43 |
+
display_text = item["name"]
|
44 |
+
score = item["score"]
|
45 |
+
elif "category" in item and "confidence" in item:
|
46 |
+
display_text = item["category"]
|
47 |
+
score = item["confidence"]
|
48 |
+
elif "ingredient" in item and "relevance_score" in item:
|
49 |
+
display_text = item["ingredient"]
|
50 |
+
score = item["relevance_score"]
|
51 |
+
|
52 |
+
# Ensure score is a float
|
53 |
+
try:
|
54 |
+
score = float(score)
|
55 |
+
except (ValueError, TypeError):
|
56 |
+
score = 0.0
|
57 |
+
|
58 |
+
return display_text, score
|
59 |
+
|
60 |
+
def filter_results_by_threshold(results, confidence_threshold=0.0):
|
61 |
+
"""Helper function to filter results by confidence threshold"""
|
62 |
+
filtered_results = []
|
63 |
+
for item in results:
|
64 |
+
_, score = parse_result_item(item)
|
65 |
+
if score >= confidence_threshold:
|
66 |
+
filtered_results.append(item)
|
67 |
+
return filtered_results
|
68 |
+
|
69 |
+
def format_confidence_badge(score):
|
70 |
+
"""Formats the confidence score as a styled span (badge)"""
|
71 |
+
confidence_percent = int(score * 100)
|
72 |
+
confidence_color = get_confidence_color(score)
|
73 |
+
bg_color = get_confidence_bg_color(score)
|
74 |
+
# Determine text color based on background lightness for better contrast
|
75 |
+
# Simple heuristic: if bg_color is light (e.g., yellow, light green), use black text, otherwise use white/light text.
|
76 |
+
# This is approximate. A proper solution would involve calculating luminance.
|
77 |
+
light_bgs = ["#ffffcc", "#ccffcc", "#cceeff"] # Add more light hex codes if needed
|
78 |
+
text_color = "#000000" if bg_color.lower() in light_bgs else "var(--text-color)" # Default to theme text color otherwise
|
79 |
+
|
80 |
+
return (
|
81 |
+
f"<span style='background-color: {bg_color}; border: 1px solid {confidence_color}; color: {text_color}; "
|
82 |
+
f"font-weight: 600; padding: 3px 8px; border-radius: 5px; font-size: 0.9em; "
|
83 |
+
f"min-width: 80px; text-align: center; display: inline-block; margin-left: 10px;'>"
|
84 |
+
f"{confidence_percent}%</span>"
|
85 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
86 |
|
87 |
+
def format_result_list_html(results, confidence_threshold=0.0):
|
88 |
+
"""Formats a list of results (matches) into an HTML list."""
|
89 |
+
filtered_results = filter_results_by_threshold(results, confidence_threshold)
|
90 |
+
|
91 |
+
if not filtered_results:
|
92 |
+
return "<p style='color: grey; font-style: italic; margin: 10px 0;'>No matches found above confidence threshold.</p>"
|
93 |
+
|
94 |
+
html = "<ul style='list-style-type: none; padding-left: 0; margin-top: 10px;'>"
|
95 |
+
for item in filtered_results:
|
96 |
+
display_text, score = parse_result_item(item)
|
97 |
+
badge = format_confidence_badge(score)
|
98 |
+
# Reintroduce subtle background and border using theme variables
|
99 |
+
html += (
|
100 |
+
f"<li style='display: flex; justify-content: space-between; align-items: center; "
|
101 |
+
f"margin-bottom: 8px; padding: 8px; border-radius: 4px; "
|
102 |
+
f"background-color: var(--secondary-background-color); " # Use secondary background
|
103 |
+
f"border: 1px solid rgba(128, 128, 128, 0.2);'>" # Subtle grey border
|
104 |
+
f"<span style='flex-grow: 1; margin-right: 10px; word-wrap: break-word; color: var(--text-color);'>{display_text}</span>" # Ensure text uses theme color
|
105 |
+
f"{badge}"
|
106 |
+
f"</li>"
|
107 |
+
)
|
108 |
+
html += "</ul>"
|
109 |
return html
|
110 |
|
111 |
+
def format_result_card(title, content_html):
|
112 |
+
"""Creates a basic card structure for a single product result."""
|
113 |
+
# Use secondary background for the card, slightly stronger border
|
114 |
+
return (
|
115 |
+
f"<div style='margin-bottom: 20px; border: 1px solid rgba(128, 128, 128, 0.3); border-radius: 8px; padding: 15px; background-color: var(--secondary-background-color);'>"
|
116 |
+
f"<h3 style='margin-top: 0; margin-bottom: 15px; font-size: 1.1em; border-bottom: 1px solid rgba(128, 128, 128, 0.2); padding-bottom: 10px; color: var(--text-color);'>{title}</h3>" # Ensure header uses theme text color
|
117 |
+
f"{content_html}"
|
118 |
+
f"</div>"
|
119 |
+
)
|
120 |
+
|
121 |
+
def format_info_panel(title, text):
|
122 |
+
"""Formats an informational panel (e.g., for expanded description)."""
|
123 |
+
# Use a slightly different background, maybe derived from primary color with transparency
|
124 |
+
# Or stick to secondary background for consistency
|
125 |
+
return (
|
126 |
+
f"<div style='border-left: 4px solid var(--primary-color); padding: 10px 15px; margin-bottom: 15px; border-radius: 4px; background-color: var(--secondary-background-color);'>"
|
127 |
+
f"<h4 style='margin-top: 0; margin-bottom: 8px; font-size: 1em; color: var(--text-color);'>{title}</h4>" # Use theme text color
|
128 |
+
f"<p style='margin-bottom: 0; font-size: 0.95em; color: var(--text-color);'>{text}</p>" # Use theme text color
|
129 |
+
f"</div>"
|
130 |
+
)
|
131 |
+
|
132 |
+
def format_method_results_section(method_key, results, confidence_threshold=0.0):
|
133 |
+
"""Formats results for a specific method within a comparison."""
|
134 |
+
method_name = METHOD_NAMES.get(method_key, method_key.replace('_', ' ').title())
|
135 |
+
color_hex = METHOD_COLORS.get(method_key, "var(--text-color)") # Fallback to theme text color
|
136 |
+
results_html = format_result_list_html(results, confidence_threshold)
|
137 |
+
|
138 |
+
# Keep the method-specific color for the header border
|
139 |
+
return (
|
140 |
+
f"<div style='margin-bottom: 15px; padding-bottom: 15px; border-bottom: 1px dashed rgba(128, 128, 128, 0.2);'>" # Subtle dashed border between methods
|
141 |
+
f"<h4 style='margin-top: 0; margin-bottom: 10px; color: {color_hex}; border-bottom: 2px solid {color_hex}; padding-bottom: 5px; display: inline-block;'>{method_name}</h4>"
|
142 |
+
f"{results_html}"
|
143 |
+
f"</div>"
|
144 |
+
)
|
145 |
+
|
146 |
+
# --- Main Formatting Functions Called by UI Tabs ---
|
147 |
+
|
148 |
+
def format_comparison_html(product, method_results, expanded_description="", confidence_threshold=0.5):
|
149 |
+
"""Formats the comparison results for multiple methods for one product."""
|
150 |
+
content_html = ""
|
151 |
if expanded_description:
|
152 |
+
content_html += format_info_panel("Expanded Description", expanded_description)
|
153 |
+
|
154 |
+
method_order = ["base", "voyage", "chicory", "openai"]
|
155 |
+
sections_html = []
|
156 |
+
for method_key in method_order:
|
157 |
+
if method_key in method_results and method_results[method_key]:
|
158 |
+
sections_html.append(format_method_results_section(
|
159 |
+
method_key=method_key,
|
160 |
+
results=method_results.get(method_key, []),
|
161 |
+
confidence_threshold=confidence_threshold
|
162 |
+
))
|
163 |
+
|
164 |
+
# Join sections, remove last border if sections exist
|
165 |
+
if sections_html:
|
166 |
+
# Remove the border-bottom style from the last section's div
|
167 |
+
last_section = sections_html[-1]
|
168 |
+
if "border-bottom: 1px dashed rgba(128, 128, 128, 0.2);" in last_section:
|
169 |
+
sections_html[-1] = last_section.replace("border-bottom: 1px dashed rgba(128, 128, 128, 0.2);", "")
|
170 |
|
171 |
+
content_html += "".join(sections_html)
|
172 |
+
|
173 |
+
return format_result_card(title=product, content_html=content_html)
|
174 |
+
|
175 |
+
|
176 |
+
def format_reranking_results_html(results, match_type="ingredients", show_scores=True, include_explanation=False,
|
177 |
method="voyage", confidence_threshold=0.0):
|
178 |
+
"""Formats results from reranking methods (Voyage, OpenAI)."""
|
179 |
+
if not results:
|
180 |
+
return f"<p style='color: grey; font-style: italic;'>No {match_type.lower()} matches found.</p>"
|
181 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
182 |
html_elements = []
|
|
|
183 |
for result in results:
|
184 |
+
product_name = result.get("product_name", "Unknown Product")
|
185 |
matching_items = result.get("matching_items", [])
|
186 |
item_scores = result.get("item_scores", [])
|
187 |
explanation = result.get("explanation", "") if include_explanation else ""
|
188 |
+
|
|
|
|
|
|
|
|
|
189 |
if len(item_scores) != len(matching_items):
|
190 |
+
item_scores = [result.get("confidence", 0.0)] * len(matching_items)
|
191 |
+
|
192 |
+
formatted_matches = []
|
|
|
193 |
for i, item in enumerate(matching_items):
|
194 |
+
if ":" in str(item) and match_type == "categories":
|
195 |
+
parts = str(item).split(":", 1)
|
196 |
+
id_val = parts[0].strip()
|
197 |
+
text = parts[1].strip() if len(parts) > 1 else ""
|
198 |
+
formatted_matches.append((id_val, text, item_scores[i]))
|
|
|
|
|
199 |
else:
|
200 |
+
formatted_matches.append((str(item), item_scores[i]))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
201 |
|
202 |
+
content_html = ""
|
203 |
+
if explanation:
|
204 |
+
content_html += format_info_panel("Expanded Description", explanation)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
205 |
|
206 |
+
list_html = format_result_list_html(formatted_matches, confidence_threshold)
|
207 |
+
content_html += list_html
|
208 |
+
|
209 |
+
# Only add card if there's content to show (explanation or non-empty list html)
|
210 |
+
if explanation or "<li" in list_html: # Check if list contains items
|
211 |
+
html_elements.append(format_result_card(title=product_name, content_html=content_html))
|
212 |
+
|
213 |
+
return "".join(html_elements)
|
214 |
+
|
215 |
+
|
216 |
+
def format_categories_html(product, categories, chicory_result=None, explanation="", match_type="categories", confidence_threshold=0.0):
|
217 |
+
"""Formats category or ingredient matching results (non-reranked)."""
|
218 |
+
content_html = ""
|
219 |
+
has_content = False
|
|
|
|
|
|
|
220 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
221 |
if explanation:
|
222 |
+
content_html += format_info_panel("Expanded Description", explanation)
|
223 |
+
has_content = True
|
224 |
+
|
225 |
+
# Add Chicory results if available and applicable
|
226 |
+
if chicory_result and match_type == "ingredients":
|
227 |
+
chicory_title = METHOD_NAMES.get("chicory", "Chicory Parser")
|
228 |
+
content_html += f"<h4 style='margin-top: 15px; margin-bottom: 10px; color: {METHOD_COLORS.get('chicory', '#9b59b6')};'>{chicory_title}</h4>"
|
|
|
|
|
|
|
229 |
if isinstance(chicory_result, dict):
|
230 |
ingredient = chicory_result.get("ingredient", "Not found")
|
231 |
+
score = chicory_result.get("confidence", 0)
|
232 |
+
badge = format_confidence_badge(score)
|
233 |
+
content_html += (
|
234 |
+
f"<div style='display: flex; justify-content: space-between; align-items: center; padding: 8px; border-radius: 4px; margin-bottom: 15px; background-color: var(--secondary-background-color); border: 1px solid rgba(128, 128, 128, 0.2);'>" # Use theme bg/border
|
235 |
+
f"<span style='flex-grow: 1; margin-right: 10px; color: var(--text-color);'>{ingredient}</span>" # Use theme text color
|
236 |
+
f"{badge}"
|
237 |
+
f"</div>"
|
238 |
+
)
|
239 |
+
has_content = True
|
240 |
else:
|
241 |
+
content_html += "<p style='color: grey; font-style: italic;'>No Chicory results available.</p>"
|
242 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
243 |
|
244 |
+
# Add the main category/ingredient results
|
245 |
+
match_title = METHOD_NAMES.get(match_type, match_type.capitalize())
|
246 |
+
color_hex = METHOD_COLORS.get(match_type, "var(--text-color)") # Fallback to theme text color
|
247 |
+
content_html += f"<h4 style='margin-top: 15px; margin-bottom: 10px; color: {color_hex};'>{match_title}</h4>"
|
248 |
+
list_html = format_result_list_html(categories, confidence_threshold)
|
249 |
+
content_html += list_html
|
250 |
+
if "<li" in list_html: # Check if list contains items
|
251 |
+
has_content = True
|
252 |
+
|
253 |
+
# Only return card if there's content to show
|
254 |
+
if has_content:
|
255 |
+
return format_result_card(title=product, content_html=content_html)
|
256 |
+
else:
|
257 |
+
return "" # Return empty string if nothing to show
|
ui_hybrid_matching.py
CHANGED
@@ -3,7 +3,7 @@
|
|
3 |
from category_matching import load_categories, hybrid_category_matching
|
4 |
from similarity import hybrid_ingredient_matching, compute_similarities
|
5 |
from ui_core import embeddings, parse_input
|
6 |
-
from ui_formatters import
|
7 |
from openai_expansion import expand_product_descriptions
|
8 |
from api_utils import get_voyage_client
|
9 |
|
|
|
3 |
from category_matching import load_categories, hybrid_category_matching
|
4 |
from similarity import hybrid_ingredient_matching, compute_similarities
|
5 |
from ui_core import embeddings, parse_input
|
6 |
+
from ui_formatters import format_reranking_results_html
|
7 |
from openai_expansion import expand_product_descriptions
|
8 |
from api_utils import get_voyage_client
|
9 |
|
ui_ingredient_matching.py
CHANGED
@@ -4,7 +4,7 @@ from embeddings import create_product_embeddings
|
|
4 |
from similarity import compute_similarities
|
5 |
from chicory_api import call_chicory_parser
|
6 |
from ui_core import embeddings, parse_input
|
7 |
-
from ui_formatters import format_categories_html
|
8 |
from openai_expansion import expand_product_descriptions
|
9 |
|
10 |
def categorize_products(product_input, is_file=False, use_expansion=False, top_n=10, confidence_threshold=0.5):
|
@@ -82,5 +82,5 @@ def categorize_products(product_input, is_file=False, use_expansion=False, top_n
|
|
82 |
output_html = "<div style='color: #d32f2f; font-weight: bold; padding: 20px;'>No results found. Please check your input or try different products.</div>"
|
83 |
|
84 |
progress_tracker(1.0, desc="Done!")
|
85 |
-
return
|
86 |
|
|
|
4 |
from similarity import compute_similarities
|
5 |
from chicory_api import call_chicory_parser
|
6 |
from ui_core import embeddings, parse_input
|
7 |
+
from ui_formatters import format_categories_html
|
8 |
from openai_expansion import expand_product_descriptions
|
9 |
|
10 |
def categorize_products(product_input, is_file=False, use_expansion=False, top_n=10, confidence_threshold=0.5):
|
|
|
82 |
output_html = "<div style='color: #d32f2f; font-weight: bold; padding: 20px;'>No results found. Please check your input or try different products.</div>"
|
83 |
|
84 |
progress_tracker(1.0, desc="Done!")
|
85 |
+
return output_html # Return the generated HTML directly
|
86 |
|
utils.py
CHANGED
@@ -152,5 +152,4 @@ def get_confidence_text_color(score):
|
|
152 |
|
153 |
# Remove any UI formatting-specific functions that now exist in ui_formatters.py:
|
154 |
# - format_categories_html
|
155 |
-
# - create_results_container
|
156 |
# - Any other UI formatting functions
|
|
|
152 |
|
153 |
# Remove any UI formatting-specific functions that now exist in ui_formatters.py:
|
154 |
# - format_categories_html
|
|
|
155 |
# - Any other UI formatting functions
|