Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -9,15 +9,15 @@ import plotly.express as px
|
|
| 9 |
from plotly.subplots import make_subplots
|
| 10 |
import plotly.graph_objects as go
|
| 11 |
from datetime import date, timedelta
|
| 12 |
-
from huggingface_hub import HfApi
|
| 13 |
|
| 14 |
# =====================================================
|
| 15 |
# CONFIG / SECRETS
|
| 16 |
# =====================================================
|
| 17 |
API_TOKEN = os.getenv("LEADFEEDER_API_TOKEN")
|
| 18 |
APP_PASSWORD = os.getenv("APP_PASSWORD")
|
| 19 |
-
HF_TOKEN = os.getenv("HF_TOKEN")
|
| 20 |
-
SPACE_ID = os.getenv("SPACE_ID")
|
| 21 |
|
| 22 |
if not API_TOKEN or not APP_PASSWORD:
|
| 23 |
print("โ ๏ธ WARNING: Secrets missing. App will launch but API calls will fail.")
|
|
@@ -27,45 +27,36 @@ if not API_TOKEN or not APP_PASSWORD:
|
|
| 27 |
ACCOUNT_ID = "255333"
|
| 28 |
BASE_URL = "https://api.leadfeeder.com"
|
| 29 |
PAGE_SIZE = 100
|
|
|
|
| 30 |
HEADERS = {
|
| 31 |
"Authorization": f"Token token={API_TOKEN}",
|
| 32 |
"Accept": "application/json"
|
| 33 |
}
|
|
|
|
| 34 |
CAMPAIGN_CONFIG_FILE = "campaign_rules.json"
|
| 35 |
|
| 36 |
# =====================================================
|
| 37 |
# 1. CAMPAIGN MANAGER & LOGIC
|
| 38 |
# =====================================================
|
| 39 |
-
DEFAULT_CAMPAIGNS = {
|
| 40 |
-
"MS Tech": [
|
| 41 |
-
"microsoft", "power", "dynamics", "crm", "365", "xamarin", "sharepoint",
|
| 42 |
-
"dot", ".net", "asp.net", "azure", "copilot", "real-estate-website-development",
|
| 43 |
-
"manufacturing-production", "legacy-hrms", "healthcare-appointment", "eld-fleet",
|
| 44 |
-
"legacy-erp", "clinical-trial", "digital-e-learning", "bi-analytics",
|
| 45 |
-
"application-modernization", "data-warehouse", "workforce", "contingent"
|
| 46 |
-
],
|
| 47 |
-
"Fintech": [
|
| 48 |
-
"fintech", "finance", "money", "credit", "debit", "card", "invest", "bank",
|
| 49 |
-
"wallet", "account", "bookkeeping", "payment", "loan", "nfc", "lend", "debt",
|
| 50 |
-
"tax", "invoice", "quot", "borrow", "bill", "coin", "currency", "aml", "kyc",
|
| 51 |
-
"blockchain", "cryptocurrency", "budget", "expense", "wealth", "sox", "sarbanes"
|
| 52 |
-
],
|
| 53 |
-
"Adtech": [
|
| 54 |
-
"programmatic", "publisher", "media", "entertainment", "trading", "ott",
|
| 55 |
-
"email", "news", "social", "marketing", "video", "audio", "radio", "livestream",
|
| 56 |
-
"bid", "anime", "dooh", "vod", "rtb", "supply-side-platform", "demand-side-platform",
|
| 57 |
-
"data-management-platform"
|
| 58 |
-
]
|
| 59 |
-
}
|
| 60 |
-
|
| 61 |
def load_campaign_rules():
|
|
|
|
| 62 |
if os.path.exists(CAMPAIGN_CONFIG_FILE):
|
| 63 |
try:
|
| 64 |
with open(CAMPAIGN_CONFIG_FILE, "r") as f:
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
|
| 70 |
def categorize_quality(score):
|
| 71 |
if pd.isna(score): return "Unknown"
|
|
@@ -81,30 +72,42 @@ def categorize_quality(score):
|
|
| 81 |
def get_campaign_match(text, rules):
|
| 82 |
if not text or not isinstance(text, str):
|
| 83 |
return None
|
|
|
|
| 84 |
text_lower = text.lower()
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
return None
|
| 90 |
|
| 91 |
def apply_business_logic(df):
|
| 92 |
if df is None or df.empty: return df
|
| 93 |
-
|
| 94 |
# 1. Quality Group
|
| 95 |
if "lead_quality_score" in df.columns:
|
| 96 |
df["Quality_Group"] = df["lead_quality_score"].apply(categorize_quality)
|
| 97 |
-
|
| 98 |
-
# 2. Campaign Logic (Smart Fallback)
|
| 99 |
-
rules = load_campaign_rules()
|
| 100 |
|
|
|
|
|
|
|
|
|
|
| 101 |
def resolve_campaign(row):
|
| 102 |
# Priority 1: Landing Page
|
| 103 |
if row.get("landing_page_path"):
|
| 104 |
match = get_campaign_match(row["landing_page_path"], rules)
|
| 105 |
if match: return match
|
| 106 |
|
| 107 |
-
# Priority 2: Exit Page (
|
| 108 |
if row.get("exit_page_path"):
|
| 109 |
match = get_campaign_match(row["exit_page_path"], rules)
|
| 110 |
if match: return match
|
|
@@ -118,56 +121,40 @@ def apply_business_logic(df):
|
|
| 118 |
if row.get("primary_industry"):
|
| 119 |
match = get_campaign_match(row["primary_industry"], rules)
|
| 120 |
if match: return match
|
| 121 |
-
|
| 122 |
return "Uncategorized"
|
| 123 |
|
| 124 |
df["Campaign"] = df.apply(resolve_campaign, axis=1)
|
| 125 |
return df
|
| 126 |
|
| 127 |
# =====================================================
|
| 128 |
-
# 2. PRESETS
|
| 129 |
# =====================================================
|
| 130 |
DASHBOARD_PRESETS = {
|
| 131 |
-
|
| 132 |
-
"Key Campaigns Bifurcation (MS Tech, Fintech, Adtech)": ("SPECIAL_KEY_BIFURCATION", None, None),
|
| 133 |
"All Campaigns Performance": ("SPECIAL_ALL_PERFORMANCE", None, None),
|
| 134 |
-
|
| 135 |
-
# -- CAMPAIGN & QUALITY --
|
| 136 |
"Visits by Campaign": ("Campaign", "total_visits", "sum"),
|
| 137 |
"Leads by Campaign": ("Campaign", "company_name", "count"),
|
| 138 |
"Leads by Quality Group": ("Quality_Group", "company_name", "count"),
|
| 139 |
-
|
| 140 |
-
# -- GEOGRAPHY (NEW) --
|
| 141 |
"Visits by Country": ("country", "total_visits", "sum"),
|
| 142 |
"Leads by Country": ("country", "company_name", "count"),
|
| 143 |
"Visits by City": ("city", "total_visits", "sum"),
|
| 144 |
"Leads by City": ("city", "company_name", "count"),
|
| 145 |
-
|
| 146 |
-
# -- INDUSTRY (NEW) --
|
| 147 |
"Visits by Industry": ("primary_industry", "total_visits", "sum"),
|
| 148 |
"Leads by Industry": ("primary_industry", "company_name", "count"),
|
| 149 |
-
|
| 150 |
-
# -- OTHERS --
|
| 151 |
"Top Accounts by Visits": ("company_name", "total_visits", "sum"),
|
| 152 |
}
|
| 153 |
|
| 154 |
TREND_PRESETS = {
|
| 155 |
-
# -- CAMPAIGN & QUALITY --
|
| 156 |
"Visits Trend by Campaign": ("total_visits", "sum", "Campaign"),
|
| 157 |
"Leads Trend by Campaign": ("company_name", "count", "Campaign"),
|
| 158 |
"Visits Trend by Quality": ("total_visits", "sum", "Quality_Group"),
|
| 159 |
-
|
| 160 |
-
# -- GEOGRAPHY (NEW) --
|
| 161 |
"Visits Trend by Country": ("total_visits", "sum", "country"),
|
| 162 |
"Leads Trend by Country": ("company_name", "count", "country"),
|
| 163 |
"Visits Trend by City": ("total_visits", "sum", "city"),
|
| 164 |
"Leads Trend by City": ("company_name", "count", "city"),
|
| 165 |
-
|
| 166 |
-
# -- INDUSTRY (NEW) --
|
| 167 |
"Visits Trend by Industry": ("total_visits", "sum", "primary_industry"),
|
| 168 |
"Leads Trend by Industry": ("company_name", "count", "primary_industry"),
|
| 169 |
-
|
| 170 |
-
# -- GENERAL --
|
| 171 |
"Total Visits Trend": ("total_visits", "sum", None),
|
| 172 |
"Active Accounts Trend": ("company_name", "count", None),
|
| 173 |
}
|
|
@@ -281,7 +268,7 @@ def enrich_leads_with_visits(df, start_date, end_date, max_rows=None, progress=g
|
|
| 281 |
if not landing:
|
| 282 |
first_step = visit_route[0]
|
| 283 |
landing = first_step.get("page_path") or first_step.get("page_url")
|
| 284 |
-
|
| 285 |
if not landing or not exit_p:
|
| 286 |
pv_map = {p["id"]: p["attributes"] for p in included if p["type"] == "page_views"}
|
| 287 |
pv_ids = [r["id"] for r in visit.get("relationships", {}).get("page_views", {}).get("data", [])]
|
|
@@ -353,8 +340,9 @@ def build_dashboard(df, preset, top_n):
|
|
| 353 |
if df is None or df.empty: return px.bar(title="No Data")
|
| 354 |
if "Campaign" not in df.columns: df = apply_business_logic(df)
|
| 355 |
|
| 356 |
-
if preset == "Key Campaigns Bifurcation (
|
| 357 |
-
|
|
|
|
| 358 |
filtered = df[df["Campaign"].isin(target)].copy()
|
| 359 |
if filtered.empty: return px.bar(title="No Data for Key Campaigns")
|
| 360 |
|
|
@@ -423,17 +411,15 @@ def get_trend_filter_options(df, preset):
|
|
| 423 |
metric, agg, segment = TREND_PRESETS[preset]
|
| 424 |
if not segment: return gr.update(choices=[], value=None, visible=False)
|
| 425 |
|
| 426 |
-
# FORCE LOGIC
|
| 427 |
if segment not in df.columns:
|
| 428 |
df = apply_business_logic(df)
|
| 429 |
|
| 430 |
options = sorted(df[segment].astype(str).unique().tolist())
|
| 431 |
|
| 432 |
-
# Smart Defaults for specific columns
|
| 433 |
if segment == "Campaign":
|
| 434 |
-
defaults = [
|
| 435 |
else:
|
| 436 |
-
defaults = options[:5]
|
| 437 |
|
| 438 |
if not defaults: defaults = options[:5]
|
| 439 |
return gr.update(choices=options, value=defaults, visible=True, label=f"Filter {segment}")
|
|
@@ -447,9 +433,10 @@ with gr.Blocks(title="Leadfeeder Campaign Pro") as demo:
|
|
| 447 |
with gr.Row():
|
| 448 |
pwd = gr.Textbox(type="password", label="App Password")
|
| 449 |
gr.Button("Auth").click(lambda p: gr.Info("Success") if p==APP_PASSWORD else gr.Error("Invalid"), pwd, None)
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
|
|
|
| 453 |
with gr.Tabs():
|
| 454 |
# --- TAB 1: DATA ---
|
| 455 |
with gr.Tab("๐ Data & Report"):
|
|
@@ -462,6 +449,7 @@ with gr.Blocks(title="Leadfeeder Campaign Pro") as demo:
|
|
| 462 |
file_dl = gr.File(label="Download Excel")
|
| 463 |
|
| 464 |
table = gr.Dataframe(label="Preview (Top 50 Enriched)", interactive=True)
|
|
|
|
| 465 |
btn_load.click(load_preview, [start, end], [df_state, table, status])
|
| 466 |
btn_dl.click(download_full_excel, [df_state, start, end], file_dl)
|
| 467 |
|
|
@@ -472,7 +460,7 @@ with gr.Blocks(title="Leadfeeder Campaign Pro") as demo:
|
|
| 472 |
|
| 473 |
gr.Markdown("### ๐ Charts")
|
| 474 |
with gr.Row():
|
| 475 |
-
preset = gr.Dropdown(choices=list(DASHBOARD_PRESETS.keys()), label="Chart View", value="Key Campaigns Bifurcation (
|
| 476 |
top_n = gr.Slider(5, 50, value=10, label="Top N Items")
|
| 477 |
chart = gr.Plot()
|
| 478 |
gr.Button("Build View").click(build_dashboard, [df_state, preset, top_n], chart)
|
|
@@ -493,41 +481,49 @@ with gr.Blocks(title="Leadfeeder Campaign Pro") as demo:
|
|
| 493 |
gr.Markdown("### Manage Campaign Groups")
|
| 494 |
|
| 495 |
init_rules = load_campaign_rules()
|
| 496 |
-
camp_choices = list(init_rules.keys()) + ["+ Create New Campaign"]
|
| 497 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 498 |
|
| 499 |
with gr.Row():
|
| 500 |
camp_dropdown = gr.Dropdown(choices=camp_choices, label="Select Campaign to Edit", value=camp_choices[0])
|
| 501 |
-
new_camp_name = gr.Textbox(label="New Campaign Name", visible=
|
|
|
|
|
|
|
|
|
|
|
|
|
| 502 |
|
| 503 |
-
kw_input = gr.Textbox(label="Keywords (comma separated)", lines=5, value=default_kws)
|
| 504 |
-
|
| 505 |
def update_ui_on_select(selected_camp):
|
| 506 |
rules = load_campaign_rules()
|
| 507 |
if selected_camp == "+ Create New Campaign":
|
| 508 |
-
return gr.update(visible=True, value=""), gr.update(value="")
|
| 509 |
else:
|
| 510 |
-
|
| 511 |
-
|
|
|
|
| 512 |
|
| 513 |
-
camp_dropdown.change(update_ui_on_select, inputs=[camp_dropdown], outputs=[new_camp_name,
|
| 514 |
|
| 515 |
save_config_btn = gr.Button("๐พ Save Configuration to Hugging Face", variant="primary")
|
| 516 |
config_status = gr.Markdown()
|
| 517 |
|
| 518 |
-
def save_easy_config(selected_camp, new_name,
|
| 519 |
rules = load_campaign_rules()
|
| 520 |
|
| 521 |
-
|
| 522 |
-
|
| 523 |
|
| 524 |
target_camp = new_name.strip() if selected_camp == "+ Create New Campaign" else selected_camp
|
| 525 |
|
| 526 |
if not target_camp:
|
| 527 |
return "โ Error: Campaign name cannot be empty.", gr.update()
|
| 528 |
|
| 529 |
-
# 1. Update the dictionary
|
| 530 |
-
rules[target_camp] =
|
| 531 |
|
| 532 |
# 2. Save file locally first
|
| 533 |
with open(CAMPAIGN_CONFIG_FILE, "w") as f:
|
|
@@ -554,7 +550,7 @@ with gr.Blocks(title="Leadfeeder Campaign Pro") as demo:
|
|
| 554 |
updated_choices = list(rules.keys()) + ["+ Create New Campaign"]
|
| 555 |
return status_message, gr.update(choices=updated_choices, value=target_camp)
|
| 556 |
|
| 557 |
-
save_config_btn.click(save_easy_config, inputs=[camp_dropdown, new_camp_name,
|
| 558 |
|
| 559 |
# --- TAB 5: DEBUGGER ---
|
| 560 |
with gr.Tab("๐ ๏ธ Debugger"):
|
|
|
|
| 9 |
from plotly.subplots import make_subplots
|
| 10 |
import plotly.graph_objects as go
|
| 11 |
from datetime import date, timedelta
|
| 12 |
+
from huggingface_hub import HfApi
|
| 13 |
|
| 14 |
# =====================================================
|
| 15 |
# CONFIG / SECRETS
|
| 16 |
# =====================================================
|
| 17 |
API_TOKEN = os.getenv("LEADFEEDER_API_TOKEN")
|
| 18 |
APP_PASSWORD = os.getenv("APP_PASSWORD")
|
| 19 |
+
HF_TOKEN = os.getenv("HF_TOKEN")
|
| 20 |
+
SPACE_ID = os.getenv("SPACE_ID")
|
| 21 |
|
| 22 |
if not API_TOKEN or not APP_PASSWORD:
|
| 23 |
print("โ ๏ธ WARNING: Secrets missing. App will launch but API calls will fail.")
|
|
|
|
| 27 |
ACCOUNT_ID = "255333"
|
| 28 |
BASE_URL = "https://api.leadfeeder.com"
|
| 29 |
PAGE_SIZE = 100
|
| 30 |
+
|
| 31 |
HEADERS = {
|
| 32 |
"Authorization": f"Token token={API_TOKEN}",
|
| 33 |
"Accept": "application/json"
|
| 34 |
}
|
| 35 |
+
|
| 36 |
CAMPAIGN_CONFIG_FILE = "campaign_rules.json"
|
| 37 |
|
| 38 |
# =====================================================
|
| 39 |
# 1. CAMPAIGN MANAGER & LOGIC
|
| 40 |
# =====================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
def load_campaign_rules():
|
| 42 |
+
"""Loads rules from JSON. Auto-migrates old list-based format to new include/exclude format."""
|
| 43 |
if os.path.exists(CAMPAIGN_CONFIG_FILE):
|
| 44 |
try:
|
| 45 |
with open(CAMPAIGN_CONFIG_FILE, "r") as f:
|
| 46 |
+
rules = json.load(f)
|
| 47 |
+
|
| 48 |
+
# Backward compatibility: Convert flat lists into include/exclude dicts
|
| 49 |
+
for campaign, config in rules.items():
|
| 50 |
+
if isinstance(config, list):
|
| 51 |
+
rules[campaign] = {
|
| 52 |
+
"include": config,
|
| 53 |
+
"exclude": []
|
| 54 |
+
}
|
| 55 |
+
return rules
|
| 56 |
+
except Exception as e:
|
| 57 |
+
print(f"Error loading {CAMPAIGN_CONFIG_FILE}: {e}")
|
| 58 |
+
return {}
|
| 59 |
+
return {}
|
| 60 |
|
| 61 |
def categorize_quality(score):
|
| 62 |
if pd.isna(score): return "Unknown"
|
|
|
|
| 72 |
def get_campaign_match(text, rules):
|
| 73 |
if not text or not isinstance(text, str):
|
| 74 |
return None
|
| 75 |
+
|
| 76 |
text_lower = text.lower()
|
| 77 |
+
|
| 78 |
+
for campaign_name, config in rules.items():
|
| 79 |
+
includes = config.get("include", [])
|
| 80 |
+
excludes = config.get("exclude", [])
|
| 81 |
+
|
| 82 |
+
# 1st: Check Exclusions. If an excluded word is present, skip this campaign entirely
|
| 83 |
+
has_exclusion = any(ex.lower() in text_lower for ex in excludes if ex.strip())
|
| 84 |
+
if has_exclusion:
|
| 85 |
+
continue
|
| 86 |
+
|
| 87 |
+
# 2nd: Check Inclusions. If an included word is present, it's a match
|
| 88 |
+
has_inclusion = any(inc.lower() in text_lower for inc in includes if inc.strip())
|
| 89 |
+
if has_inclusion:
|
| 90 |
+
return campaign_name
|
| 91 |
+
|
| 92 |
return None
|
| 93 |
|
| 94 |
def apply_business_logic(df):
|
| 95 |
if df is None or df.empty: return df
|
| 96 |
+
|
| 97 |
# 1. Quality Group
|
| 98 |
if "lead_quality_score" in df.columns:
|
| 99 |
df["Quality_Group"] = df["lead_quality_score"].apply(categorize_quality)
|
|
|
|
|
|
|
|
|
|
| 100 |
|
| 101 |
+
# 2. Campaign Logic (Strict Fallback)
|
| 102 |
+
rules = load_campaign_rules()
|
| 103 |
+
|
| 104 |
def resolve_campaign(row):
|
| 105 |
# Priority 1: Landing Page
|
| 106 |
if row.get("landing_page_path"):
|
| 107 |
match = get_campaign_match(row["landing_page_path"], rules)
|
| 108 |
if match: return match
|
| 109 |
|
| 110 |
+
# Priority 2: Exit Page (Only checked if Landing Page DID NOT match)
|
| 111 |
if row.get("exit_page_path"):
|
| 112 |
match = get_campaign_match(row["exit_page_path"], rules)
|
| 113 |
if match: return match
|
|
|
|
| 121 |
if row.get("primary_industry"):
|
| 122 |
match = get_campaign_match(row["primary_industry"], rules)
|
| 123 |
if match: return match
|
| 124 |
+
|
| 125 |
return "Uncategorized"
|
| 126 |
|
| 127 |
df["Campaign"] = df.apply(resolve_campaign, axis=1)
|
| 128 |
return df
|
| 129 |
|
| 130 |
# =====================================================
|
| 131 |
+
# 2. PRESETS
|
| 132 |
# =====================================================
|
| 133 |
DASHBOARD_PRESETS = {
|
| 134 |
+
"Key Campaigns Bifurcation (Top Campaigns)": ("SPECIAL_KEY_BIFURCATION", None, None),
|
|
|
|
| 135 |
"All Campaigns Performance": ("SPECIAL_ALL_PERFORMANCE", None, None),
|
|
|
|
|
|
|
| 136 |
"Visits by Campaign": ("Campaign", "total_visits", "sum"),
|
| 137 |
"Leads by Campaign": ("Campaign", "company_name", "count"),
|
| 138 |
"Leads by Quality Group": ("Quality_Group", "company_name", "count"),
|
|
|
|
|
|
|
| 139 |
"Visits by Country": ("country", "total_visits", "sum"),
|
| 140 |
"Leads by Country": ("country", "company_name", "count"),
|
| 141 |
"Visits by City": ("city", "total_visits", "sum"),
|
| 142 |
"Leads by City": ("city", "company_name", "count"),
|
|
|
|
|
|
|
| 143 |
"Visits by Industry": ("primary_industry", "total_visits", "sum"),
|
| 144 |
"Leads by Industry": ("primary_industry", "company_name", "count"),
|
|
|
|
|
|
|
| 145 |
"Top Accounts by Visits": ("company_name", "total_visits", "sum"),
|
| 146 |
}
|
| 147 |
|
| 148 |
TREND_PRESETS = {
|
|
|
|
| 149 |
"Visits Trend by Campaign": ("total_visits", "sum", "Campaign"),
|
| 150 |
"Leads Trend by Campaign": ("company_name", "count", "Campaign"),
|
| 151 |
"Visits Trend by Quality": ("total_visits", "sum", "Quality_Group"),
|
|
|
|
|
|
|
| 152 |
"Visits Trend by Country": ("total_visits", "sum", "country"),
|
| 153 |
"Leads Trend by Country": ("company_name", "count", "country"),
|
| 154 |
"Visits Trend by City": ("total_visits", "sum", "city"),
|
| 155 |
"Leads Trend by City": ("company_name", "count", "city"),
|
|
|
|
|
|
|
| 156 |
"Visits Trend by Industry": ("total_visits", "sum", "primary_industry"),
|
| 157 |
"Leads Trend by Industry": ("company_name", "count", "primary_industry"),
|
|
|
|
|
|
|
| 158 |
"Total Visits Trend": ("total_visits", "sum", None),
|
| 159 |
"Active Accounts Trend": ("company_name", "count", None),
|
| 160 |
}
|
|
|
|
| 268 |
if not landing:
|
| 269 |
first_step = visit_route[0]
|
| 270 |
landing = first_step.get("page_path") or first_step.get("page_url")
|
| 271 |
+
|
| 272 |
if not landing or not exit_p:
|
| 273 |
pv_map = {p["id"]: p["attributes"] for p in included if p["type"] == "page_views"}
|
| 274 |
pv_ids = [r["id"] for r in visit.get("relationships", {}).get("page_views", {}).get("data", [])]
|
|
|
|
| 340 |
if df is None or df.empty: return px.bar(title="No Data")
|
| 341 |
if "Campaign" not in df.columns: df = apply_business_logic(df)
|
| 342 |
|
| 343 |
+
if preset == "Key Campaigns Bifurcation (Top Campaigns)":
|
| 344 |
+
rules = load_campaign_rules()
|
| 345 |
+
target = list(rules.keys())[:3] if rules else []
|
| 346 |
filtered = df[df["Campaign"].isin(target)].copy()
|
| 347 |
if filtered.empty: return px.bar(title="No Data for Key Campaigns")
|
| 348 |
|
|
|
|
| 411 |
metric, agg, segment = TREND_PRESETS[preset]
|
| 412 |
if not segment: return gr.update(choices=[], value=None, visible=False)
|
| 413 |
|
|
|
|
| 414 |
if segment not in df.columns:
|
| 415 |
df = apply_business_logic(df)
|
| 416 |
|
| 417 |
options = sorted(df[segment].astype(str).unique().tolist())
|
| 418 |
|
|
|
|
| 419 |
if segment == "Campaign":
|
| 420 |
+
defaults = options[:3] # Pick top 3 dynamic campaigns automatically
|
| 421 |
else:
|
| 422 |
+
defaults = options[:5]
|
| 423 |
|
| 424 |
if not defaults: defaults = options[:5]
|
| 425 |
return gr.update(choices=options, value=defaults, visible=True, label=f"Filter {segment}")
|
|
|
|
| 433 |
with gr.Row():
|
| 434 |
pwd = gr.Textbox(type="password", label="App Password")
|
| 435 |
gr.Button("Auth").click(lambda p: gr.Info("Success") if p==APP_PASSWORD else gr.Error("Invalid"), pwd, None)
|
| 436 |
+
|
| 437 |
+
df_state = gr.State()
|
| 438 |
+
status = gr.Markdown()
|
| 439 |
+
|
| 440 |
with gr.Tabs():
|
| 441 |
# --- TAB 1: DATA ---
|
| 442 |
with gr.Tab("๐ Data & Report"):
|
|
|
|
| 449 |
file_dl = gr.File(label="Download Excel")
|
| 450 |
|
| 451 |
table = gr.Dataframe(label="Preview (Top 50 Enriched)", interactive=True)
|
| 452 |
+
|
| 453 |
btn_load.click(load_preview, [start, end], [df_state, table, status])
|
| 454 |
btn_dl.click(download_full_excel, [df_state, start, end], file_dl)
|
| 455 |
|
|
|
|
| 460 |
|
| 461 |
gr.Markdown("### ๐ Charts")
|
| 462 |
with gr.Row():
|
| 463 |
+
preset = gr.Dropdown(choices=list(DASHBOARD_PRESETS.keys()), label="Chart View", value="Key Campaigns Bifurcation (Top Campaigns)")
|
| 464 |
top_n = gr.Slider(5, 50, value=10, label="Top N Items")
|
| 465 |
chart = gr.Plot()
|
| 466 |
gr.Button("Build View").click(build_dashboard, [df_state, preset, top_n], chart)
|
|
|
|
| 481 |
gr.Markdown("### Manage Campaign Groups")
|
| 482 |
|
| 483 |
init_rules = load_campaign_rules()
|
| 484 |
+
camp_choices = list(init_rules.keys()) + ["+ Create New Campaign"] if init_rules else ["+ Create New Campaign"]
|
| 485 |
+
|
| 486 |
+
default_inc = ""
|
| 487 |
+
default_exc = ""
|
| 488 |
+
if init_rules and camp_choices[0] in init_rules:
|
| 489 |
+
default_inc = ", ".join(init_rules[camp_choices[0]].get("include", []))
|
| 490 |
+
default_exc = ", ".join(init_rules[camp_choices[0]].get("exclude", []))
|
| 491 |
|
| 492 |
with gr.Row():
|
| 493 |
camp_dropdown = gr.Dropdown(choices=camp_choices, label="Select Campaign to Edit", value=camp_choices[0])
|
| 494 |
+
new_camp_name = gr.Textbox(label="New Campaign Name", visible=(not init_rules))
|
| 495 |
+
|
| 496 |
+
with gr.Row():
|
| 497 |
+
inc_kw_input = gr.Textbox(label="Include Keywords (comma separated)", lines=3, value=default_inc)
|
| 498 |
+
exc_kw_input = gr.Textbox(label="Exclude Keywords (comma separated)", lines=3, value=default_exc)
|
| 499 |
|
|
|
|
|
|
|
| 500 |
def update_ui_on_select(selected_camp):
|
| 501 |
rules = load_campaign_rules()
|
| 502 |
if selected_camp == "+ Create New Campaign":
|
| 503 |
+
return gr.update(visible=True, value=""), gr.update(value=""), gr.update(value="")
|
| 504 |
else:
|
| 505 |
+
inc_kws = rules.get(selected_camp, {}).get("include", [])
|
| 506 |
+
exc_kws = rules.get(selected_camp, {}).get("exclude", [])
|
| 507 |
+
return gr.update(visible=False), gr.update(value=", ".join(inc_kws)), gr.update(value=", ".join(exc_kws))
|
| 508 |
|
| 509 |
+
camp_dropdown.change(update_ui_on_select, inputs=[camp_dropdown], outputs=[new_camp_name, inc_kw_input, exc_kw_input])
|
| 510 |
|
| 511 |
save_config_btn = gr.Button("๐พ Save Configuration to Hugging Face", variant="primary")
|
| 512 |
config_status = gr.Markdown()
|
| 513 |
|
| 514 |
+
def save_easy_config(selected_camp, new_name, inc_string, exc_string):
|
| 515 |
rules = load_campaign_rules()
|
| 516 |
|
| 517 |
+
clean_inc = [k.strip().lower() for k in inc_string.split(",") if k.strip()]
|
| 518 |
+
clean_exc = [k.strip().lower() for k in exc_string.split(",") if k.strip()]
|
| 519 |
|
| 520 |
target_camp = new_name.strip() if selected_camp == "+ Create New Campaign" else selected_camp
|
| 521 |
|
| 522 |
if not target_camp:
|
| 523 |
return "โ Error: Campaign name cannot be empty.", gr.update()
|
| 524 |
|
| 525 |
+
# 1. Update the dictionary format
|
| 526 |
+
rules[target_camp] = {"include": clean_inc, "exclude": clean_exc}
|
| 527 |
|
| 528 |
# 2. Save file locally first
|
| 529 |
with open(CAMPAIGN_CONFIG_FILE, "w") as f:
|
|
|
|
| 550 |
updated_choices = list(rules.keys()) + ["+ Create New Campaign"]
|
| 551 |
return status_message, gr.update(choices=updated_choices, value=target_camp)
|
| 552 |
|
| 553 |
+
save_config_btn.click(save_easy_config, inputs=[camp_dropdown, new_camp_name, inc_kw_input, exc_kw_input], outputs=[config_status, camp_dropdown])
|
| 554 |
|
| 555 |
# --- TAB 5: DEBUGGER ---
|
| 556 |
with gr.Tab("๐ ๏ธ Debugger"):
|