Spaces:
Sleeping
Sleeping
Commit ·
71dd534
1
Parent(s): cddf904
Polish merchant spotlight and category inputs
Browse files
app.py
CHANGED
|
@@ -322,6 +322,16 @@ def _tags_for_category(category: str) -> List[str]:
|
|
| 322 |
return sorted({tag for p in products for tag in p.get("tags", [])})
|
| 323 |
|
| 324 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 325 |
def update_product_choices(category: str):
|
| 326 |
names = _product_names_for_category(category)
|
| 327 |
return gr.update(choices=names, value=names[0] if names else None)
|
|
@@ -397,7 +407,7 @@ def _predict_product(product_name: str) -> Dict[str, Any]:
|
|
| 397 |
|
| 398 |
|
| 399 |
def _predict_custom(review: str, features: str, categories: str, price: Any, rating: Any, count: Any) -> Dict[str, Any]:
|
| 400 |
-
meta = {"features_text": features or "", "categories_text": categories
|
| 401 |
return _predictor().predict(review or "No review text provided.", meta)
|
| 402 |
|
| 403 |
|
|
@@ -749,6 +759,38 @@ def export_merchant_scores(rows):
|
|
| 749 |
|
| 750 |
|
| 751 |
def negative_product_spotlight(limit: int = 6) -> str:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 752 |
items = []
|
| 753 |
for product in PRODUCTS:
|
| 754 |
try:
|
|
@@ -865,7 +907,7 @@ def _read_uploaded_records(file_obj) -> List[Dict[str, Any]]:
|
|
| 865 |
def download_new_product_template():
|
| 866 |
rows = [[
|
| 867 |
"Cotton Polyester Blend, Slim Fit, Graphic Print, Machine Wash",
|
| 868 |
-
"
|
| 869 |
29.99,
|
| 870 |
4.1,
|
| 871 |
35,
|
|
@@ -873,7 +915,7 @@ def download_new_product_template():
|
|
| 873 |
]]
|
| 874 |
return _write_csv_download(
|
| 875 |
"new_product_metadata_template.csv",
|
| 876 |
-
["features", "
|
| 877 |
rows,
|
| 878 |
)
|
| 879 |
|
|
@@ -885,7 +927,7 @@ def batch_screen_new_products(file_obj) -> Tuple[str, List[List[Any]]]:
|
|
| 885 |
rows = []
|
| 886 |
for i, data in enumerate(records, 1):
|
| 887 |
features = data.get("features") or data.get("features_text") or ""
|
| 888 |
-
categories = data.get("categories") or data.get("categories_text") or ""
|
| 889 |
price = _safe_float(data.get("price"), 0.0)
|
| 890 |
rating = _safe_float(data.get("average_rating") or data.get("rating"), 4.0)
|
| 891 |
count = _safe_float(data.get("rating_number") or data.get("rating_count"), 0.0)
|
|
@@ -945,7 +987,7 @@ def download_external_review_template():
|
|
| 945 |
rows = [[
|
| 946 |
"The fabric is soft and the color looks good, but it runs small.",
|
| 947 |
"Cotton Blend, Slim Fit, Zipper Closure",
|
| 948 |
-
"
|
| 949 |
39.99,
|
| 950 |
4.2,
|
| 951 |
312,
|
|
@@ -953,7 +995,7 @@ def download_external_review_template():
|
|
| 953 |
]]
|
| 954 |
return _write_csv_download(
|
| 955 |
"external_review_template.csv",
|
| 956 |
-
["review", "features", "
|
| 957 |
rows,
|
| 958 |
)
|
| 959 |
|
|
@@ -966,7 +1008,7 @@ def batch_external_review_predict(file_obj) -> Tuple[str, str, List[List[Any]]]:
|
|
| 966 |
for i, data in enumerate(records, 1):
|
| 967 |
review = data.get("review") or data.get("review_text") or ""
|
| 968 |
features = data.get("features") or data.get("features_text") or ""
|
| 969 |
-
categories = data.get("categories") or data.get("categories_text") or ""
|
| 970 |
price = _safe_float(data.get("price"), 0.0)
|
| 971 |
rating = _safe_float(data.get("average_rating") or data.get("rating"), 4.0)
|
| 972 |
count = _safe_float(data.get("rating_number") or data.get("rating_count"), 0.0)
|
|
@@ -1060,7 +1102,14 @@ mark { background:#fde68a; color:#111827; border-radius:4px; padding:1px 3px; }
|
|
| 1060 |
.negative-grid { display:grid; grid-template-columns:repeat(6, minmax(0, 1fr)); gap:10px; margin:8px 0 14px; }
|
| 1061 |
.negative-card { display:grid; grid-template-columns:64px 1fr; gap:10px; border:1px solid #fecaca; border-left:4px solid #ef4444; border-radius:8px; padding:10px; background:#fffafa; }
|
| 1062 |
.negative-card p { margin:5px 0 0; color:#334155; font-size:13px; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1063 |
.mini-product-img { width:64px; height:76px; object-fit:cover; border-radius:6px; border:1px solid var(--line); background:#f8fafc; }
|
|
|
|
| 1064 |
@media (max-width:1100px) { .negative-grid { grid-template-columns:repeat(3, minmax(0, 1fr)); } }
|
| 1065 |
@media (max-width:860px) { .aspect-grid, .metric-grid, .product-layout, .decision-layout, .negative-grid { grid-template-columns:1fr; } .product-img { width:100%; height:180px; } }
|
| 1066 |
"""
|
|
@@ -1068,6 +1117,8 @@ mark { background:#fde68a; color:#111827; border-radius:4px; padding:1px 3px; }
|
|
| 1068 |
|
| 1069 |
def build_app() -> gr.Blocks:
|
| 1070 |
categories = ["All"] + sorted({p["category"] for p in PRODUCTS})
|
|
|
|
|
|
|
| 1071 |
tags = sorted({tag for p in PRODUCTS for tag in p.get("tags", [])})
|
| 1072 |
with gr.Blocks(css=CSS, title="Clothing Sentiment Analysis") as demo:
|
| 1073 |
gr.HTML('<div id="hero"><div class="eyebrow">BERT + Metadata Cross-Attention</div><h1>Clothing Review Sentiment Intelligence App</h1><p>Explore overall sentiment, six aspect-level opinions, metadata-driven risks, and updated 10W experiment reports in a compact customer decision-support prototype.</p><div class="hero-pills"><span>Consumer decision support</span><span>Merchant diagnostics</span><span>Research dashboard</span></div></div>')
|
|
@@ -1107,7 +1158,6 @@ def build_app() -> gr.Blocks:
|
|
| 1107 |
gr.HTML('<div class="module-head"><h3>Product Score Monitor</h3><span class="info-tip">i<span class="tip-content">' + model_info_html() + '</span></span></div>')
|
| 1108 |
gr.HTML('<div class="small-label">Most negative products across the six aspects</div>')
|
| 1109 |
negative_spotlight = gr.HTML('<div class="note-card">Loading most negative products...</div>')
|
| 1110 |
-
aspect_overview = gr.Dataframe(headers=["Aspect", "Negative Count", "Most Negative Product", "Confidence", "Reason"], value=merchant_aspect_overview(), datatype=["str", "number", "str", "number", "str"], interactive=False, label="Six-aspect negative overview")
|
| 1111 |
with gr.Accordion("Score filters and export", open=True):
|
| 1112 |
with gr.Row():
|
| 1113 |
merchant_metric = gr.Dropdown(["Overall"] + ASPECTS, value="Overall", label="Aspect")
|
|
@@ -1124,7 +1174,7 @@ def build_app() -> gr.Blocks:
|
|
| 1124 |
with gr.Column(scale=1):
|
| 1125 |
with gr.Group(visible=True) as risk_single_group:
|
| 1126 |
new_features = gr.Textbox("Cotton Polyester Blend, Slim Fit, Graphic Print, Machine Wash", label="New product features", lines=4)
|
| 1127 |
-
new_categories = gr.
|
| 1128 |
with gr.Row():
|
| 1129 |
new_price = gr.Number(29.99, label="Price")
|
| 1130 |
new_rating = gr.Number(4.1, label="Expected or early average rating")
|
|
@@ -1151,7 +1201,7 @@ def build_app() -> gr.Blocks:
|
|
| 1151 |
external_review = gr.Textbox("The fabric is soft and the color looks good, but it runs small and the zipper feels weak.", label="External customer review", lines=5)
|
| 1152 |
with gr.Row():
|
| 1153 |
ext_features = gr.Textbox("Cotton Blend, Slim Fit, Zipper Closure", label="Product features", lines=3)
|
| 1154 |
-
ext_categories = gr.
|
| 1155 |
with gr.Row():
|
| 1156 |
ext_price = gr.Number(39.99, label="Price")
|
| 1157 |
ext_rating = gr.Number(4.2, label="Average rating")
|
|
|
|
| 322 |
return sorted({tag for p in products for tag in p.get("tags", [])})
|
| 323 |
|
| 324 |
|
| 325 |
+
def _category_to_metadata_text(category: str) -> str:
|
| 326 |
+
category = str(category or "").strip()
|
| 327 |
+
if not category:
|
| 328 |
+
return "Clothing"
|
| 329 |
+
for product in PRODUCTS:
|
| 330 |
+
if product.get("category") == category and product.get("categories"):
|
| 331 |
+
return product["categories"]
|
| 332 |
+
return f"Clothing > {category}"
|
| 333 |
+
|
| 334 |
+
|
| 335 |
def update_product_choices(category: str):
|
| 336 |
names = _product_names_for_category(category)
|
| 337 |
return gr.update(choices=names, value=names[0] if names else None)
|
|
|
|
| 407 |
|
| 408 |
|
| 409 |
def _predict_custom(review: str, features: str, categories: str, price: Any, rating: Any, count: Any) -> Dict[str, Any]:
|
| 410 |
+
meta = {"features_text": features or "", "categories_text": _category_to_metadata_text(categories), "price": _safe_float(price), "average_rating": _safe_float(rating), "rating_number": _safe_float(count)}
|
| 411 |
return _predictor().predict(review or "No review text provided.", meta)
|
| 412 |
|
| 413 |
|
|
|
|
| 759 |
|
| 760 |
|
| 761 |
def negative_product_spotlight(limit: int = 6) -> str:
|
| 762 |
+
cards = []
|
| 763 |
+
for aspect in ASPECTS:
|
| 764 |
+
best = None
|
| 765 |
+
for product in PRODUCTS:
|
| 766 |
+
try:
|
| 767 |
+
result = _predict_product(product["name"])
|
| 768 |
+
except Exception:
|
| 769 |
+
continue
|
| 770 |
+
d = result.get("aspect_details", {}).get(aspect, {})
|
| 771 |
+
if d.get("label") != "Negative":
|
| 772 |
+
continue
|
| 773 |
+
conf = _safe_float(d.get("confidence"))
|
| 774 |
+
hits = _keyword_hits(product.get("review", ""), aspect)
|
| 775 |
+
reason = ", ".join(hits[:3]) if hits else _short_text(product.get("review") or product.get("features"), 48)
|
| 776 |
+
if best is None or conf > best[0]:
|
| 777 |
+
best = (conf, product, reason)
|
| 778 |
+
if best is None:
|
| 779 |
+
cards.append(
|
| 780 |
+
f'<div class="negative-card compact-negative"><h4>{aspect}</h4>'
|
| 781 |
+
'<div class="empty-negative">No strong negative sample</div></div>'
|
| 782 |
+
)
|
| 783 |
+
continue
|
| 784 |
+
conf, product, reason = best
|
| 785 |
+
img = f'<img class="mini-product-img" src="{_esc(_product_image_url(product))}" alt="{_esc(product["name"])}">'
|
| 786 |
+
cards.append(
|
| 787 |
+
f'<div class="negative-card compact-negative"><h4>{aspect}</h4>'
|
| 788 |
+
f'<div class="negative-body">{img}<div><b>{_esc(_short_text(product["name"], 42))}</b>'
|
| 789 |
+
f'<div class="small-label">{_esc(product["category"])} | conf {conf:.2f}</div>'
|
| 790 |
+
f'<p>{_esc(_short_text(reason, 56))}</p></div></div></div>'
|
| 791 |
+
)
|
| 792 |
+
return '<div class="negative-grid">' + "".join(cards[:limit]) + '</div>'
|
| 793 |
+
|
| 794 |
items = []
|
| 795 |
for product in PRODUCTS:
|
| 796 |
try:
|
|
|
|
| 907 |
def download_new_product_template():
|
| 908 |
rows = [[
|
| 909 |
"Cotton Polyester Blend, Slim Fit, Graphic Print, Machine Wash",
|
| 910 |
+
"T-Shirts",
|
| 911 |
29.99,
|
| 912 |
4.1,
|
| 913 |
35,
|
|
|
|
| 915 |
]]
|
| 916 |
return _write_csv_download(
|
| 917 |
"new_product_metadata_template.csv",
|
| 918 |
+
["features", "category", "price", "average_rating", "rating_number", "focus_aspect"],
|
| 919 |
rows,
|
| 920 |
)
|
| 921 |
|
|
|
|
| 927 |
rows = []
|
| 928 |
for i, data in enumerate(records, 1):
|
| 929 |
features = data.get("features") or data.get("features_text") or ""
|
| 930 |
+
categories = data.get("category") or data.get("categories") or data.get("categories_text") or ""
|
| 931 |
price = _safe_float(data.get("price"), 0.0)
|
| 932 |
rating = _safe_float(data.get("average_rating") or data.get("rating"), 4.0)
|
| 933 |
count = _safe_float(data.get("rating_number") or data.get("rating_count"), 0.0)
|
|
|
|
| 987 |
rows = [[
|
| 988 |
"The fabric is soft and the color looks good, but it runs small.",
|
| 989 |
"Cotton Blend, Slim Fit, Zipper Closure",
|
| 990 |
+
"Jackets",
|
| 991 |
39.99,
|
| 992 |
4.2,
|
| 993 |
312,
|
|
|
|
| 995 |
]]
|
| 996 |
return _write_csv_download(
|
| 997 |
"external_review_template.csv",
|
| 998 |
+
["review", "features", "category", "price", "average_rating", "rating_number", "highlight_aspect"],
|
| 999 |
rows,
|
| 1000 |
)
|
| 1001 |
|
|
|
|
| 1008 |
for i, data in enumerate(records, 1):
|
| 1009 |
review = data.get("review") or data.get("review_text") or ""
|
| 1010 |
features = data.get("features") or data.get("features_text") or ""
|
| 1011 |
+
categories = data.get("category") or data.get("categories") or data.get("categories_text") or ""
|
| 1012 |
price = _safe_float(data.get("price"), 0.0)
|
| 1013 |
rating = _safe_float(data.get("average_rating") or data.get("rating"), 4.0)
|
| 1014 |
count = _safe_float(data.get("rating_number") or data.get("rating_count"), 0.0)
|
|
|
|
| 1102 |
.negative-grid { display:grid; grid-template-columns:repeat(6, minmax(0, 1fr)); gap:10px; margin:8px 0 14px; }
|
| 1103 |
.negative-card { display:grid; grid-template-columns:64px 1fr; gap:10px; border:1px solid #fecaca; border-left:4px solid #ef4444; border-radius:8px; padding:10px; background:#fffafa; }
|
| 1104 |
.negative-card p { margin:5px 0 0; color:#334155; font-size:13px; }
|
| 1105 |
+
.compact-negative { display:block; min-height:178px; }
|
| 1106 |
+
.compact-negative h4 { margin:0 0 8px; color:#b91c1c; font-size:14px; letter-spacing:.02em; }
|
| 1107 |
+
.negative-body { display:grid; grid-template-columns:54px 1fr; gap:8px; align-items:start; }
|
| 1108 |
+
.negative-body b { display:block; font-size:13px; line-height:1.25; }
|
| 1109 |
+
.negative-body p { font-size:12px; line-height:1.35; }
|
| 1110 |
+
.empty-negative { color:#64748b; font-size:12px; border:1px dashed #fecaca; border-radius:8px; padding:14px 8px; background:white; }
|
| 1111 |
.mini-product-img { width:64px; height:76px; object-fit:cover; border-radius:6px; border:1px solid var(--line); background:#f8fafc; }
|
| 1112 |
+
.negative-body .mini-product-img { width:54px; height:64px; }
|
| 1113 |
@media (max-width:1100px) { .negative-grid { grid-template-columns:repeat(3, minmax(0, 1fr)); } }
|
| 1114 |
@media (max-width:860px) { .aspect-grid, .metric-grid, .product-layout, .decision-layout, .negative-grid { grid-template-columns:1fr; } .product-img { width:100%; height:180px; } }
|
| 1115 |
"""
|
|
|
|
| 1117 |
|
| 1118 |
def build_app() -> gr.Blocks:
|
| 1119 |
categories = ["All"] + sorted({p["category"] for p in PRODUCTS})
|
| 1120 |
+
merchant_categories = categories[1:] or ["Clothing"]
|
| 1121 |
+
default_merchant_category = merchant_categories[0]
|
| 1122 |
tags = sorted({tag for p in PRODUCTS for tag in p.get("tags", [])})
|
| 1123 |
with gr.Blocks(css=CSS, title="Clothing Sentiment Analysis") as demo:
|
| 1124 |
gr.HTML('<div id="hero"><div class="eyebrow">BERT + Metadata Cross-Attention</div><h1>Clothing Review Sentiment Intelligence App</h1><p>Explore overall sentiment, six aspect-level opinions, metadata-driven risks, and updated 10W experiment reports in a compact customer decision-support prototype.</p><div class="hero-pills"><span>Consumer decision support</span><span>Merchant diagnostics</span><span>Research dashboard</span></div></div>')
|
|
|
|
| 1158 |
gr.HTML('<div class="module-head"><h3>Product Score Monitor</h3><span class="info-tip">i<span class="tip-content">' + model_info_html() + '</span></span></div>')
|
| 1159 |
gr.HTML('<div class="small-label">Most negative products across the six aspects</div>')
|
| 1160 |
negative_spotlight = gr.HTML('<div class="note-card">Loading most negative products...</div>')
|
|
|
|
| 1161 |
with gr.Accordion("Score filters and export", open=True):
|
| 1162 |
with gr.Row():
|
| 1163 |
merchant_metric = gr.Dropdown(["Overall"] + ASPECTS, value="Overall", label="Aspect")
|
|
|
|
| 1174 |
with gr.Column(scale=1):
|
| 1175 |
with gr.Group(visible=True) as risk_single_group:
|
| 1176 |
new_features = gr.Textbox("Cotton Polyester Blend, Slim Fit, Graphic Print, Machine Wash", label="New product features", lines=4)
|
| 1177 |
+
new_categories = gr.Dropdown(merchant_categories, value=default_merchant_category, label="New product category")
|
| 1178 |
with gr.Row():
|
| 1179 |
new_price = gr.Number(29.99, label="Price")
|
| 1180 |
new_rating = gr.Number(4.1, label="Expected or early average rating")
|
|
|
|
| 1201 |
external_review = gr.Textbox("The fabric is soft and the color looks good, but it runs small and the zipper feels weak.", label="External customer review", lines=5)
|
| 1202 |
with gr.Row():
|
| 1203 |
ext_features = gr.Textbox("Cotton Blend, Slim Fit, Zipper Closure", label="Product features", lines=3)
|
| 1204 |
+
ext_categories = gr.Dropdown(merchant_categories, value=default_merchant_category, label="Product category")
|
| 1205 |
with gr.Row():
|
| 1206 |
ext_price = gr.Number(39.99, label="Price")
|
| 1207 |
ext_rating = gr.Number(4.2, label="Average rating")
|