mktgtech commited on
Commit
0de841a
·
verified ·
1 Parent(s): 65eaeb6

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +93 -63
app.py CHANGED
@@ -3,8 +3,11 @@ import time
3
  import requests
4
  import json
5
  import pandas as pd
 
6
  import gradio as gr
7
  import plotly.express as px
 
 
8
  from datetime import date, timedelta
9
 
10
  # =====================================================
@@ -29,7 +32,7 @@ HEADERS = {
29
  CAMPAIGN_CONFIG_FILE = "campaign_rules.json"
30
 
31
  # =====================================================
32
- # 1. CAMPAIGN MANAGER & UTILS
33
  # =====================================================
34
  DEFAULT_CAMPAIGNS = {
35
  "MS Stack": [
@@ -54,7 +57,6 @@ DEFAULT_CAMPAIGNS = {
54
  }
55
 
56
  def load_campaign_rules():
57
- """Load campaign groups from JSON file or use defaults."""
58
  if os.path.exists(CAMPAIGN_CONFIG_FILE):
59
  try:
60
  with open(CAMPAIGN_CONFIG_FILE, "r") as f:
@@ -64,7 +66,6 @@ def load_campaign_rules():
64
  return DEFAULT_CAMPAIGNS
65
 
66
  def save_campaign_rules(rules_text):
67
- """Parse JSON text from UI and save to file."""
68
  try:
69
  rules = json.loads(rules_text)
70
  with open(CAMPAIGN_CONFIG_FILE, "w") as f:
@@ -74,7 +75,6 @@ def save_campaign_rules(rules_text):
74
  return "❌ Error: Invalid JSON format.", None
75
 
76
  def categorize_quality(score):
77
- """Classify Lead Quality Score into High/Mid/Low."""
78
  if pd.isna(score): return "Unknown"
79
  try:
80
  s = int(score)
@@ -86,10 +86,8 @@ def categorize_quality(score):
86
  return "Unknown"
87
 
88
  def assign_campaign(url, rules):
89
- """Match URL against campaign keywords."""
90
  if not url or not isinstance(url, str):
91
  return "Uncategorized"
92
-
93
  url_lower = url.lower()
94
  for campaign_name, keywords in rules.items():
95
  for kw in keywords:
@@ -98,29 +96,31 @@ def assign_campaign(url, rules):
98
  return "Other"
99
 
100
  def apply_business_logic(df):
101
- """Apply Campaign and Quality logic to the DataFrame."""
102
  if df is None or df.empty: return df
103
 
104
- # 1. Apply Quality Logic
105
- df["Quality_Group"] = df["lead_quality_score"].apply(categorize_quality)
 
106
 
107
- # 2. Apply Campaign Logic
108
  rules = load_campaign_rules()
109
- # We use landing_page_path primarily.
110
- df["Campaign"] = df["landing_page_path"].apply(lambda x: assign_campaign(x, rules))
111
-
 
 
112
  return df
113
 
114
  # =====================================================
115
- # 2. PRESETS (Original + New)
116
  # =====================================================
117
  DASHBOARD_PRESETS = {
118
- # -- NEW PRESETS --
 
119
  "Visits by Campaign": ("Campaign", "total_visits", "sum"),
120
  "Leads by Campaign": ("Campaign", "company_name", "count"),
121
  "Leads by Quality Group": ("Quality_Group", "company_name", "count"),
122
 
123
- # -- ORIGINAL PRESETS --
124
  "Total Visits by Industry": ("primary_industry", "total_visits", "sum"),
125
  "Total Visits by Country": ("country", "total_visits", "sum"),
126
  "Avg Quality by Industry": ("primary_industry", "lead_quality_score", "mean"),
@@ -128,15 +128,13 @@ DASHBOARD_PRESETS = {
128
  "Companies by Country": ("country", "company_name", "count"),
129
  "Visits by City": ("city", "total_visits", "sum"),
130
  "Accounts by Assignee": ("assignee", "company_name", "count"),
131
- "Visits by Assignee": ("assignee", "total_visits", "sum"),
132
  }
133
 
134
  TREND_PRESETS = {
135
- # -- NEW TRENDS --
136
  "Visits Trend by Campaign": ("total_visits", "sum", "Campaign"),
 
137
  "Visits Trend by Quality": ("total_visits", "sum", "Quality_Group"),
138
 
139
- # -- ORIGINAL TRENDS --
140
  "Total Visits Trend": ("total_visits", "sum", None),
141
  "Active Accounts Trend": ("company_name", "count", None),
142
  "Avg Lead Quality Trend": ("lead_quality_score", "mean", None),
@@ -146,7 +144,7 @@ TREND_PRESETS = {
146
  }
147
 
148
  # =====================================================
149
- # 3. API HANDLING
150
  # =====================================================
151
  def make_request(url, params=None):
152
  retries = 3
@@ -162,7 +160,7 @@ def make_request(url, params=None):
162
  raise Exception("Max retries exceeded")
163
 
164
  def fetch_basic_leads(start_date, end_date):
165
- """ Step 1: Fetch ALL company details (fast fetch). """
166
  page = 1
167
  rows = []
168
  print(f"🚀 Fetching full company list for {start_date} to {end_date}...")
@@ -202,17 +200,21 @@ def fetch_basic_leads(start_date, end_date):
202
  "lead_quality_score": a.get("quality"),
203
  "revenue": a.get("revenue"),
204
  "employee_count": a.get("employee_count"),
 
 
205
  "assignee": a.get("assignee"),
206
  "emailed_to": a.get("emailed_to"),
 
207
  "crm_organization_id": a.get("crm_organization_id"),
208
  "tags": ", ".join(a.get("tags", [])) if a.get("tags") else None,
209
  "linkedin_url": a.get("linkedin_url"),
 
 
210
  "country": loc.get("country"),
211
  "region": loc.get("region"),
212
  "city": loc.get("city"),
213
  "leadfeeder_url": a.get("view_in_leadfeeder"),
214
- # Placeholders for enrichment
215
- "landing_page_path": None,
216
  "exit_page_path": None
217
  })
218
  print(f"✅ Page {page} loaded. Rows: {len(rows)}")
@@ -227,7 +229,7 @@ def fetch_basic_leads(start_date, end_date):
227
  return df
228
 
229
  def enrich_leads_with_visits(df, start_date, end_date, max_rows=None, progress=gr.Progress()):
230
- """ Step 2: Deep enrich with visit_route parsing for Exit/Landing Page. """
231
  if df.empty: return df
232
  target_df = df.head(max_rows) if max_rows else df
233
  total = len(target_df)
@@ -291,18 +293,15 @@ def enrich_leads_with_visits(df, start_date, end_date, max_rows=None, progress=g
291
  return df
292
 
293
  # =====================================================
294
- # 4. MAIN FUNCTIONS
295
  # =====================================================
296
  def load_preview(start, end):
297
- # 1. Fetch
298
  df = fetch_basic_leads(start, end)
299
  if df.empty: return df, pd.DataFrame(), "⚠️ No data found."
300
 
301
- # 2. Enrich top 20 for preview speed
302
  df_preview = df.copy()
303
  df_preview = enrich_leads_with_visits(df_preview, start, end, max_rows=20)
304
-
305
- # 3. Apply New Business Logic (Campaign/Quality)
306
  df_preview = apply_business_logic(df_preview)
307
 
308
  status = f"✅ Loaded **{len(df):,} companies**. Preview shows top 20 rows enriched."
@@ -312,10 +311,7 @@ def download_full_excel(df, start, end):
312
  if df is None or df.empty: return None
313
  print("⏳ Starting full enrichment for Excel export...")
314
 
315
- # 1. Enrich ALL rows (Time consuming but necessary for Campaign logic)
316
  enriched_df = enrich_leads_with_visits(df.copy(), start, end)
317
-
318
- # 2. Apply Business Logic
319
  final_df = apply_business_logic(enriched_df)
320
 
321
  path = "/tmp/leadfeeder_campaign_data.xlsx"
@@ -336,8 +332,8 @@ def inspect_raw_json(lead_id, start_date, end_date):
336
  # 5. CHART ENGINES
337
  # =====================================================
338
  def build_kpis(df):
339
- if df is None or df.empty: return 0, 0, 0, 0, 0, 0
340
- # Apply logic if not present
341
  if "Quality_Group" not in df.columns:
342
  df = apply_business_logic(df)
343
 
@@ -347,21 +343,60 @@ def build_kpis(df):
347
  int(df["total_visits"].sum()),
348
  round(df["lead_quality_score"].mean(), 2),
349
  round(df["crm_organization_id"].notna().mean() * 100, 1),
350
- df[df["Quality_Group"] == "High Quality (8-10)"].shape[0], # Count of High Quality
 
351
  )
352
 
353
  def build_dashboard(df, preset, top_n):
354
  if df is None or df.empty: return px.bar(title="No Data")
355
 
356
- # Ensure columns exist
357
- if "Campaign" not in df.columns or "Quality_Group" not in df.columns:
358
- # Warn if viewing campaign data without full enrichment
359
- if "Campaign" in preset:
360
- return px.bar(title="⚠️ Please Download Excel first to enrich Campaigns for all rows.")
361
- df = apply_business_logic(df)
362
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
363
  dim, metric, agg = DASHBOARD_PRESETS[preset]
364
-
365
  if agg == "count":
366
  grouped = df.groupby(dim, dropna=False).size().reset_index(name="value")
367
  else:
@@ -375,41 +410,40 @@ def get_trend_filter_options(df, preset):
375
  metric, agg, segment = TREND_PRESETS[preset]
376
  if not segment: return gr.update(choices=[], value=None, visible=False)
377
 
378
- # Ensure logic applied
379
  if segment not in df.columns: df = apply_business_logic(df)
380
 
381
  options = sorted(df[segment].astype(str).unique().tolist())
382
  return gr.update(choices=options, value=options[:5], visible=True, label=f"Filter {segment}")
383
 
384
  def build_trend(df, preset, grain, filter_values):
385
- if df is None or df.empty: return px.line(title="No Data Loaded")
386
 
387
  metric, agg, segment = TREND_PRESETS[preset]
388
- df = df.dropna(subset=["last_visit_date"]).copy()
389
 
390
  if segment:
391
- if segment not in df.columns: df = apply_business_logic(df)
392
  if filter_values:
393
- df = df[df[segment].isin(filter_values)]
394
 
395
  if grain == "Weekly":
396
- df["period"] = df["last_visit_date"].dt.to_period("W").astype(str)
397
  elif grain == "Monthly":
398
- df["period"] = df["last_visit_date"].dt.to_period("M").astype(str)
399
  else:
400
- df["period"] = df["last_visit_date"].dt.date
401
 
402
  val = metric
403
  if agg == "count":
404
- df["_v"] = 1
405
  val = "_v"
406
 
407
  if segment:
408
- ts = df.groupby(["period", segment])[val].agg(agg).reset_index()
409
- return px.line(ts, x="period", y=val, color=segment, template="plotly_white", title=preset)
410
 
411
- ts = df.groupby("period")[val].agg(agg).reset_index()
412
- return px.line(ts, x="period", y=val, markers=True, template="plotly_white", title=preset)
413
 
414
  # =====================================================
415
  # 6. UI LAYOUT
@@ -442,12 +476,12 @@ with gr.Blocks(title="Leadfeeder Campaign Pro") as demo:
442
 
443
  # --- TAB 2: DASHBOARD ---
444
  with gr.Tab("📊 Dashboard"):
445
- kpis = [gr.Number(label=l) for l in ["Total Companies", "Active Companies", "Total Visits", "Avg Quality", "CRM %", "High Quality Leads (8-10)"]]
446
  gr.Button("Refresh KPIs").click(build_kpis, df_state, kpis)
447
 
448
  with gr.Row():
449
- preset = gr.Dropdown(choices=list(DASHBOARD_PRESETS.keys()), label="Chart", value="Visits by Campaign")
450
- top_n = gr.Slider(5, 50, value=10, label="Top N")
451
 
452
  chart = gr.Plot()
453
  gr.Button("Build View").click(build_dashboard, [df_state, preset, top_n], chart)
@@ -464,17 +498,13 @@ with gr.Blocks(title="Leadfeeder Campaign Pro") as demo:
464
  trend_view.change(get_trend_filter_options, [df_state, trend_view], filter_dropdown)
465
  gr.Button("Show Trend", variant="primary").click(build_trend, [df_state, trend_view, grain, filter_dropdown], plot)
466
 
467
- # --- TAB 4: SETTINGS (NEW) ---
468
  with gr.Tab("⚙️ Campaign Settings"):
469
  gr.Markdown("### Manage Campaign Groups & Keywords")
470
- gr.Markdown("Modify the JSON below to add new groups or keywords. Click 'Save' to apply changes.")
471
-
472
- # Load current rules into text box
473
  current_rules = json.dumps(load_campaign_rules(), indent=4)
474
  config_editor = gr.Code(value=current_rules, language="json", label="Campaign Configuration")
475
  save_config_btn = gr.Button("💾 Save Configuration")
476
  config_status = gr.Markdown()
477
-
478
  save_config_btn.click(save_campaign_rules, config_editor, [config_status, gr.State()])
479
 
480
  # --- TAB 5: DEBUGGER ---
 
3
  import requests
4
  import json
5
  import pandas as pd
6
+ import math
7
  import gradio as gr
8
  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
 
13
  # =====================================================
 
32
  CAMPAIGN_CONFIG_FILE = "campaign_rules.json"
33
 
34
  # =====================================================
35
+ # 1. CAMPAIGN MANAGER & BUSINESS LOGIC
36
  # =====================================================
37
  DEFAULT_CAMPAIGNS = {
38
  "MS Stack": [
 
57
  }
58
 
59
  def load_campaign_rules():
 
60
  if os.path.exists(CAMPAIGN_CONFIG_FILE):
61
  try:
62
  with open(CAMPAIGN_CONFIG_FILE, "r") as f:
 
66
  return DEFAULT_CAMPAIGNS
67
 
68
  def save_campaign_rules(rules_text):
 
69
  try:
70
  rules = json.loads(rules_text)
71
  with open(CAMPAIGN_CONFIG_FILE, "w") as f:
 
75
  return "❌ Error: Invalid JSON format.", None
76
 
77
  def categorize_quality(score):
 
78
  if pd.isna(score): return "Unknown"
79
  try:
80
  s = int(score)
 
86
  return "Unknown"
87
 
88
  def assign_campaign(url, rules):
 
89
  if not url or not isinstance(url, str):
90
  return "Uncategorized"
 
91
  url_lower = url.lower()
92
  for campaign_name, keywords in rules.items():
93
  for kw in keywords:
 
96
  return "Other"
97
 
98
  def apply_business_logic(df):
 
99
  if df is None or df.empty: return df
100
 
101
+ # 1. Quality Group
102
+ if "lead_quality_score" in df.columns:
103
+ df["Quality_Group"] = df["lead_quality_score"].apply(categorize_quality)
104
 
105
+ # 2. Campaign Logic (requires landing_page_path)
106
  rules = load_campaign_rules()
107
+ if "landing_page_path" in df.columns:
108
+ df["Campaign"] = df["landing_page_path"].apply(lambda x: assign_campaign(x, rules))
109
+ else:
110
+ df["Campaign"] = "Uncategorized"
111
+
112
  return df
113
 
114
  # =====================================================
115
+ # 2. PRESETS
116
  # =====================================================
117
  DASHBOARD_PRESETS = {
118
+ "All Campaigns (Multi-Chart)": ("SPECIAL_MULTI", None, None), # DYNAMIC ALL CAMPAIGNS
119
+
120
  "Visits by Campaign": ("Campaign", "total_visits", "sum"),
121
  "Leads by Campaign": ("Campaign", "company_name", "count"),
122
  "Leads by Quality Group": ("Quality_Group", "company_name", "count"),
123
 
 
124
  "Total Visits by Industry": ("primary_industry", "total_visits", "sum"),
125
  "Total Visits by Country": ("country", "total_visits", "sum"),
126
  "Avg Quality by Industry": ("primary_industry", "lead_quality_score", "mean"),
 
128
  "Companies by Country": ("country", "company_name", "count"),
129
  "Visits by City": ("city", "total_visits", "sum"),
130
  "Accounts by Assignee": ("assignee", "company_name", "count"),
 
131
  }
132
 
133
  TREND_PRESETS = {
 
134
  "Visits Trend by Campaign": ("total_visits", "sum", "Campaign"),
135
+ "Leads Trend by Campaign": ("company_name", "count", "Campaign"),
136
  "Visits Trend by Quality": ("total_visits", "sum", "Quality_Group"),
137
 
 
138
  "Total Visits Trend": ("total_visits", "sum", None),
139
  "Active Accounts Trend": ("company_name", "count", None),
140
  "Avg Lead Quality Trend": ("lead_quality_score", "mean", None),
 
144
  }
145
 
146
  # =====================================================
147
+ # 3. API HANDLING (FULL DATA RESTORED)
148
  # =====================================================
149
  def make_request(url, params=None):
150
  retries = 3
 
160
  raise Exception("Max retries exceeded")
161
 
162
  def fetch_basic_leads(start_date, end_date):
163
+ """ Fetch ALL company details (Restored all original fields). """
164
  page = 1
165
  rows = []
166
  print(f"🚀 Fetching full company list for {start_date} to {end_date}...")
 
200
  "lead_quality_score": a.get("quality"),
201
  "revenue": a.get("revenue"),
202
  "employee_count": a.get("employee_count"),
203
+ "employees_min": a.get("employees_range", {}).get("min") if a.get("employees_range") else None,
204
+ "employees_max": a.get("employees_range", {}).get("max") if a.get("employees_range") else None,
205
  "assignee": a.get("assignee"),
206
  "emailed_to": a.get("emailed_to"),
207
+ "crm_lead_id": a.get("crm_lead_id"),
208
  "crm_organization_id": a.get("crm_organization_id"),
209
  "tags": ", ".join(a.get("tags", [])) if a.get("tags") else None,
210
  "linkedin_url": a.get("linkedin_url"),
211
+ "twitter_handle": a.get("twitter_handle"),
212
+ "facebook_url": a.get("facebook_url"),
213
  "country": loc.get("country"),
214
  "region": loc.get("region"),
215
  "city": loc.get("city"),
216
  "leadfeeder_url": a.get("view_in_leadfeeder"),
217
+ "landing_page_path": None, # Init for Enrichment
 
218
  "exit_page_path": None
219
  })
220
  print(f"✅ Page {page} loaded. Rows: {len(rows)}")
 
229
  return df
230
 
231
  def enrich_leads_with_visits(df, start_date, end_date, max_rows=None, progress=gr.Progress()):
232
+ """ Step 2: Deep enrich (Restored full logic). """
233
  if df.empty: return df
234
  target_df = df.head(max_rows) if max_rows else df
235
  total = len(target_df)
 
293
  return df
294
 
295
  # =====================================================
296
+ # 4. WRAPPERS
297
  # =====================================================
298
  def load_preview(start, end):
 
299
  df = fetch_basic_leads(start, end)
300
  if df.empty: return df, pd.DataFrame(), "⚠️ No data found."
301
 
302
+ # Enrich top 20 for preview speed
303
  df_preview = df.copy()
304
  df_preview = enrich_leads_with_visits(df_preview, start, end, max_rows=20)
 
 
305
  df_preview = apply_business_logic(df_preview)
306
 
307
  status = f"✅ Loaded **{len(df):,} companies**. Preview shows top 20 rows enriched."
 
311
  if df is None or df.empty: return None
312
  print("⏳ Starting full enrichment for Excel export...")
313
 
 
314
  enriched_df = enrich_leads_with_visits(df.copy(), start, end)
 
 
315
  final_df = apply_business_logic(enriched_df)
316
 
317
  path = "/tmp/leadfeeder_campaign_data.xlsx"
 
332
  # 5. CHART ENGINES
333
  # =====================================================
334
  def build_kpis(df):
335
+ if df is None or df.empty: return 0, 0, 0, 0, 0, 0, 0
336
+
337
  if "Quality_Group" not in df.columns:
338
  df = apply_business_logic(df)
339
 
 
343
  int(df["total_visits"].sum()),
344
  round(df["lead_quality_score"].mean(), 2),
345
  round(df["crm_organization_id"].notna().mean() * 100, 1),
346
+ round(df["linkedin_url"].notna().mean() * 100, 1), # Restored LinkedIn %
347
+ df[df["Quality_Group"] == "High Quality (8-10)"].shape[0],
348
  )
349
 
350
  def build_dashboard(df, preset, top_n):
351
  if df is None or df.empty: return px.bar(title="No Data")
352
 
353
+ if "Campaign" not in df.columns: df = apply_business_logic(df)
 
 
 
 
 
354
 
355
+ # --- SPECIAL MULTI-CHART FOR ALL CAMPAIGNS ---
356
+ if preset == "All Campaigns (Multi-Chart)":
357
+
358
+ # 1. Get ALL campaigns present in data
359
+ campaigns = df["Campaign"].dropna().unique().tolist()
360
+ if not campaigns: return px.bar(title="No Campaigns Found")
361
+
362
+ # Sort them (maybe put 'Other' last)
363
+ campaigns.sort(key=lambda x: (x == "Other", x))
364
+
365
+ num_cams = len(campaigns)
366
+ cols = 2 # 2 columns wide grid
367
+ rows = math.ceil(num_cams / cols)
368
+
369
+ fig = make_subplots(
370
+ rows=rows, cols=cols,
371
+ subplot_titles=campaigns,
372
+ vertical_spacing=0.1
373
+ )
374
+
375
+ for i, camp in enumerate(campaigns):
376
+ # Calculate grid position
377
+ r = (i // cols) + 1
378
+ c = (i % cols) + 1
379
+
380
+ # Filter and Get Top Companies for this campaign
381
+ camp_df = df[df["Campaign"] == camp]
382
+ grouped = camp_df.groupby("company_name")["total_visits"].sum().reset_index()
383
+ grouped = grouped.sort_values("total_visits", ascending=False).head(top_n)
384
+
385
+ # Add Bar Trace
386
+ fig.add_trace(
387
+ go.Bar(x=grouped["company_name"], y=grouped["total_visits"], name=camp),
388
+ row=r, col=c
389
+ )
390
+
391
+ fig.update_layout(
392
+ title_text=f"Top Companies by Campaign (All {num_cams})",
393
+ height=300 * rows, # Dynamic height
394
+ showlegend=False
395
+ )
396
+ return fig
397
+
398
+ # --- STANDARD CHARTS ---
399
  dim, metric, agg = DASHBOARD_PRESETS[preset]
 
400
  if agg == "count":
401
  grouped = df.groupby(dim, dropna=False).size().reset_index(name="value")
402
  else:
 
410
  metric, agg, segment = TREND_PRESETS[preset]
411
  if not segment: return gr.update(choices=[], value=None, visible=False)
412
 
 
413
  if segment not in df.columns: df = apply_business_logic(df)
414
 
415
  options = sorted(df[segment].astype(str).unique().tolist())
416
  return gr.update(choices=options, value=options[:5], visible=True, label=f"Filter {segment}")
417
 
418
  def build_trend(df, preset, grain, filter_values):
419
+ if df is None or df.empty: return px.line(title="No Data")
420
 
421
  metric, agg, segment = TREND_PRESETS[preset]
422
+ df_t = df.dropna(subset=["last_visit_date"]).copy()
423
 
424
  if segment:
425
+ if segment not in df_t.columns: df_t = apply_business_logic(df_t)
426
  if filter_values:
427
+ df_t = df_t[df_t[segment].isin(filter_values)]
428
 
429
  if grain == "Weekly":
430
+ df_t["period"] = df_t["last_visit_date"].dt.to_period("W").astype(str)
431
  elif grain == "Monthly":
432
+ df_t["period"] = df_t["last_visit_date"].dt.to_period("M").astype(str)
433
  else:
434
+ df_t["period"] = df_t["last_visit_date"].dt.date
435
 
436
  val = metric
437
  if agg == "count":
438
+ df_t["_v"] = 1
439
  val = "_v"
440
 
441
  if segment:
442
+ ts = df_t.groupby(["period", segment])[val].agg(agg).reset_index()
443
+ return px.line(ts, x="period", y=val, color=segment, title=preset)
444
 
445
+ ts = df_t.groupby("period")[val].agg(agg).reset_index()
446
+ return px.line(ts, x="period", y=val, markers=True, title=preset)
447
 
448
  # =====================================================
449
  # 6. UI LAYOUT
 
476
 
477
  # --- TAB 2: DASHBOARD ---
478
  with gr.Tab("📊 Dashboard"):
479
+ kpis = [gr.Number(label=l) for l in ["Total Companies", "Active Companies", "Total Visits", "Avg Quality", "CRM %", "LinkedIn %", "High Quality (8-10)"]]
480
  gr.Button("Refresh KPIs").click(build_kpis, df_state, kpis)
481
 
482
  with gr.Row():
483
+ preset = gr.Dropdown(choices=list(DASHBOARD_PRESETS.keys()), label="Chart", value="All Campaigns (Multi-Chart)")
484
+ top_n = gr.Slider(5, 50, value=5, label="Top N per Chart")
485
 
486
  chart = gr.Plot()
487
  gr.Button("Build View").click(build_dashboard, [df_state, preset, top_n], chart)
 
498
  trend_view.change(get_trend_filter_options, [df_state, trend_view], filter_dropdown)
499
  gr.Button("Show Trend", variant="primary").click(build_trend, [df_state, trend_view, grain, filter_dropdown], plot)
500
 
501
+ # --- TAB 4: SETTINGS ---
502
  with gr.Tab("⚙️ Campaign Settings"):
503
  gr.Markdown("### Manage Campaign Groups & Keywords")
 
 
 
504
  current_rules = json.dumps(load_campaign_rules(), indent=4)
505
  config_editor = gr.Code(value=current_rules, language="json", label="Campaign Configuration")
506
  save_config_btn = gr.Button("💾 Save Configuration")
507
  config_status = gr.Markdown()
 
508
  save_config_btn.click(save_campaign_rules, config_editor, [config_status, gr.State()])
509
 
510
  # --- TAB 5: DEBUGGER ---