Lavender825 commited on
Commit
cddf904
·
1 Parent(s): 320d35a

Refine merchant interface workflow

Browse files
Files changed (1) hide show
  1. app.py +170 -20
app.py CHANGED
@@ -696,6 +696,29 @@ def merchant_product_scores(metric: str) -> List[List[Any]]:
696
  return rows or [["No rows", "Try Refresh", metric, "-", 0.0, 0.0, 0.0]]
697
 
698
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
699
  def merchant_score_filter(aspect: str, prediction: str, category: str) -> List[List[Any]]:
700
  rows = []
701
  for product in PRODUCTS:
@@ -823,6 +846,62 @@ def export_risk_rows(rows):
823
  )
824
 
825
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
826
  def external_review_predict(review: str, features: str, categories: str, price: Any, rating: Any, count: Any, selected_aspect: str) -> Tuple[str, str, List[List[Any]]]:
827
  try:
828
  result = _predict_custom(review, features, categories, price, rating, count)
@@ -862,6 +941,52 @@ def export_external_rows(rows):
862
  )
863
 
864
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
865
  def _status_html() -> str:
866
  missing = []
867
  if not CHECKPOINT_PATH.exists() or CHECKPOINT_PATH.stat().st_size < 1024 * 1024:
@@ -932,10 +1057,11 @@ mark { background:#fde68a; color:#111827; border-radius:4px; padding:1px 3px; }
932
  .info-tip { position:relative; display:inline-flex; align-items:center; justify-content:center; width:24px; height:24px; border-radius:999px; border:1px solid #bfdbfe; background:#eff6ff; color:#1e40af; font-weight:800; cursor:help; }
933
  .info-tip .tip-content { display:none; position:absolute; right:0; top:30px; z-index:20; width:420px; max-width:80vw; background:white; border:1px solid var(--line); border-radius:8px; padding:10px; box-shadow:0 12px 30px rgba(15,23,42,.16); }
934
  .info-tip:hover .tip-content { display:block; }
935
- .negative-grid { display:grid; grid-template-columns:repeat(3, minmax(0, 1fr)); gap:10px; margin:8px 0 14px; }
936
  .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; }
937
  .negative-card p { margin:5px 0 0; color:#334155; font-size:13px; }
938
  .mini-product-img { width:64px; height:76px; object-fit:cover; border-radius:6px; border:1px solid var(--line); background:#f8fafc; }
 
939
  @media (max-width:860px) { .aspect-grid, .metric-grid, .product-layout, .decision-layout, .negative-grid { grid-template-columns:1fr; } .product-img { width:100%; height:180px; } }
940
  """
941
 
@@ -981,6 +1107,7 @@ def build_app() -> gr.Blocks:
981
  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>')
982
  gr.HTML('<div class="small-label">Most negative products across the six aspects</div>')
983
  negative_spotlight = gr.HTML('<div class="note-card">Loading most negative products...</div>')
 
984
  with gr.Accordion("Score filters and export", open=True):
985
  with gr.Row():
986
  merchant_metric = gr.Dropdown(["Overall"] + ASPECTS, value="Overall", label="Aspect")
@@ -992,33 +1119,52 @@ def build_app() -> gr.Blocks:
992
  export_scores = gr.Button("Export Score List")
993
  merchant_scores_file = gr.File(label="Downloaded score CSV", interactive=False)
994
  with gr.Accordion("New Product Metadata Risk Screening", open=False):
 
995
  with gr.Row():
996
  with gr.Column(scale=1):
997
- new_import = gr.File(label="Import product metadata JSON/CSV")
998
- new_features = gr.Textbox("Cotton Polyester Blend, Slim Fit, Graphic Print, Machine Wash", label="New product features", lines=4)
999
- new_categories = gr.Textbox("Clothing > Women > Tops > T-Shirts", label="New product categories")
1000
- new_price = gr.Number(29.99, label="Price")
1001
- new_rating = gr.Number(4.1, label="Expected or early average rating")
1002
- new_count = gr.Number(35, label="Expected or early rating count")
1003
- new_focus = gr.Dropdown(["All"] + ASPECTS, value="All", label="Focus aspect")
1004
- screen_btn = gr.Button("Predict Metadata Risk", variant="primary")
 
 
 
 
 
 
 
 
1005
  with gr.Column(scale=1):
1006
  risk_summary = gr.HTML()
1007
  risk_table = gr.Dataframe(headers=["Aspect", "Risk Level", "Model Signal", "Confidence", "Reason"], value=[["Click Predict Metadata Risk", "", "", 0.0, ""]], datatype=["str", "str", "str", "number", "str"], interactive=False)
1008
  export_risk = gr.Button("Export Risk Result")
1009
  risk_file = gr.File(label="Downloaded risk CSV", interactive=False)
1010
  with gr.Accordion("External Review Prediction", open=False):
 
1011
  with gr.Row():
1012
  with gr.Column(scale=1):
1013
- ext_import = gr.File(label="Import review metadata JSON/CSV")
1014
- 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)
1015
- ext_features = gr.Textbox("Cotton Blend, Slim Fit, Zipper Closure", label="Product features", lines=3)
1016
- ext_categories = gr.Textbox("Clothing > Women > Jackets", label="Product categories")
1017
- ext_price = gr.Number(39.99, label="Price")
1018
- ext_rating = gr.Number(4.2, label="Average rating")
1019
- ext_count = gr.Number(312, label="Rating count")
1020
- ext_aspect = gr.Dropdown(["All"] + ASPECTS, value="All", label="Highlight aspect")
1021
- external_btn = gr.Button("Analyze External Review", variant="primary")
 
 
 
 
 
 
 
 
 
1022
  with gr.Column(scale=1):
1023
  ext_overall = gr.HTML()
1024
  ext_aspects = gr.HTML()
@@ -1030,11 +1176,15 @@ def build_app() -> gr.Blocks:
1030
  merchant_metric.change(merchant_score_filter, [merchant_metric, merchant_prediction, merchant_category], merchant_scores)
1031
  merchant_prediction.change(merchant_score_filter, [merchant_metric, merchant_prediction, merchant_category], merchant_scores)
1032
  merchant_category.change(merchant_score_filter, [merchant_metric, merchant_prediction, merchant_category], merchant_scores)
1033
- new_import.change(import_new_product_payload, new_import, [new_features, new_categories, new_price, new_rating, new_count, new_focus])
1034
  screen_btn.click(screen_new_product, [new_features, new_categories, new_price, new_rating, new_count, new_focus], [risk_summary, risk_table])
 
 
1035
  export_risk.click(export_risk_rows, risk_table, risk_file)
1036
- ext_import.change(import_external_review_payload, ext_import, [external_review, ext_features, ext_categories, ext_price, ext_rating, ext_count, ext_aspect])
1037
  external_btn.click(external_review_predict, [external_review, ext_features, ext_categories, ext_price, ext_rating, ext_count, ext_aspect], [ext_overall, ext_aspects, ext_table])
 
 
1038
  export_ext.click(export_external_rows, ext_table, ext_file)
1039
 
1040
  with gr.Tab("Research Metrics"):
 
696
  return rows or [["No rows", "Try Refresh", metric, "-", 0.0, 0.0, 0.0]]
697
 
698
 
699
+ def merchant_aspect_overview() -> List[List[Any]]:
700
+ rows = []
701
+ for aspect in ASPECTS:
702
+ neg_count, top_name, top_conf, top_reason = 0, "-", 0.0, "-"
703
+ for product in PRODUCTS:
704
+ try:
705
+ result = _predict_product(product["name"])
706
+ except Exception:
707
+ continue
708
+ d = result.get("aspect_details", {}).get(aspect, {})
709
+ if d.get("label") != "Negative":
710
+ continue
711
+ neg_count += 1
712
+ conf = _safe_float(d.get("confidence"))
713
+ if conf >= top_conf:
714
+ hits = _keyword_hits(product.get("review", ""), aspect)
715
+ top_name = product["name"]
716
+ top_conf = conf
717
+ top_reason = ", ".join(hits) if hits else _short_text(product.get("review") or product.get("features"), 90)
718
+ rows.append([aspect, neg_count, top_name, round(top_conf, 4), top_reason])
719
+ return rows
720
+
721
+
722
  def merchant_score_filter(aspect: str, prediction: str, category: str) -> List[List[Any]]:
723
  rows = []
724
  for product in PRODUCTS:
 
846
  )
847
 
848
 
849
+ def _read_uploaded_records(file_obj) -> List[Dict[str, Any]]:
850
+ if not file_obj:
851
+ return []
852
+ path = Path(getattr(file_obj, "name", file_obj))
853
+ try:
854
+ if path.suffix.lower() == ".json":
855
+ data = json.loads(path.read_text(encoding="utf-8"))
856
+ if isinstance(data, dict):
857
+ data = data.get("items") or data.get("data") or [data]
858
+ return data if isinstance(data, list) else []
859
+ with path.open("r", encoding="utf-8-sig", newline="") as fh:
860
+ return list(csv.DictReader(fh))
861
+ except Exception:
862
+ return []
863
+
864
+
865
+ def download_new_product_template():
866
+ rows = [[
867
+ "Cotton Polyester Blend, Slim Fit, Graphic Print, Machine Wash",
868
+ "Clothing > Women > Tops > T-Shirts",
869
+ 29.99,
870
+ 4.1,
871
+ 35,
872
+ "All",
873
+ ]]
874
+ return _write_csv_download(
875
+ "new_product_metadata_template.csv",
876
+ ["features", "categories", "price", "average_rating", "rating_number", "focus_aspect"],
877
+ rows,
878
+ )
879
+
880
+
881
+ def batch_screen_new_products(file_obj) -> Tuple[str, List[List[Any]]]:
882
+ records = _read_uploaded_records(file_obj)
883
+ if not records:
884
+ return '<div class="note-card">Upload a CSV or JSON file first.</div>', []
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)
892
+ focus = data.get("focus_aspect") or data.get("focus") or "All"
893
+ _, detail_rows = screen_new_product(features, categories, price, rating, count, focus)
894
+ high = [r[0] for r in detail_rows if r[1] == "High"]
895
+ rows.append([
896
+ f"Product {i}",
897
+ "High" if high else "Low",
898
+ focus,
899
+ len(high),
900
+ f"{_short_text(categories, 55)} | high-risk aspects: {', '.join(high) or 'None'}",
901
+ ])
902
+ return f'<div class="note-card"><b>Batch screening completed:</b> {len(rows)} products analyzed.</div>', rows
903
+
904
+
905
  def external_review_predict(review: str, features: str, categories: str, price: Any, rating: Any, count: Any, selected_aspect: str) -> Tuple[str, str, List[List[Any]]]:
906
  try:
907
  result = _predict_custom(review, features, categories, price, rating, count)
 
941
  )
942
 
943
 
944
+ 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
+ "Clothing > Women > Jackets",
949
+ 39.99,
950
+ 4.2,
951
+ 312,
952
+ "All",
953
+ ]]
954
+ return _write_csv_download(
955
+ "external_review_template.csv",
956
+ ["review", "features", "categories", "price", "average_rating", "rating_number", "highlight_aspect"],
957
+ rows,
958
+ )
959
+
960
+
961
+ def batch_external_review_predict(file_obj) -> Tuple[str, str, List[List[Any]]]:
962
+ records = _read_uploaded_records(file_obj)
963
+ if not records:
964
+ return '<div class="note-card">Upload a CSV or JSON file first.</div>', "", []
965
+ rows = []
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)
973
+ aspect = data.get("highlight_aspect") or data.get("aspect") or "All"
974
+ overall_html, _, detail_rows = external_review_predict(review, features, categories, price, rating, count, aspect)
975
+ negative = [r[0] for r in detail_rows if r[1] == "Negative"]
976
+ rows.append([
977
+ f"Review {i}",
978
+ f"Negative: {', '.join(negative)}" if negative else "No major negative",
979
+ len(negative),
980
+ f"{_short_text(review, 70)} | {_short_text(categories, 45)}",
981
+ ])
982
+ return f'<div class="note-card"><b>Batch review prediction completed:</b> {len(rows)} reviews analyzed.</div>', "", rows
983
+
984
+
985
+ def toggle_analysis_mode(mode: str):
986
+ single = mode.startswith("Single")
987
+ return gr.update(visible=single), gr.update(visible=not single)
988
+
989
+
990
  def _status_html() -> str:
991
  missing = []
992
  if not CHECKPOINT_PATH.exists() or CHECKPOINT_PATH.stat().st_size < 1024 * 1024:
 
1057
  .info-tip { position:relative; display:inline-flex; align-items:center; justify-content:center; width:24px; height:24px; border-radius:999px; border:1px solid #bfdbfe; background:#eff6ff; color:#1e40af; font-weight:800; cursor:help; }
1058
  .info-tip .tip-content { display:none; position:absolute; right:0; top:30px; z-index:20; width:420px; max-width:80vw; background:white; border:1px solid var(--line); border-radius:8px; padding:10px; box-shadow:0 12px 30px rgba(15,23,42,.16); }
1059
  .info-tip:hover .tip-content { display:block; }
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
  """
1067
 
 
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")
 
1119
  export_scores = gr.Button("Export Score List")
1120
  merchant_scores_file = gr.File(label="Downloaded score CSV", interactive=False)
1121
  with gr.Accordion("New Product Metadata Risk Screening", open=False):
1122
+ risk_mode = gr.Radio(["Single product analysis", "Batch import analysis"], value="Single product analysis", label="Analysis mode")
1123
  with gr.Row():
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.Textbox("Clothing > Women > Tops > T-Shirts", label="New product categories")
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")
1131
+ with gr.Row():
1132
+ new_count = gr.Number(35, label="Expected or early rating count")
1133
+ new_focus = gr.Dropdown(["All"] + ASPECTS, value="All", label="Focus aspect")
1134
+ screen_btn = gr.Button("Predict Metadata Risk", variant="primary")
1135
+ with gr.Group(visible=False) as risk_batch_group:
1136
+ new_import = gr.File(label="Import product metadata JSON/CSV")
1137
+ with gr.Row():
1138
+ new_template = gr.Button("Download Import Template")
1139
+ batch_screen_btn = gr.Button("Batch Analyze Metadata", variant="primary")
1140
+ new_template_file = gr.File(label="Template CSV", interactive=False)
1141
  with gr.Column(scale=1):
1142
  risk_summary = gr.HTML()
1143
  risk_table = gr.Dataframe(headers=["Aspect", "Risk Level", "Model Signal", "Confidence", "Reason"], value=[["Click Predict Metadata Risk", "", "", 0.0, ""]], datatype=["str", "str", "str", "number", "str"], interactive=False)
1144
  export_risk = gr.Button("Export Risk Result")
1145
  risk_file = gr.File(label="Downloaded risk CSV", interactive=False)
1146
  with gr.Accordion("External Review Prediction", open=False):
1147
+ ext_mode = gr.Radio(["Single product analysis", "Batch import analysis"], value="Single product analysis", label="Analysis mode")
1148
  with gr.Row():
1149
  with gr.Column(scale=1):
1150
+ with gr.Group(visible=True) as ext_single_group:
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.Textbox("Clothing > Women > Jackets", label="Product categories", lines=3)
1155
+ with gr.Row():
1156
+ ext_price = gr.Number(39.99, label="Price")
1157
+ ext_rating = gr.Number(4.2, label="Average rating")
1158
+ with gr.Row():
1159
+ ext_count = gr.Number(312, label="Rating count")
1160
+ ext_aspect = gr.Dropdown(["All"] + ASPECTS, value="All", label="Highlight aspect")
1161
+ external_btn = gr.Button("Analyze External Review", variant="primary")
1162
+ with gr.Group(visible=False) as ext_batch_group:
1163
+ ext_import = gr.File(label="Import review metadata JSON/CSV")
1164
+ with gr.Row():
1165
+ ext_template = gr.Button("Download Import Template")
1166
+ batch_external_btn = gr.Button("Batch Analyze Reviews", variant="primary")
1167
+ ext_template_file = gr.File(label="Template CSV", interactive=False)
1168
  with gr.Column(scale=1):
1169
  ext_overall = gr.HTML()
1170
  ext_aspects = gr.HTML()
 
1176
  merchant_metric.change(merchant_score_filter, [merchant_metric, merchant_prediction, merchant_category], merchant_scores)
1177
  merchant_prediction.change(merchant_score_filter, [merchant_metric, merchant_prediction, merchant_category], merchant_scores)
1178
  merchant_category.change(merchant_score_filter, [merchant_metric, merchant_prediction, merchant_category], merchant_scores)
1179
+ risk_mode.change(toggle_analysis_mode, risk_mode, [risk_single_group, risk_batch_group])
1180
  screen_btn.click(screen_new_product, [new_features, new_categories, new_price, new_rating, new_count, new_focus], [risk_summary, risk_table])
1181
+ new_template.click(download_new_product_template, None, new_template_file)
1182
+ batch_screen_btn.click(batch_screen_new_products, new_import, [risk_summary, risk_table])
1183
  export_risk.click(export_risk_rows, risk_table, risk_file)
1184
+ ext_mode.change(toggle_analysis_mode, ext_mode, [ext_single_group, ext_batch_group])
1185
  external_btn.click(external_review_predict, [external_review, ext_features, ext_categories, ext_price, ext_rating, ext_count, ext_aspect], [ext_overall, ext_aspects, ext_table])
1186
+ ext_template.click(download_external_review_template, None, ext_template_file)
1187
+ batch_external_btn.click(batch_external_review_predict, ext_import, [ext_overall, ext_aspects, ext_table])
1188
  export_ext.click(export_external_rows, ext_table, ext_file)
1189
 
1190
  with gr.Tab("Research Metrics"):