| """UI Components - Build HTML cards and tables with evaluation results.""" |
| import pandas as pd |
|
|
|
|
| def build_product_cards(df, evaluations): |
| """ |
| Build HTML cards for products grouped by verdict. |
| |
| Args: |
| df: DataFrame with product data |
| evaluations: List of evaluation dicts (same length as df) |
| |
| Returns: |
| str: HTML string with product cards |
| """ |
| if df.empty: |
| return "<p>No products to display</p>" |
| |
| |
| df = df.copy() |
| df["verdict"] = [e.get("verdict", "possible") for e in evaluations] |
| df["notes"] = [e.get("notes", "") for e in evaluations] |
| df["requirements_met"] = [e.get("requirements_met", []) for e in evaluations] |
| |
| |
| has_requirements = any(e.get("requirements_met") for e in evaluations) |
| |
| |
| matches = df[df["verdict"] == "match"] |
| possibles = df[df["verdict"] == "possible"] |
| no_matches = df[df["verdict"] == "no_match"] |
| |
| html_parts = ["<div style='font-family: system-ui, sans-serif; background: #1a1a1a; padding: 1rem; border-radius: 0.5rem;'>"] |
| |
| |
| |
| all_products = pd.concat([matches, possibles, no_matches]) if not matches.empty or not possibles.empty or not no_matches.empty else pd.DataFrame() |
| |
| if not all_products.empty: |
| html_parts.append("<h3 style='color: #ffffff; margin-bottom: 1rem;'>Products ({})</h3>".format(len(all_products))) |
| html_parts.append("<div style='display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 1rem; margin-bottom: 2rem;'>") |
| for _, product in all_products.iterrows(): |
| html_parts.append(_build_card(product, "match", has_requirements)) |
| html_parts.append("</div>") |
| |
| html_parts.append("</div>") |
| |
| return "\n".join(html_parts) |
|
|
|
|
| def _build_card(product, verdict, has_requirements=True): |
| """Build a single product card.""" |
| |
| colors = { |
| "bg": "#2d2d2d", |
| "border": "#4a4a4a", |
| "text": "#ffffff", |
| "price": "#93c5fd" |
| } |
| |
| |
| icon = "" |
| |
| title = product.get("title", "Unknown") |
| price = product.get("price", 0) |
| source = product.get("source", "") |
| rating = product.get("rating", None) |
| reviews = product.get("reviews", 0) |
| link = product.get("link", "") |
| thumbnail = product.get("thumbnail", "") |
| notes = product.get("notes", "") |
| requirements_met = product.get("requirements_met", []) |
| |
| |
| req_html = "" |
| if has_requirements and requirements_met: |
| confirmed_features = [r for r in requirements_met if r.get("status") == "met"] |
| |
| if confirmed_features: |
| req_html = "<div style='margin-top: 0.5rem; font-size: 0.85rem;'>" |
| for req_item in confirmed_features: |
| req_text = req_item.get("req", "") |
| req_html += f"<div style='color: #4ade80;'>✓ {req_text}</div>" |
| req_html += "</div>" |
| |
| |
| thumbnail_html = "" |
| if thumbnail and link: |
| thumbnail_html = f'<a href="{link}" target="_blank"><img src="{thumbnail}" style="width: 100%; height: 200px; object-fit: contain; margin-bottom: 0.75rem; cursor: pointer; transition: opacity 0.2s;" onmouseover="this.style.opacity=\'0.8\'" onmouseout="this.style.opacity=\'1\'" /></a>' |
| elif thumbnail: |
| thumbnail_html = f'<img src="{thumbnail}" style="width: 100%; height: 200px; object-fit: contain; margin-bottom: 0.75rem;" />' |
| |
| card_html = f""" |
| <div style='border: 2px solid {colors["border"]}; border-radius: 0.5rem; padding: 1rem; background: {colors["bg"]};'> |
| {thumbnail_html} |
| <h4 style='margin: 0 0 0.5rem 0; font-size: 0.95rem; line-height: 1.3; color: {colors["text"]};'>{title}</h4> |
| <div style='display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem;'> |
| <span style='font-weight: bold; font-size: 1.25rem; color: {colors["price"]};'>${price:.2f}</span> |
| <span style='font-size: 0.85rem; color: #a0a0a0;'>{source}</span> |
| </div> |
| {f'<div style="margin-bottom: 0.5rem; font-size: 0.85rem; color: #d0d0d0;">⭐ {rating} ({reviews} reviews)</div>' if rating else ''} |
| {req_html} |
| </div> |
| """ |
| |
| return card_html |
|
|
|
|
| def build_comparison_table(df, evaluations): |
| """ |
| Build a comparison table with verdict column. |
| |
| Args: |
| df: DataFrame with product data |
| evaluations: List of evaluation dicts |
| |
| Returns: |
| pd.DataFrame: DataFrame ready for gr.Dataframe |
| """ |
| if df.empty: |
| return pd.DataFrame() |
| |
| display_df = df.copy() |
| |
| |
| display_df["Verdict"] = [e.get("verdict", "possible") for e in evaluations] |
| display_df["AI Notes"] = [e.get("notes", "") for e in evaluations] |
| |
| |
| columns_map = { |
| "title": "Title", |
| "price": "Price ($)", |
| "source": "Store", |
| "rating": "Rating", |
| "reviews": "Reviews", |
| "Verdict": "Verdict", |
| "AI Notes": "AI Notes" |
| } |
| |
| display_df = display_df[[c for c in columns_map.keys() if c in display_df.columns]] |
| display_df = display_df.rename(columns=columns_map) |
| |
| |
| verdict_order = {"match": 0, "possible": 1, "no_match": 2} |
| display_df["_sort"] = display_df["Verdict"].map(verdict_order) |
| display_df = display_df.sort_values("_sort").drop("_sort", axis=1) |
| |
| return display_df.reset_index(drop=True) |
|
|