esilver commited on
Commit
c15c118
·
1 Parent(s): 135e4ed

feat: Update UI components

Browse files
Files changed (7) hide show
  1. comparison.py +4 -5
  2. ui.py +205 -169
  3. ui_core.py +2 -5
  4. ui_formatters.py +211 -529
  5. ui_hybrid_matching.py +1 -1
  6. ui_ingredient_matching.py +2 -2
  7. 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, create_results_container
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
- output_html = create_results_container(
395
- result_elements,
396
- header_text=header_text
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
- # Handle button click *before* rendering the text area
48
- if st.button("Load Examples", key="ingredient_examples"):
49
- st.session_state.ingredient_input = load_examples() # Update state for next rerun
 
 
50
 
51
- # Input section - Use the session state value
52
- text_input = st.text_area(
53
- "Product Names (one per line)",
54
- value=st.session_state.ingredient_input, # Use value from state
55
- placeholder="Enter product names, one per line",
56
- height=250,
57
- key="ingredient_input_widget" # Use a different key for the widget itself if needed, or manage via value
58
- )
59
- # Update session state if user types manually
60
- st.session_state.ingredient_input = text_input
61
 
62
- use_expansion = st.checkbox(
63
- "Use Description Expansion (AI)",
64
- value=False,
65
- key="ingredient_expansion",
66
- help="Expand product descriptions using AI before matching"
67
- )
68
- top_n = st.slider("Top N Results", 1, 25, 10, step=1, key="ingredient_top_n")
69
- confidence = st.slider("Similarity Threshold", 0.1, 0.9, 0.5, step=0.05, key="ingredient_confidence")
 
 
70
 
71
- find_ingredients_btn = st.button("Find Similar Ingredients", type="primary", key="ingredient_find")
72
 
73
  with col2:
74
- # Results section
75
- st.subheader("Results")
76
- results_placeholder_ingredient = st.empty()
77
- if find_ingredients_btn:
78
- if st.session_state.ingredient_input: # Check state value
79
- results_html = categorize_products(
80
- st.session_state.ingredient_input,
81
- False,
82
- use_expansion,
83
- top_n,
84
- confidence
85
- )
86
- results_placeholder_ingredient.markdown(results_html, unsafe_allow_html=True)
 
 
 
 
87
  else:
88
- results_placeholder_ingredient.warning("Please enter product names.")
 
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
- if st.button("Load Examples", key="category_examples"):
96
- st.session_state.category_input = load_examples()
 
 
97
 
98
- category_text_input = st.text_area(
99
- "Product Names (one per line)",
100
- value=st.session_state.category_input,
101
- placeholder="Enter product names, one per line",
102
- height=250,
103
- key="category_input_widget"
104
- )
105
- st.session_state.category_input = category_text_input
106
 
107
- category_use_expansion = st.checkbox(
108
- "Use Description Expansion (AI)",
109
- value=False,
110
- key="category_expansion",
111
- help="Expand product descriptions using AI before matching"
112
- )
113
- category_top_n = st.slider("Top N Categories", 1, 10, 5, step=1, key="category_top_n")
114
- category_confidence = st.slider("Matching Threshold", 0.1, 0.9, 0.5, step=0.05, key="category_confidence")
 
 
115
 
116
- match_categories_btn = st.button("Match to Categories", type="primary", key="category_match")
117
 
118
  with col2:
119
- st.subheader("Results")
120
- results_placeholder_category = st.empty()
121
- if match_categories_btn:
122
- if st.session_state.category_input:
123
- results_html = categorize_products_by_category(
124
- st.session_state.category_input,
125
- False,
126
- category_use_expansion,
127
- category_top_n,
128
- category_confidence
129
- )
130
- results_placeholder_category.markdown(results_html, unsafe_allow_html=True)
 
 
 
 
131
  else:
132
- results_placeholder_category.warning("Please enter product names.")
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
- if st.button("Load Examples", key=f"{tab_key_prefix}_examples"):
141
- st.session_state[f"{tab_key_prefix}_input"] = load_examples()
 
 
142
 
143
- tab_input_value = st.text_area(
144
- "Product Names (one per line)",
145
- value=st.session_state[f"{tab_key_prefix}_input"],
146
- placeholder="Enter product names, one per line",
147
- height=250,
148
- key=f"{tab_key_prefix}_input_widget"
149
- )
150
- st.session_state[f"{tab_key_prefix}_input"] = tab_input_value # Update state
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- tab_match_btn = st.button(f"Match using {tab_name}", type="primary", key=f"{tab_key_prefix}_match")
171
 
172
  with col2:
173
- st.subheader("Results")
174
- results_placeholder_rerank = st.empty()
175
- if tab_match_btn:
176
- if st.session_state[f"{tab_key_prefix}_input"]:
177
- results_html = backend_function(
178
- st.session_state[f"{tab_key_prefix}_input"],
179
- False,
180
- tab_expansion,
181
- tab_emb_top_n,
182
- tab_top_n,
183
- tab_confidence,
184
- tab_match_type
185
- )
186
- results_placeholder_rerank.markdown(results_html, unsafe_allow_html=True)
 
 
 
 
187
  else:
188
- results_placeholder_rerank.warning("Please enter product names.")
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
- if st.button("Load Examples", key="compare_examples"):
200
- st.session_state.compare_input = load_examples()
 
 
201
 
202
- compare_product_input_value = st.text_area(
203
- "Product Names (one per line)",
204
- value=st.session_state.compare_input,
205
- placeholder="4 Tbsp sweet pickle relish\nchocolate chips\nfresh parsley",
206
- height=200,
207
- key="compare_input_widget"
208
- )
209
- st.session_state.compare_input = compare_product_input_value # Update state
210
 
211
- compare_embedding_top_n = st.slider(
212
- "Initial embedding candidates",
213
- min_value=5, max_value=50, value=20, step=5,
214
- key="compare_emb_top_n"
215
- )
216
- compare_final_top_n = st.slider(
217
- "Final results per method",
218
- min_value=1, max_value=10, value=3, step=1,
219
- key="compare_final_top_n"
220
- )
221
- compare_confidence_threshold = st.slider(
222
- "Confidence threshold",
223
- min_value=0.0, max_value=1.0, value=0.5, step=0.05,
224
- key="compare_confidence"
225
- )
226
- compare_match_type = st.radio(
227
- "Match Type",
228
- options=["categories", "ingredients"],
229
- index=0,
230
- key="compare_match_type",
231
- horizontal=True,
232
- help="Choose whether to match against ingredients or categories"
233
- )
234
- compare_expansion = st.checkbox(
235
- "Use Description Expansion (AI)",
236
- value=False,
237
- key="compare_expansion",
238
- help="Expand product descriptions using AI before matching"
239
- )
 
 
 
 
240
 
241
- compare_btn = st.button("Compare Methods", type="primary", key="compare_run")
242
 
243
  with col2:
244
- st.subheader("Comparison Results")
245
- results_placeholder_compare = st.empty()
246
- if compare_btn:
247
- if st.session_state.compare_input:
248
- results_html = compare_ingredient_methods_ui(
249
- st.session_state.compare_input,
250
- compare_embedding_top_n,
251
- compare_final_top_n,
252
- compare_confidence_threshold,
253
- compare_match_type,
254
- compare_expansion
255
- )
256
- results_placeholder_compare.markdown(results_html, unsafe_allow_html=True)
 
 
 
 
257
  else:
258
- results_placeholder_compare.warning("Please enter product names.")
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
- from ui_formatters import get_formatted_css, THEME, set_theme
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 format_method_results(method_key, results, color_hex=None):
72
- """
73
- Format results for a single method section
74
-
75
- Args:
76
- method_key: Key identifying the method (base, voyage, etc.)
77
- results: List of (name, score) tuples or format-specific data structure
78
- color_hex: Optional color override (otherwise uses METHOD_COLORS)
79
-
80
- Returns:
81
- HTML string for the method section
82
- """
83
- # Get color from METHOD_COLORS if not provided
84
- if color_hex is None:
85
- color_hex = METHOD_COLORS.get(method_key, "#777777")
86
-
87
- # Get method name from METHOD_NAMES or use the key with capitalization
88
- method_name = METHOD_NAMES.get(method_key, method_key.replace('_', ' ').title())
89
-
90
- html = f"<div class='method-results' style='{STYLES['method_container']}'>"
91
- html += f"<h4 style='{STYLES['method_title']}; border-bottom: 2px solid {color_hex};'>{method_name}</h4>"
92
-
93
- if results:
94
- html += f"<ul style='{STYLES['item_list']}'>"
95
-
96
- # Handle different result formats
97
- for item in results:
98
- # Handle tuple with 2 elements (name, score)
99
- if isinstance(item, tuple) and len(item) == 2:
100
- name, score = item
101
- # Handle tuple with 3 elements (common in category results)
102
- elif isinstance(item, tuple) and len(item) == 3:
103
- id_val, text, score = item
104
- name = f"<strong>{id_val}</strong>: {text}" if text else id_val
105
- # Handle dictionary format
106
- elif isinstance(item, dict) and "name" in item and "score" in item:
107
- name = item["name"]
108
- score = item["score"]
109
- # Handle dictionary format with different keys
110
- elif isinstance(item, dict) and "category" in item and "confidence" in item:
111
- name = item["category"]
112
- score = item["confidence"]
113
- # Handle dictionary format for ingredients
114
- elif isinstance(item, dict) and "ingredient" in item and "relevance_score" in item:
115
- name = item["ingredient"]
116
- score = item["relevance_score"]
117
- # Default case - just convert to string
118
- else:
119
- name = str(item)
120
- score = 0.0
121
-
122
- # Ensure score is a float
123
- try:
124
- score = float(score)
125
- except (ValueError, TypeError):
126
- score = 0.0
127
-
128
- confidence_percent = int(score * 100)
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 format_result_card(title, content, header_bg_color=None):
146
- """
147
- Create a styled card with a header and content
148
-
149
- Args:
150
- title: Card title
151
- content: HTML content for the card body
152
- header_bg_color: Optional header background color
153
-
154
- Returns:
155
- HTML string for the card
156
- """
157
- if header_bg_color is None:
158
- header_bg_color = COLORS['header_bg'] # Default header background color
159
-
160
- html = f"<div class='result-card' style='{STYLES['card']}'>"
161
- html += f"<div class='card-header' style='{STYLES['header']}; background-color: {header_bg_color};'>"
162
- html += f"<h3 style='{STYLES['header_text']}'>{title}</h3>"
163
- html += "</div>"
164
- html += f"<div class='card-content'>{content}</div>"
165
- html += "</div>"
 
166
  return html
167
 
168
- def format_comparison_html(product, method_results, expanded_description=""):
169
- """
170
- Format the comparison results as HTML
171
-
172
- Args:
173
- product: Product name
174
- method_results: Dictionary with results from different methods
175
- expanded_description: Optional expanded product description
176
-
177
- Returns:
178
- HTML string
179
- """
180
- # Create the methods comparison content with column direction
181
- methods_html = f"<div class='methods-comparison' style='{STYLES['flex_container']}; flex-direction: column;'>"
182
-
183
- # Add expanded description if available
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
  if expanded_description:
185
- methods_html += f"<div style='{STYLES['info_panel']}'>"
186
- methods_html += "<h4 style='margin-top: 0; border-bottom: 1px solid rgba(0,0,0,0.1); padding-bottom: 8px;'>Expanded Description</h4>"
187
- methods_html += f"<p style='margin-bottom: 8px;'>{expanded_description}</p>"
188
- methods_html += "</div>"
189
-
190
- # Add results for each method
191
- for method_key in ["base", "voyage", "chicory", "openai"]:
192
- methods_html += format_method_results(
193
- method_key=method_key,
194
- results=method_results.get(method_key, [])
195
- )
196
-
197
- methods_html += "</div>"
198
-
199
- # Create the full card with the methods content
200
- return format_result_card(title=product, content=methods_html)
 
 
201
 
202
- def format_reranking_results_html(results, match_type="ingredients", show_scores=True, include_explanation=False,
 
 
 
 
 
203
  method="voyage", confidence_threshold=0.0):
204
- """
205
- Unified formatter that works for both Voyage and OpenAI results, using the individual elements approach
206
- with the original visual style.
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
- # If scores are missing, use overall confidence for all
244
- result_confidence = result.get("confidence", 0.5)
245
- item_scores = [result_confidence] * len(matching_items)
246
-
247
  for i, item in enumerate(matching_items):
248
- score = item_scores[i]
249
- if ":" in item and match_type == "categories":
250
- # Handle category format "id: description"
251
- parts = item.split(":", 1)
252
- cat_id = parts[0].strip()
253
- cat_text = parts[1].strip() if len(parts) > 1 else ""
254
- formatted_matches.append((cat_id, cat_text, score))
255
  else:
256
- # Handle ingredient format (just name and score)
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
- def filter_results_by_threshold(results, confidence_threshold=0.0):
311
- """Helper function to filter results by confidence threshold"""
312
- filtered_results = []
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
- def parse_result_item(item):
322
- """Helper function to parse result items into display text and score"""
323
- # Handle both 2-value (match, score) and 3-value (id, text, score) tuples
324
- if isinstance(item, tuple):
325
- if len(item) == 2:
326
- match, score = item
327
- display_text = match
328
- elif len(item) == 3:
329
- cat_id, cat_text, score = item
330
- display_text = f"{cat_id}: {cat_text}" if cat_text else cat_id
331
- else:
332
- display_text = str(item)
333
- score = 0.0
334
- else:
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
- content += f"<div style='{STYLES['info_panel']}'>"
544
- content += "<h4 style='margin-top: 0; border-bottom: 1px solid rgba(0,0,0,0.1); padding-bottom: 8px;'>Expanded Description</h4>"
545
- content += f"<p style='margin-bottom: 8px;'>{explanation}</p>"
546
- content += "</div>"
547
-
548
- # Add Chicory results if available
549
- if chicory_result:
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
- confidence = chicory_result.get("confidence", 0)
556
- confidence_percent = int(confidence * 100)
557
-
558
- content += f"<div style='display: flex; justify-content: space-between; align-items: center; padding: 8px; border-radius: 4px;'>"
559
- content += f"<span style='font-weight: bold;'>{ingredient}</span>"
560
- 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>"
561
- content += "</div>"
 
 
562
  else:
563
- content += f"<p style='{STYLES['empty_message']}'>No Chicory results available</p>"
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 format_hybrid_results_html, create_results_container, format_reranking_results_html
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, create_results_container
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 create_results_container(output_html)
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