Lavender825 commited on
Commit
5cee72e
·
1 Parent(s): 7dfbd5c

Redesign merchant tools and exports

Browse files
Files changed (1) hide show
  1. app.py +191 -35
app.py CHANGED
@@ -8,6 +8,7 @@ import re
8
  import traceback
9
  from functools import lru_cache
10
  from pathlib import Path
 
11
  from typing import Any, Dict, List, Tuple
12
  from urllib.parse import quote
13
 
@@ -267,6 +268,15 @@ def _report_image(name: str):
267
  return str(path) if path.exists() else None
268
 
269
 
 
 
 
 
 
 
 
 
 
270
  @lru_cache(maxsize=1)
271
  def _predictor() -> AspectPredictor:
272
  return AspectPredictor(checkpoint_dir=CHECKPOINT_DIR)
@@ -662,6 +672,63 @@ def merchant_product_scores(metric: str) -> List[List[Any]]:
662
  return rows or [["No rows", "Try Refresh", metric, "-", 0.0, 0.0, 0.0]]
663
 
664
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
665
  def _metadata_risks(features: str, categories: str, price: Any, rating: Any, count: Any) -> Dict[str, str]:
666
  text = f"{features} {categories}".lower()
667
  risks = {}
@@ -702,6 +769,36 @@ def screen_new_product(features: str, categories: str, price: Any, rating: Any,
702
  return f'<div class="note-card"><b>New product risk focus:</b> {_esc(summary)}<br><span class="muted">This is a metadata screening tool, not a replacement for real review evaluation.</span></div>', rows
703
 
704
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
705
  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]]]:
706
  try:
707
  result = _predict_custom(review, features, categories, price, rating, count)
@@ -710,6 +807,37 @@ def external_review_predict(review: str, features: str, categories: str, price:
710
  return _overall_html(result), _aspect_cards(result, review or "", selected_aspect), _aspect_rows(result, review or "")
711
 
712
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
713
  def _status_html() -> str:
714
  missing = []
715
  if not CHECKPOINT_PATH.exists() or CHECKPOINT_PATH.stat().st_size < 1024 * 1024:
@@ -776,7 +904,15 @@ mark { background:#fde68a; color:#111827; border-radius:4px; padding:1px 3px; }
776
  .kv-table td { border-bottom:1px solid #e2e8f0; padding:9px 12px; }
777
  .kv-table td:first-child { width:220px; color:#334155; font-weight:700; background:#f8fafc; }
778
  .compact-note { color:#475569; font-size:13px; margin:4px 0 10px; }
779
- @media (max-width:860px) { .aspect-grid, .metric-grid, .product-layout, .decision-layout { grid-template-columns:1fr; } .product-img { width:100%; height:180px; } }
 
 
 
 
 
 
 
 
780
  """
781
 
782
 
@@ -818,45 +954,64 @@ def build_app() -> gr.Blocks:
818
  filter_btn.click(filter_products, [filter_aspect, filter_sentiment, filter_category, filter_tags, min_rating], [filter_table, filter_summary])
819
 
820
  with gr.Tab("Merchant Interface"):
821
- with gr.Row():
822
- with gr.Column(scale=4):
823
- gr.Markdown("### Model Basic Information")
824
- gr.HTML(model_info_html())
825
- with gr.Column(scale=6):
826
- gr.Markdown("### Product Score Monitor")
827
- merchant_metric = gr.Dropdown(["Overall"] + ASPECTS, value="Overall", label="Choose overall or aspect")
828
- merchant_scores = gr.Dataframe(headers=["Product", "Category", "Metric", "Prediction", "Confidence", "Price", "Rating"], value=[["Click Refresh Product Scores", "", "", "", 0.0, 0.0, 0.0]], datatype=["str", "str", "str", "str", "number", "number", "number"], interactive=False)
829
- refresh_scores = gr.Button("Refresh Product Scores", variant="primary")
830
- with gr.Accordion("New Product Metadata Risk Screening", open=False):
831
  with gr.Row():
832
- new_features = gr.Textbox("Cotton Polyester Blend, Slim Fit, Graphic Print, Machine Wash", label="New product features")
833
- new_categories = gr.Textbox("Clothing > Women > Tops > T-Shirts", label="New product categories")
 
 
834
  with gr.Row():
835
- new_price = gr.Number(29.99, label="Price")
836
- new_rating = gr.Number(4.1, label="Expected or early average rating")
837
- new_count = gr.Number(35, label="Expected or early rating count")
838
- new_focus = gr.Dropdown(["All"] + ASPECTS, value="All", label="Focus aspect")
839
- screen_btn = gr.Button("Predict Metadata Risk", variant="primary")
840
- risk_summary = gr.HTML()
841
- 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)
842
- with gr.Accordion("External Review Prediction", open=False):
843
- 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=4)
844
  with gr.Row():
845
- ext_features = gr.Textbox("Cotton Blend, Slim Fit, Zipper Closure", label="Product features")
846
- ext_categories = gr.Textbox("Clothing > Women > Jackets", label="Product categories")
 
 
 
 
 
 
 
 
 
 
 
 
 
847
  with gr.Row():
848
- ext_price = gr.Number(39.99, label="Price")
849
- ext_rating = gr.Number(4.2, label="Average rating")
850
- ext_count = gr.Number(312, label="Rating count")
851
- ext_aspect = gr.Dropdown(["All"] + ASPECTS, value="All", label="Highlight aspect")
852
- external_btn = gr.Button("Analyze External Review", variant="primary")
853
- ext_overall = gr.HTML()
854
- ext_aspects = gr.HTML()
855
- ext_table = gr.Dataframe(headers=["Aspect", "Prediction", "Confidence", "Key review evidence"], value=[["Click Analyze External Review", "", 0.0, ""]], datatype=["str", "str", "number", "str"], interactive=False)
856
- refresh_scores.click(merchant_product_scores, merchant_metric, merchant_scores)
857
- merchant_metric.change(merchant_product_scores, merchant_metric, merchant_scores)
 
 
 
 
 
 
 
 
 
 
 
 
858
  screen_btn.click(screen_new_product, [new_features, new_categories, new_price, new_rating, new_count, new_focus], [risk_summary, risk_table])
 
 
859
  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])
 
860
 
861
  with gr.Tab("Research Metrics"):
862
  gr.Markdown("Metrics are loaded from the updated 10W experiment reports. Tables use CSV outputs when available, with JSON fallback.")
@@ -879,7 +1034,8 @@ def build_app() -> gr.Blocks:
879
  refresh_research = gr.Button("Refresh Research Metrics", variant="primary")
880
  refresh_research.click(refresh_research_outputs, outputs=[research_cards, overall_table, aspect_table, ablation_table, meta_source_table])
881
  demo.load(consumer_product_view, [product_select, consumer_aspect], [product_detail, aspect_html, evidence_html, consumer_table])
882
- demo.load(merchant_product_scores, merchant_metric, merchant_scores)
 
883
  demo.load(filter_products, [filter_aspect, filter_sentiment, filter_category, filter_tags, min_rating], [filter_table, filter_summary])
884
  return demo
885
 
 
8
  import traceback
9
  from functools import lru_cache
10
  from pathlib import Path
11
+ import tempfile
12
  from typing import Any, Dict, List, Tuple
13
  from urllib.parse import quote
14
 
 
268
  return str(path) if path.exists() else None
269
 
270
 
271
+ def _write_csv_download(name: str, headers: List[str], rows: List[List[Any]]):
272
+ path = Path(tempfile.gettempdir()) / name
273
+ with path.open("w", encoding="utf-8-sig", newline="") as fh:
274
+ writer = csv.writer(fh)
275
+ writer.writerow(headers)
276
+ writer.writerows(rows or [])
277
+ return str(path)
278
+
279
+
280
  @lru_cache(maxsize=1)
281
  def _predictor() -> AspectPredictor:
282
  return AspectPredictor(checkpoint_dir=CHECKPOINT_DIR)
 
672
  return rows or [["No rows", "Try Refresh", metric, "-", 0.0, 0.0, 0.0]]
673
 
674
 
675
+ def merchant_score_filter(aspect: str, prediction: str, category: str) -> List[List[Any]]:
676
+ rows = []
677
+ for product in PRODUCTS:
678
+ if category != "All" and product.get("category") != category:
679
+ continue
680
+ try:
681
+ result = _predict_product(product["name"])
682
+ except Exception:
683
+ continue
684
+ d = result.get("overall", {}) if aspect == "Overall" else result.get("aspect_details", {}).get(aspect, {})
685
+ label = d.get("label", "Unknown")
686
+ if prediction != "Any" and label != prediction:
687
+ continue
688
+ rows.append([
689
+ product["name"], product["category"], aspect, label,
690
+ round(_safe_float(d.get("confidence")), 4),
691
+ product["price"], product["average_rating"],
692
+ ])
693
+ return rows or [["No matching products", category, aspect, prediction, 0.0, 0.0, 0.0]]
694
+
695
+
696
+ def export_merchant_scores(rows):
697
+ return _write_csv_download(
698
+ "merchant_product_scores.csv",
699
+ ["Product", "Category", "Metric", "Prediction", "Confidence", "Price", "Rating"],
700
+ rows,
701
+ )
702
+
703
+
704
+ def negative_product_spotlight(limit: int = 6) -> str:
705
+ items = []
706
+ for product in PRODUCTS:
707
+ try:
708
+ result = _predict_product(product["name"])
709
+ except Exception:
710
+ continue
711
+ for aspect in ASPECTS:
712
+ d = result.get("aspect_details", {}).get(aspect, {})
713
+ if d.get("label") != "Negative":
714
+ continue
715
+ hits = _keyword_hits(product.get("review", ""), aspect)
716
+ reason = ", ".join(hits) if hits else _short_text(product.get("review") or product.get("features"), 90)
717
+ items.append((_safe_float(d.get("confidence")), aspect, product, reason))
718
+ items.sort(key=lambda x: x[0], reverse=True)
719
+ cards = []
720
+ for conf, aspect, product, reason in items[:limit]:
721
+ img = f'<img class="mini-product-img" src="{_esc(_product_image_url(product))}" alt="{_esc(product["name"])}">'
722
+ cards.append(
723
+ f'<div class="negative-card">{img}<div><b>{_esc(product["name"])}</b>'
724
+ f'<div class="small-label">{_esc(product["category"])} · {aspect} Negative · conf {conf:.2f}</div>'
725
+ f'<p>{_esc(reason)}</p></div></div>'
726
+ )
727
+ if not cards:
728
+ return '<div class="note-card">No highly negative product found in the current catalog sample.</div>'
729
+ return '<div class="negative-grid">' + "".join(cards) + '</div>'
730
+
731
+
732
  def _metadata_risks(features: str, categories: str, price: Any, rating: Any, count: Any) -> Dict[str, str]:
733
  text = f"{features} {categories}".lower()
734
  risks = {}
 
769
  return f'<div class="note-card"><b>New product risk focus:</b> {_esc(summary)}<br><span class="muted">This is a metadata screening tool, not a replacement for real review evaluation.</span></div>', rows
770
 
771
 
772
+ def import_new_product_payload(file_obj):
773
+ if not file_obj:
774
+ return gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update()
775
+ path = Path(getattr(file_obj, "name", file_obj))
776
+ try:
777
+ if path.suffix.lower() == ".json":
778
+ data = json.loads(path.read_text(encoding="utf-8"))
779
+ else:
780
+ with path.open("r", encoding="utf-8-sig", newline="") as fh:
781
+ data = next(csv.DictReader(fh), {})
782
+ except Exception:
783
+ data = {}
784
+ return (
785
+ data.get("features") or data.get("features_text") or "",
786
+ data.get("categories") or data.get("categories_text") or "",
787
+ _safe_float(data.get("price"), 0.0),
788
+ _safe_float(data.get("average_rating") or data.get("rating"), 4.0),
789
+ _safe_float(data.get("rating_number") or data.get("rating_count"), 0.0),
790
+ data.get("focus_aspect") or data.get("focus") or "All",
791
+ )
792
+
793
+
794
+ def export_risk_rows(rows):
795
+ return _write_csv_download(
796
+ "new_product_metadata_risk.csv",
797
+ ["Aspect", "Risk Level", "Model Signal", "Confidence", "Reason"],
798
+ rows,
799
+ )
800
+
801
+
802
  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]]]:
803
  try:
804
  result = _predict_custom(review, features, categories, price, rating, count)
 
807
  return _overall_html(result), _aspect_cards(result, review or "", selected_aspect), _aspect_rows(result, review or "")
808
 
809
 
810
+ def import_external_review_payload(file_obj):
811
+ if not file_obj:
812
+ return gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update()
813
+ path = Path(getattr(file_obj, "name", file_obj))
814
+ try:
815
+ if path.suffix.lower() == ".json":
816
+ data = json.loads(path.read_text(encoding="utf-8"))
817
+ else:
818
+ with path.open("r", encoding="utf-8-sig", newline="") as fh:
819
+ data = next(csv.DictReader(fh), {})
820
+ except Exception:
821
+ data = {}
822
+ return (
823
+ data.get("review") or data.get("review_text") or "",
824
+ data.get("features") or data.get("features_text") or "",
825
+ data.get("categories") or data.get("categories_text") or "",
826
+ _safe_float(data.get("price"), 0.0),
827
+ _safe_float(data.get("average_rating") or data.get("rating"), 4.0),
828
+ _safe_float(data.get("rating_number") or data.get("rating_count"), 0.0),
829
+ data.get("highlight_aspect") or data.get("aspect") or "All",
830
+ )
831
+
832
+
833
+ def export_external_rows(rows):
834
+ return _write_csv_download(
835
+ "external_review_prediction.csv",
836
+ ["Aspect", "Prediction", "Confidence", "Key review evidence"],
837
+ rows,
838
+ )
839
+
840
+
841
  def _status_html() -> str:
842
  missing = []
843
  if not CHECKPOINT_PATH.exists() or CHECKPOINT_PATH.stat().st_size < 1024 * 1024:
 
904
  .kv-table td { border-bottom:1px solid #e2e8f0; padding:9px 12px; }
905
  .kv-table td:first-child { width:220px; color:#334155; font-weight:700; background:#f8fafc; }
906
  .compact-note { color:#475569; font-size:13px; margin:4px 0 10px; }
907
+ .module-head { display:flex; justify-content:space-between; align-items:center; gap:12px; margin:0 0 10px; }
908
+ .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; }
909
+ .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); }
910
+ .info-tip:hover .tip-content { display:block; }
911
+ .negative-grid { display:grid; grid-template-columns:repeat(3, minmax(0, 1fr)); gap:10px; margin:8px 0 14px; }
912
+ .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; }
913
+ .negative-card p { margin:5px 0 0; color:#334155; font-size:13px; }
914
+ .mini-product-img { width:64px; height:76px; object-fit:cover; border-radius:6px; border:1px solid var(--line); background:#f8fafc; }
915
+ @media (max-width:860px) { .aspect-grid, .metric-grid, .product-layout, .decision-layout, .negative-grid { grid-template-columns:1fr; } .product-img { width:100%; height:180px; } }
916
  """
917
 
918
 
 
954
  filter_btn.click(filter_products, [filter_aspect, filter_sentiment, filter_category, filter_tags, min_rating], [filter_table, filter_summary])
955
 
956
  with gr.Tab("Merchant Interface"):
957
+ 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>')
958
+ gr.HTML('<div class="small-label">Most negative products across the six aspects</div>')
959
+ negative_spotlight = gr.HTML('<div class="note-card">Loading most negative products...</div>')
960
+ with gr.Accordion("Score filters and export", open=True):
 
 
 
 
 
 
961
  with gr.Row():
962
+ merchant_metric = gr.Dropdown(["Overall"] + ASPECTS, value="Overall", label="Aspect")
963
+ merchant_prediction = gr.Dropdown(["Any", "Positive", "Negative", "Not_Mentioned", "Neutral"], value="Any", label="Prediction")
964
+ merchant_category = gr.Dropdown(categories, value="All", label="Category")
965
+ merchant_scores = gr.Dataframe(headers=["Product", "Category", "Metric", "Prediction", "Confidence", "Price", "Rating"], value=[["Click Apply Score Filter", "", "", "", 0.0, 0.0, 0.0]], datatype=["str", "str", "str", "str", "number", "number", "number"], interactive=False)
966
  with gr.Row():
967
+ refresh_scores = gr.Button("Apply Score Filter", variant="primary")
968
+ export_scores = gr.Button("Export Score List")
969
+ merchant_scores_file = gr.File(label="Downloaded score CSV", interactive=False)
970
+ with gr.Accordion("New Product Metadata Risk Screening", open=False):
 
 
 
 
 
971
  with gr.Row():
972
+ with gr.Column(scale=1):
973
+ new_import = gr.File(label="Import product metadata JSON/CSV")
974
+ new_features = gr.Textbox("Cotton Polyester Blend, Slim Fit, Graphic Print, Machine Wash", label="New product features", lines=4)
975
+ new_categories = gr.Textbox("Clothing > Women > Tops > T-Shirts", label="New product categories")
976
+ new_price = gr.Number(29.99, label="Price")
977
+ new_rating = gr.Number(4.1, label="Expected or early average rating")
978
+ new_count = gr.Number(35, label="Expected or early rating count")
979
+ new_focus = gr.Dropdown(["All"] + ASPECTS, value="All", label="Focus aspect")
980
+ screen_btn = gr.Button("Predict Metadata Risk", variant="primary")
981
+ with gr.Column(scale=1):
982
+ risk_summary = gr.HTML()
983
+ 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)
984
+ export_risk = gr.Button("Export Risk Result")
985
+ risk_file = gr.File(label="Downloaded risk CSV", interactive=False)
986
+ with gr.Accordion("External Review Prediction", open=False):
987
  with gr.Row():
988
+ with gr.Column(scale=1):
989
+ ext_import = gr.File(label="Import review metadata JSON/CSV")
990
+ 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)
991
+ ext_features = gr.Textbox("Cotton Blend, Slim Fit, Zipper Closure", label="Product features", lines=3)
992
+ ext_categories = gr.Textbox("Clothing > Women > Jackets", label="Product categories")
993
+ ext_price = gr.Number(39.99, label="Price")
994
+ ext_rating = gr.Number(4.2, label="Average rating")
995
+ ext_count = gr.Number(312, label="Rating count")
996
+ ext_aspect = gr.Dropdown(["All"] + ASPECTS, value="All", label="Highlight aspect")
997
+ external_btn = gr.Button("Analyze External Review", variant="primary")
998
+ with gr.Column(scale=1):
999
+ ext_overall = gr.HTML()
1000
+ ext_aspects = gr.HTML()
1001
+ ext_table = gr.Dataframe(headers=["Aspect", "Prediction", "Confidence", "Key review evidence"], value=[["Click Analyze External Review", "", 0.0, ""]], datatype=["str", "str", "number", "str"], interactive=False)
1002
+ export_ext = gr.Button("Export Review Result")
1003
+ ext_file = gr.File(label="Downloaded review CSV", interactive=False)
1004
+ refresh_scores.click(merchant_score_filter, [merchant_metric, merchant_prediction, merchant_category], merchant_scores)
1005
+ export_scores.click(export_merchant_scores, merchant_scores, merchant_scores_file)
1006
+ merchant_metric.change(merchant_score_filter, [merchant_metric, merchant_prediction, merchant_category], merchant_scores)
1007
+ merchant_prediction.change(merchant_score_filter, [merchant_metric, merchant_prediction, merchant_category], merchant_scores)
1008
+ merchant_category.change(merchant_score_filter, [merchant_metric, merchant_prediction, merchant_category], merchant_scores)
1009
+ new_import.change(import_new_product_payload, new_import, [new_features, new_categories, new_price, new_rating, new_count, new_focus])
1010
  screen_btn.click(screen_new_product, [new_features, new_categories, new_price, new_rating, new_count, new_focus], [risk_summary, risk_table])
1011
+ export_risk.click(export_risk_rows, risk_table, risk_file)
1012
+ ext_import.change(import_external_review_payload, ext_import, [external_review, ext_features, ext_categories, ext_price, ext_rating, ext_count, ext_aspect])
1013
  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])
1014
+ export_ext.click(export_external_rows, ext_table, ext_file)
1015
 
1016
  with gr.Tab("Research Metrics"):
1017
  gr.Markdown("Metrics are loaded from the updated 10W experiment reports. Tables use CSV outputs when available, with JSON fallback.")
 
1034
  refresh_research = gr.Button("Refresh Research Metrics", variant="primary")
1035
  refresh_research.click(refresh_research_outputs, outputs=[research_cards, overall_table, aspect_table, ablation_table, meta_source_table])
1036
  demo.load(consumer_product_view, [product_select, consumer_aspect], [product_detail, aspect_html, evidence_html, consumer_table])
1037
+ demo.load(negative_product_spotlight, outputs=negative_spotlight)
1038
+ demo.load(merchant_score_filter, [merchant_metric, merchant_prediction, merchant_category], merchant_scores)
1039
  demo.load(filter_products, [filter_aspect, filter_sentiment, filter_category, filter_tags, min_rating], [filter_table, filter_summary])
1040
  return demo
1041