mktgtech commited on
Commit
ed14c6f
Β·
verified Β·
1 Parent(s): fe7cce0

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +118 -62
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
  # =====================================================
@@ -85,67 +88,77 @@ def get_campaign_match(text, rules):
85
  def apply_business_logic(df):
86
  if df is None or df.empty: return df
87
 
 
88
  if "lead_quality_score" in df.columns:
89
  df["Quality_Group"] = df["lead_quality_score"].apply(categorize_quality)
90
-
 
91
  rules = load_campaign_rules()
92
 
93
  def resolve_campaign(row):
94
  path = row.get("landing_page_path")
95
- # Strictly check landing page to avoid generic categorizations
 
96
  if path and path not in ["", "/", "/home"]:
97
  match = get_campaign_match(path, rules)
98
- if match:
99
  return match
 
100
  return "Uncategorized"
101
-
102
  df["Campaign"] = df.apply(resolve_campaign, axis=1)
103
  return df
104
 
105
  # =====================================================
106
- # 2. PRESETS (Original structure preserved)
107
  # =====================================================
108
  DASHBOARD_PRESETS = {
109
- # --- Added New Presets ---
 
 
 
 
110
  "Visits by Campaign": ("Campaign", "total_visits", "sum"),
111
  "Leads by Campaign": ("Campaign", "company_name", "count"),
112
  "Leads by Quality Group": ("Quality_Group", "company_name", "count"),
 
 
 
 
113
  "Visits by City": ("city", "total_visits", "sum"),
114
  "Leads by City": ("city", "company_name", "count"),
115
- "Leads by Country": ("country", "company_name", "count"),
 
 
116
  "Leads by Industry": ("primary_industry", "company_name", "count"),
117
 
118
- # --- Original Presets ---
119
- "Total Visits by Industry": ("primary_industry", "total_visits", "sum"),
120
- "Total Visits by Country": ("country", "total_visits", "sum"),
121
- "Avg Quality by Industry": ("primary_industry", "lead_quality_score", "mean"),
122
  "Top Accounts by Visits": ("company_name", "total_visits", "sum"),
123
- "Companies by Country": ("country", "company_name", "count"),
124
- "Accounts by Assignee": ("assignee", "company_name", "count"),
125
- "Visits by Assignee": ("assignee", "total_visits", "sum"),
126
  }
127
 
128
  TREND_PRESETS = {
129
- # --- Added New Presets ---
130
  "Visits Trend by Campaign": ("total_visits", "sum", "Campaign"),
131
  "Leads Trend by Campaign": ("company_name", "count", "Campaign"),
132
  "Visits Trend by Quality": ("total_visits", "sum", "Quality_Group"),
 
 
 
 
133
  "Visits Trend by City": ("total_visits", "sum", "city"),
134
  "Leads Trend by City": ("company_name", "count", "city"),
135
- "Leads Trend by Country": ("company_name", "count", "country"),
 
 
136
  "Leads Trend by Industry": ("company_name", "count", "primary_industry"),
137
 
138
- # --- Original Presets ---
139
  "Total Visits Trend": ("total_visits", "sum", None),
140
  "Active Accounts Trend": ("company_name", "count", None),
141
- "Avg Lead Quality Trend": ("lead_quality_score", "mean", None),
142
- "Visits Trend by Industry": ("total_visits", "sum", "primary_industry"),
143
- "Visits Trend by Country": ("total_visits", "sum", "country"),
144
- "Visits Trend by Assignee": ("total_visits", "sum", "assignee"),
145
  }
146
 
147
  # =====================================================
148
- # 3. API HANDLING (Original structure preserved)
149
  # =====================================================
150
  def make_request(url, params=None):
151
  retries = 3
@@ -253,7 +266,7 @@ def enrich_leads_with_visits(df, start_date, end_date, max_rows=None, progress=g
253
  if not landing:
254
  first_step = visit_route[0]
255
  landing = first_step.get("page_path") or first_step.get("page_url")
256
-
257
  if not landing or not exit_p:
258
  pv_map = {p["id"]: p["attributes"] for p in included if p["type"] == "page_views"}
259
  pv_ids = [r["id"] for r in visit.get("relationships", {}).get("page_views", {}).get("data", [])]
@@ -264,14 +277,15 @@ def enrich_leads_with_visits(df, start_date, end_date, max_rows=None, progress=g
264
  if not exit_p:
265
  last_pv = pv_map.get(pv_ids[-1])
266
  if last_pv: exit_p = last_pv.get("url") or last_pv.get("path")
267
-
268
  df.at[index, "landing_page_path"] = landing
269
  df.at[index, "exit_page_path"] = exit_p
270
  except Exception as e:
271
  print(f"Failed to enrich lead {lead_id}: {e}")
272
-
273
  if max_rows and index % 5 == 0:
274
  progress(index / total, desc="Enriching...")
 
275
  return df
276
 
277
  # =====================================================
@@ -305,10 +319,11 @@ def inspect_raw_json(lead_id, start_date, end_date):
305
  return str(e)
306
 
307
  # =====================================================
308
- # 5. CHART ENGINES (Restored to exact original logic)
309
  # =====================================================
310
  def build_kpis(df):
311
- if df is None or df.empty: return 0, 0, 0, 0, 0, 0
 
312
  return (
313
  len(df),
314
  df["total_visits"].gt(0).sum(),
@@ -316,60 +331,92 @@ def build_kpis(df):
316
  round(df["lead_quality_score"].mean(), 2),
317
  round(df["crm_organization_id"].notna().mean() * 100, 1),
318
  round(df["linkedin_url"].notna().mean() * 100, 1),
 
319
  )
320
 
321
  def build_dashboard(df, preset, top_n):
322
  if df is None or df.empty: return px.bar(title="No Data")
323
  if "Campaign" not in df.columns: df = apply_business_logic(df)
324
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
325
  dim, metric, agg = DASHBOARD_PRESETS[preset]
326
  if agg == "count":
327
  grouped = df.groupby(dim, dropna=False).size().reset_index(name="value")
328
  else:
329
  grouped = df.groupby(dim, dropna=False)[metric].agg(agg).reset_index(name="value")
330
- return px.bar(grouped.sort_values("value", ascending=False).head(top_n), x=dim, y="value", title=preset)
331
 
332
  def build_trend(df, preset, grain, filter_values):
333
- if df is None or df.empty: return px.line(title="No Data Loaded")
334
- if "Campaign" not in df.columns: df = apply_business_logic(df)
335
-
336
  metric, agg, segment = TREND_PRESETS[preset]
337
- df = df.dropna(subset=["last_visit_date"]).copy()
 
 
 
 
 
 
338
 
339
- if segment and filter_values:
340
- df = df[df[segment].isin(filter_values)]
341
- if df.empty: return px.line(title="No data for selected filters")
342
-
343
  if grain == "Weekly":
344
- df["period"] = df["last_visit_date"].dt.to_period("W").astype(str)
345
- elif grain == "Monthly":
346
- df["period"] = df["last_visit_date"].dt.to_period("M").astype(str)
347
  else:
348
- df["period"] = df["last_visit_date"].dt.date
349
-
350
  val = metric
351
  if agg == "count":
352
- df["_v"] = df[metric].notna().astype(int)
353
  val = "_v"
354
-
355
  if segment:
356
- ts = df.groupby(["period", segment])[val].agg(agg).reset_index()
357
- title = f"{preset} (Filtered)" if filter_values else preset
358
- return px.line(ts, x="period", y=val, color=segment, template="plotly_white", title=title)
359
 
360
- ts = df.groupby("period")[val].agg(agg).reset_index()
361
- return px.line(ts, x="period", y=val, markers=True, template="plotly_white", title=preset)
362
 
363
  def get_trend_filter_options(df, preset):
364
  if df is None or df.empty: return gr.update(choices=[], value=None, visible=False)
365
- if "Campaign" not in df.columns: df = apply_business_logic(df)
366
-
367
  metric, agg, segment = TREND_PRESETS[preset]
368
  if not segment: return gr.update(choices=[], value=None, visible=False)
369
 
 
 
 
 
370
  options = sorted(df[segment].astype(str).unique().tolist())
371
- top_5 = df[segment].value_counts().head(5).index.tolist()
372
- return gr.update(choices=options, value=top_5, visible=True, label=f"Filter {segment}")
 
 
 
 
 
 
 
373
 
374
  # =====================================================
375
  # 6. UI LAYOUT
@@ -393,22 +440,23 @@ with gr.Blocks(title="Leadfeeder Campaign Pro") as demo:
393
  btn_load = gr.Button("1. Load Data (Preview)", variant="primary")
394
  btn_dl = gr.Button("2. Enrich & Download Full Excel")
395
  file_dl = gr.File(label="Download Excel")
396
-
397
  table = gr.Dataframe(label="Preview (Top 50 Enriched)", interactive=True)
398
  btn_load.click(load_preview, [start, end], [df_state, table, status])
399
  btn_dl.click(download_full_excel, [df_state, start, end], file_dl)
400
-
401
  # --- TAB 2: DASHBOARD ---
402
  with gr.Tab("πŸ“Š Dashboard"):
403
- kpis = [gr.Number(label=l) for l in ["Total Companies", "Active Companies", "Total Visits", "Avg Quality", "CRM %", "LinkedIn %"]]
404
  gr.Button("Refresh KPIs").click(build_kpis, df_state, kpis)
405
 
 
406
  with gr.Row():
407
- preset = gr.Dropdown(choices=list(DASHBOARD_PRESETS.keys()), label="Chart View", value="Visits by Campaign")
408
  top_n = gr.Slider(5, 50, value=10, label="Top N Items")
409
  chart = gr.Plot()
410
  gr.Button("Build View").click(build_dashboard, [df_state, preset, top_n], chart)
411
-
412
  # --- TAB 3: TRENDS ---
413
  with gr.Tab("πŸ“ˆ Trends"):
414
  with gr.Row():
@@ -418,21 +466,24 @@ with gr.Blocks(title="Leadfeeder Campaign Pro") as demo:
418
  plot = gr.Plot()
419
  trend_view.change(get_trend_filter_options, [df_state, trend_view], filter_dropdown)
420
  gr.Button("Show Trend", variant="primary").click(build_trend, [df_state, trend_view, grain, filter_dropdown], plot)
421
-
422
- # --- TAB 4: SETTINGS ---
423
  with gr.Tab("βš™οΈ Campaign Settings"):
424
  gr.Markdown("### Manage Campaign Groups")
425
- gr.Markdown("Select a campaign below and paste your keywords separated by commas.")
426
 
 
427
  init_rules = load_campaign_rules()
428
  camp_choices = list(init_rules.keys()) + ["+ Create New Campaign"]
 
429
 
430
  with gr.Row():
431
  camp_dropdown = gr.Dropdown(choices=camp_choices, label="Select Campaign to Edit", value=camp_choices[0])
432
  new_camp_name = gr.Textbox(label="New Campaign Name", visible=False)
433
 
434
- kw_input = gr.Textbox(label="Keywords (comma separated)", lines=5, value=", ".join(init_rules.get(camp_choices[0], [])))
435
 
 
436
  def update_ui_on_select(selected_camp):
437
  rules = load_campaign_rules()
438
  if selected_camp == "+ Create New Campaign":
@@ -446,11 +497,16 @@ with gr.Blocks(title="Leadfeeder Campaign Pro") as demo:
446
  save_config_btn = gr.Button("πŸ’Ύ Save Configuration", variant="primary")
447
  config_status = gr.Markdown()
448
 
 
449
  def save_easy_config(selected_camp, new_name, kw_string):
450
  rules = load_campaign_rules()
 
 
451
  raw_kws = kw_string.split(",")
452
  clean_kws = [k.strip().lower() for k in raw_kws if k.strip()]
 
453
  target_camp = new_name.strip() if selected_camp == "+ Create New Campaign" else selected_camp
 
454
  if not target_camp:
455
  return "❌ Error: Campaign name cannot be empty.", gr.update()
456
 
@@ -462,7 +518,7 @@ with gr.Blocks(title="Leadfeeder Campaign Pro") as demo:
462
  return f"βœ… Saved successfully! Updated keywords for '{target_camp}'.", gr.update(choices=updated_choices, value=target_camp)
463
 
464
  save_config_btn.click(save_easy_config, inputs=[camp_dropdown, new_camp_name, kw_input], outputs=[config_status, camp_dropdown])
465
-
466
  # --- TAB 5: DEBUGGER ---
467
  with gr.Tab("πŸ› οΈ Debugger"):
468
  dbg_id = gr.Textbox(label="Lead ID")
 
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
  # =====================================================
 
88
  def apply_business_logic(df):
89
  if df is None or df.empty: return df
90
 
91
+ # 1. Quality Group
92
  if "lead_quality_score" in df.columns:
93
  df["Quality_Group"] = df["lead_quality_score"].apply(categorize_quality)
94
+
95
+ # 2. Campaign Logic (Strict Landing Page Only)
96
  rules = load_campaign_rules()
97
 
98
  def resolve_campaign(row):
99
  path = row.get("landing_page_path")
100
+
101
+ # STRICT MATCHING: Only look at landing page. Skip empty or generic homepages.
102
  if path and path not in ["", "/", "/home"]:
103
  match = get_campaign_match(path, rules)
104
+ if match:
105
  return match
106
+
107
  return "Uncategorized"
108
+
109
  df["Campaign"] = df.apply(resolve_campaign, axis=1)
110
  return df
111
 
112
  # =====================================================
113
+ # 2. PRESETS
114
  # =====================================================
115
  DASHBOARD_PRESETS = {
116
+ # -- SPECIAL VIEWS --
117
+ "Key Campaigns Bifurcation (MS Tech, Fintech, Adtech)": ("SPECIAL_KEY_BIFURCATION", None, None),
118
+ "All Campaigns Performance": ("SPECIAL_ALL_PERFORMANCE", None, None),
119
+
120
+ # -- CAMPAIGN & QUALITY --
121
  "Visits by Campaign": ("Campaign", "total_visits", "sum"),
122
  "Leads by Campaign": ("Campaign", "company_name", "count"),
123
  "Leads by Quality Group": ("Quality_Group", "company_name", "count"),
124
+
125
+ # -- GEOGRAPHY --
126
+ "Visits by Country": ("country", "total_visits", "sum"),
127
+ "Leads by Country": ("country", "company_name", "count"),
128
  "Visits by City": ("city", "total_visits", "sum"),
129
  "Leads by City": ("city", "company_name", "count"),
130
+
131
+ # -- INDUSTRY --
132
+ "Visits by Industry": ("primary_industry", "total_visits", "sum"),
133
  "Leads by Industry": ("primary_industry", "company_name", "count"),
134
 
135
+ # -- OTHERS --
 
 
 
136
  "Top Accounts by Visits": ("company_name", "total_visits", "sum"),
 
 
 
137
  }
138
 
139
  TREND_PRESETS = {
140
+ # -- CAMPAIGN & QUALITY --
141
  "Visits Trend by Campaign": ("total_visits", "sum", "Campaign"),
142
  "Leads Trend by Campaign": ("company_name", "count", "Campaign"),
143
  "Visits Trend by Quality": ("total_visits", "sum", "Quality_Group"),
144
+
145
+ # -- GEOGRAPHY --
146
+ "Visits Trend by Country": ("total_visits", "sum", "country"),
147
+ "Leads Trend by Country": ("company_name", "count", "country"),
148
  "Visits Trend by City": ("total_visits", "sum", "city"),
149
  "Leads Trend by City": ("company_name", "count", "city"),
150
+
151
+ # -- INDUSTRY --
152
+ "Visits Trend by Industry": ("total_visits", "sum", "primary_industry"),
153
  "Leads Trend by Industry": ("company_name", "count", "primary_industry"),
154
 
155
+ # -- GENERAL --
156
  "Total Visits Trend": ("total_visits", "sum", None),
157
  "Active Accounts Trend": ("company_name", "count", None),
 
 
 
 
158
  }
159
 
160
  # =====================================================
161
+ # 3. API HANDLING
162
  # =====================================================
163
  def make_request(url, params=None):
164
  retries = 3
 
266
  if not landing:
267
  first_step = visit_route[0]
268
  landing = first_step.get("page_path") or first_step.get("page_url")
269
+
270
  if not landing or not exit_p:
271
  pv_map = {p["id"]: p["attributes"] for p in included if p["type"] == "page_views"}
272
  pv_ids = [r["id"] for r in visit.get("relationships", {}).get("page_views", {}).get("data", [])]
 
277
  if not exit_p:
278
  last_pv = pv_map.get(pv_ids[-1])
279
  if last_pv: exit_p = last_pv.get("url") or last_pv.get("path")
280
+
281
  df.at[index, "landing_page_path"] = landing
282
  df.at[index, "exit_page_path"] = exit_p
283
  except Exception as e:
284
  print(f"Failed to enrich lead {lead_id}: {e}")
285
+
286
  if max_rows and index % 5 == 0:
287
  progress(index / total, desc="Enriching...")
288
+
289
  return df
290
 
291
  # =====================================================
 
319
  return str(e)
320
 
321
  # =====================================================
322
+ # 5. CHART ENGINES
323
  # =====================================================
324
  def build_kpis(df):
325
+ if df is None or df.empty: return 0, 0, 0, 0, 0, 0, 0
326
+ if "Quality_Group" not in df.columns: df = apply_business_logic(df)
327
  return (
328
  len(df),
329
  df["total_visits"].gt(0).sum(),
 
331
  round(df["lead_quality_score"].mean(), 2),
332
  round(df["crm_organization_id"].notna().mean() * 100, 1),
333
  round(df["linkedin_url"].notna().mean() * 100, 1),
334
+ df[df["Quality_Group"] == "High Quality (8-10)"].shape[0],
335
  )
336
 
337
  def build_dashboard(df, preset, top_n):
338
  if df is None or df.empty: return px.bar(title="No Data")
339
  if "Campaign" not in df.columns: df = apply_business_logic(df)
340
 
341
+ if preset == "Key Campaigns Bifurcation (MS Tech, Fintech, Adtech)":
342
+ target = ["MS Tech", "Fintech", "Adtech"]
343
+ filtered = df[df["Campaign"].isin(target)].copy()
344
+ if filtered.empty: return px.bar(title="No Data for Key Campaigns")
345
+ agg = filtered.groupby("Campaign").agg(
346
+ Leads_Count=("company_name", "count"),
347
+ Total_Visits=("total_visits", "sum")
348
+ ).reset_index()
349
+ fig = go.Figure()
350
+ fig.add_trace(go.Bar(x=agg["Campaign"], y=agg["Leads_Count"], name="No. of Leads", marker_color="#00C49F"))
351
+ fig.add_trace(go.Bar(x=agg["Campaign"], y=agg["Total_Visits"], name="Total Visits", marker_color="#FFBB28"))
352
+ fig.update_layout(title="Key Campaigns: Leads vs Visits", barmode='group')
353
+ return fig
354
+
355
+ if preset == "All Campaigns Performance":
356
+ agg = df.groupby("Campaign").agg(
357
+ Leads_Count=("company_name", "count"),
358
+ Total_Visits=("total_visits", "sum")
359
+ ).reset_index().sort_values("Leads_Count", ascending=False)
360
+ fig = make_subplots(specs=[[{"secondary_y": True}]])
361
+ fig.add_trace(go.Bar(x=agg["Campaign"], y=agg["Leads_Count"], name="Leads", marker_color="indigo"), secondary_y=False)
362
+ fig.add_trace(go.Scatter(x=agg["Campaign"], y=agg["Total_Visits"], name="Visits", mode="lines+markers", line=dict(color="orange", width=3)), secondary_y=True)
363
+ fig.update_layout(title_text="All Campaigns Performance")
364
+ return fig
365
+
366
  dim, metric, agg = DASHBOARD_PRESETS[preset]
367
  if agg == "count":
368
  grouped = df.groupby(dim, dropna=False).size().reset_index(name="value")
369
  else:
370
  grouped = df.groupby(dim, dropna=False)[metric].agg(agg).reset_index(name="value")
371
+ return px.bar(grouped.sort_values("value", ascending=False).head(top_n), x=dim, y="value", title=preset, color=dim)
372
 
373
  def build_trend(df, preset, grain, filter_values):
374
+ if df is None or df.empty: return px.line(title="No Data")
 
 
375
  metric, agg, segment = TREND_PRESETS[preset]
376
+ df_t = df.dropna(subset=["last_visit_date"]).copy()
377
+
378
+ if segment and segment not in df_t.columns:
379
+ df_t = apply_business_logic(df_t)
380
+
381
+ if segment and filter_values:
382
+ df_t = df_t[df_t[segment].isin(filter_values)]
383
 
 
 
 
 
384
  if grain == "Weekly":
385
+ df_t["period"] = df_t["last_visit_date"].dt.to_period("W").astype(str)
 
 
386
  else:
387
+ df_t["period"] = df_t["last_visit_date"].dt.date
388
+
389
  val = metric
390
  if agg == "count":
391
+ df_t["_v"] = 1
392
  val = "_v"
393
+
394
  if segment:
395
+ ts = df_t.groupby(["period", segment])[val].agg(agg).reset_index()
396
+ return px.line(ts, x="period", y=val, color=segment, title=preset, markers=True)
 
397
 
398
+ ts = df_t.groupby("period")[val].agg(agg).reset_index()
399
+ return px.line(ts, x="period", y=val, markers=True, title=preset)
400
 
401
  def get_trend_filter_options(df, preset):
402
  if df is None or df.empty: return gr.update(choices=[], value=None, visible=False)
 
 
403
  metric, agg, segment = TREND_PRESETS[preset]
404
  if not segment: return gr.update(choices=[], value=None, visible=False)
405
 
406
+ # FORCE LOGIC
407
+ if segment not in df.columns:
408
+ df = apply_business_logic(df)
409
+
410
  options = sorted(df[segment].astype(str).unique().tolist())
411
+
412
+ # Smart Defaults for specific columns
413
+ if segment == "Campaign":
414
+ defaults = [o for o in options if o in ["MS Tech", "Fintech", "Adtech"]]
415
+ else:
416
+ defaults = options[:5] # Top 5 for City/Country/Industry
417
+
418
+ if not defaults: defaults = options[:5]
419
+ return gr.update(choices=options, value=defaults, visible=True, label=f"Filter {segment}")
420
 
421
  # =====================================================
422
  # 6. UI LAYOUT
 
440
  btn_load = gr.Button("1. Load Data (Preview)", variant="primary")
441
  btn_dl = gr.Button("2. Enrich & Download Full Excel")
442
  file_dl = gr.File(label="Download Excel")
443
+
444
  table = gr.Dataframe(label="Preview (Top 50 Enriched)", interactive=True)
445
  btn_load.click(load_preview, [start, end], [df_state, table, status])
446
  btn_dl.click(download_full_excel, [df_state, start, end], file_dl)
447
+
448
  # --- TAB 2: DASHBOARD ---
449
  with gr.Tab("πŸ“Š Dashboard"):
450
+ kpis = [gr.Number(label=l) for l in ["Companies", "Active", "Visits", "Avg Quality", "CRM %", "LinkedIn %", "High Quality (8-10)"]]
451
  gr.Button("Refresh KPIs").click(build_kpis, df_state, kpis)
452
 
453
+ gr.Markdown("### πŸ“Š Charts")
454
  with gr.Row():
455
+ preset = gr.Dropdown(choices=list(DASHBOARD_PRESETS.keys()), label="Chart View", value="Key Campaigns Bifurcation (MS Tech, Fintech, Adtech)")
456
  top_n = gr.Slider(5, 50, value=10, label="Top N Items")
457
  chart = gr.Plot()
458
  gr.Button("Build View").click(build_dashboard, [df_state, preset, top_n], chart)
459
+
460
  # --- TAB 3: TRENDS ---
461
  with gr.Tab("πŸ“ˆ Trends"):
462
  with gr.Row():
 
466
  plot = gr.Plot()
467
  trend_view.change(get_trend_filter_options, [df_state, trend_view], filter_dropdown)
468
  gr.Button("Show Trend", variant="primary").click(build_trend, [df_state, trend_view, grain, filter_dropdown], plot)
469
+
470
+ # --- TAB 4: SETTINGS (EASY TEXT-BASED UI) ---
471
  with gr.Tab("βš™οΈ Campaign Settings"):
472
  gr.Markdown("### Manage Campaign Groups")
473
+ gr.Markdown("Select a campaign below and paste your keywords separated by commas. Changes take effect on the next data load.")
474
 
475
+ # Initial Load
476
  init_rules = load_campaign_rules()
477
  camp_choices = list(init_rules.keys()) + ["+ Create New Campaign"]
478
+ default_kws = ", ".join(init_rules.get(camp_choices[0], [])) if init_rules else ""
479
 
480
  with gr.Row():
481
  camp_dropdown = gr.Dropdown(choices=camp_choices, label="Select Campaign to Edit", value=camp_choices[0])
482
  new_camp_name = gr.Textbox(label="New Campaign Name", visible=False)
483
 
484
+ kw_input = gr.Textbox(label="Keywords (comma separated)", lines=5, value=default_kws)
485
 
486
+ # Switch views dynamically
487
  def update_ui_on_select(selected_camp):
488
  rules = load_campaign_rules()
489
  if selected_camp == "+ Create New Campaign":
 
497
  save_config_btn = gr.Button("πŸ’Ύ Save Configuration", variant="primary")
498
  config_status = gr.Markdown()
499
 
500
+ # Save Logic
501
  def save_easy_config(selected_camp, new_name, kw_string):
502
  rules = load_campaign_rules()
503
+
504
+ # Split by comma and clean whitespace
505
  raw_kws = kw_string.split(",")
506
  clean_kws = [k.strip().lower() for k in raw_kws if k.strip()]
507
+
508
  target_camp = new_name.strip() if selected_camp == "+ Create New Campaign" else selected_camp
509
+
510
  if not target_camp:
511
  return "❌ Error: Campaign name cannot be empty.", gr.update()
512
 
 
518
  return f"βœ… Saved successfully! Updated keywords for '{target_camp}'.", gr.update(choices=updated_choices, value=target_camp)
519
 
520
  save_config_btn.click(save_easy_config, inputs=[camp_dropdown, new_camp_name, kw_input], outputs=[config_status, camp_dropdown])
521
+
522
  # --- TAB 5: DEBUGGER ---
523
  with gr.Tab("πŸ› οΈ Debugger"):
524
  dbg_id = gr.Textbox(label="Lead ID")