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

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +474 -0
app.py ADDED
@@ -0,0 +1,474 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ 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
+ # =====================================================
11
+ # CONFIG / SECRETS
12
+ # =====================================================
13
+ API_TOKEN = os.getenv("LEADFEEDER_API_TOKEN")
14
+ APP_PASSWORD = os.getenv("APP_PASSWORD")
15
+
16
+ if not API_TOKEN or not APP_PASSWORD:
17
+ print("⚠️ WARNING: Secrets missing. App will launch but API calls will fail.")
18
+ API_TOKEN = "PLACEHOLDER"
19
+ APP_PASSWORD = "password"
20
+
21
+ ACCOUNT_ID = "255333"
22
+ BASE_URL = "https://api.leadfeeder.com"
23
+ PAGE_SIZE = 100
24
+ HEADERS = {
25
+ "Authorization": f"Token token={API_TOKEN}",
26
+ "Accept": "application/json"
27
+ }
28
+ CAMPAIGN_CONFIG_FILE = "campaign_rules.json"
29
+
30
+ # =====================================================
31
+ # 1. CAMPAIGN MANAGER & LOGIC
32
+ # =====================================================
33
+ DEFAULT_CAMPAIGNS = {
34
+ "MS Tech": [
35
+ "microsoft", "power", "dynamics", "crm", "365", "xamarin", "sharepoint",
36
+ "dot", ".net", "asp.net", "azure", "copilot", "real-estate-website-development",
37
+ "manufacturing-production", "legacy-hrms", "healthcare-appointment", "eld-fleet",
38
+ "legacy-erp", "clinical-trial", "digital-e-learning", "bi-analytics",
39
+ "application-modernization", "data-warehouse", "workforce", "contingent"
40
+ ],
41
+ "Fintech": [
42
+ "fintech", "finance", "money", "credit", "debit", "card", "invest", "bank",
43
+ "wallet", "account", "bookkeeping", "payment", "loan", "nfc", "lend", "debt",
44
+ "tax", "invoice", "quot", "borrow", "bill", "coin", "currency", "aml", "kyc",
45
+ "blockchain", "cryptocurrency", "budget", "expense", "wealth", "sox", "sarbanes"
46
+ ],
47
+ "Adtech": [
48
+ "programmatic", "publisher", "media", "entertainment", "trading", "ott",
49
+ "email", "news", "social", "marketing", "video", "audio", "radio", "livestream",
50
+ "bid", "anime", "dooh", "vod", "rtb", "supply-side-platform", "demand-side-platform",
51
+ "data-management-platform"
52
+ ]
53
+ }
54
+
55
+ def load_campaign_rules():
56
+ if os.path.exists(CAMPAIGN_CONFIG_FILE):
57
+ try:
58
+ with open(CAMPAIGN_CONFIG_FILE, "r") as f:
59
+ return json.load(f)
60
+ except:
61
+ return DEFAULT_CAMPAIGNS
62
+ return DEFAULT_CAMPAIGNS
63
+
64
+ def categorize_quality(score):
65
+ if pd.isna(score): return "Unknown"
66
+ try:
67
+ s = int(score)
68
+ if 8 <= s <= 10: return "High Quality (8-10)"
69
+ if 5 <= s <= 7: return "Mid Quality (5-7)"
70
+ if 1 <= s <= 4: return "Low Quality (1-4)"
71
+ return "Low Quality (0)"
72
+ except:
73
+ return "Unknown"
74
+
75
+ def get_campaign_match(text, rules):
76
+ if not text or not isinstance(text, str):
77
+ return None
78
+ text_lower = text.lower()
79
+ for campaign_name, keywords in rules.items():
80
+ for kw in keywords:
81
+ if kw.lower() in text_lower:
82
+ return campaign_name
83
+ return None
84
+
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
152
+ while retries > 0:
153
+ r = requests.get(url, headers=HEADERS, params=params, timeout=45)
154
+ if r.status_code == 429:
155
+ time.sleep(61)
156
+ retries -= 1
157
+ continue
158
+ r.raise_for_status()
159
+ return r.json()
160
+ raise Exception("Max retries exceeded")
161
+
162
+ def fetch_basic_leads(start_date, end_date):
163
+ page = 1
164
+ rows = []
165
+ print(f"πŸš€ Fetching full company list for {start_date} to {end_date}...")
166
+ while True:
167
+ try:
168
+ js = make_request(
169
+ f"{BASE_URL}/accounts/{ACCOUNT_ID}/leads",
170
+ params={"start_date": start_date, "end_date": end_date, "page[number]": page, "page[size]": PAGE_SIZE, "include": "location"}
171
+ )
172
+ data = js.get("data", [])
173
+ if not data: break
174
+
175
+ included = js.get("included", [])
176
+ loc_map = {str(i["id"]): i["attributes"] for i in included if i["type"] == "locations"}
177
+
178
+ for lead in data:
179
+ a = lead["attributes"]
180
+ loc_id = lead.get("relationships", {}).get("location", {}).get("data", {}).get("id")
181
+ loc = loc_map.get(str(loc_id), {})
182
+
183
+ rows.append({
184
+ "lead_id": lead.get("id"),
185
+ "company_name": a.get("name"),
186
+ "website_url": a.get("website_url"),
187
+ "phone": a.get("phone"),
188
+ "business_id": a.get("business_id"),
189
+ "primary_industry": a.get("industry"),
190
+ "all_industries": ", ".join([i.get("name") for i in a.get("industries", [])]) if a.get("industries") else None,
191
+ "first_visit_date": a.get("first_visit_date"),
192
+ "last_visit_date": a.get("last_visit_date"),
193
+ "total_visits": a.get("visits"),
194
+ "lead_quality_score": a.get("quality"),
195
+ "revenue": a.get("revenue"),
196
+ "employee_count": a.get("employee_count"),
197
+ "employees_min": a.get("employees_range", {}).get("min") if a.get("employees_range") else None,
198
+ "employees_max": a.get("employees_range", {}).get("max") if a.get("employees_range") else None,
199
+ "assignee": a.get("assignee"),
200
+ "emailed_to": a.get("emailed_to"),
201
+ "crm_lead_id": a.get("crm_lead_id"),
202
+ "crm_organization_id": a.get("crm_organization_id"),
203
+ "tags": ", ".join(a.get("tags", [])) if a.get("tags") else None,
204
+ "linkedin_url": a.get("linkedin_url"),
205
+ "twitter_handle": a.get("twitter_handle"),
206
+ "facebook_url": a.get("facebook_url"),
207
+ "country": loc.get("country"),
208
+ "region": loc.get("region"),
209
+ "city": loc.get("city"),
210
+ "leadfeeder_url": a.get("view_in_leadfeeder"),
211
+ "landing_page_path": None,
212
+ "exit_page_path": None
213
+ })
214
+ print(f"βœ… Page {page} loaded. Rows: {len(rows)}")
215
+ page += 1
216
+ except Exception as e:
217
+ print(f"Error on page {page}: {e}")
218
+ break
219
+
220
+ df = pd.DataFrame(rows)
221
+ if not df.empty:
222
+ df["last_visit_date"] = pd.to_datetime(df["last_visit_date"], errors="coerce")
223
+ return df
224
+
225
+ def enrich_leads_with_visits(df, start_date, end_date, max_rows=None, progress=gr.Progress()):
226
+ if df.empty: return df
227
+ target_df = df.head(max_rows) if max_rows else df
228
+ total = len(target_df)
229
+
230
+ print(f"πŸ•΅οΈ Deep enriching {total} rows ({start_date} to {end_date})...")
231
+ for index, row in target_df.iterrows():
232
+ if row.get("landing_page_path"): continue
233
+ lead_id = row["lead_id"]
234
+ try:
235
+ visit_data = make_request(
236
+ f"{BASE_URL}/accounts/{ACCOUNT_ID}/leads/{lead_id}/visits",
237
+ params={"start_date": start_date, "end_date": end_date, "page[size]": 1, "include": "page_views"}
238
+ )
239
+ visits = visit_data.get("data", [])
240
+ included = visit_data.get("included", [])
241
+ landing = None
242
+ exit_p = None
243
+
244
+ if visits:
245
+ visit = visits[0]
246
+ v_attrs = visit.get("attributes", {})
247
+ landing = v_attrs.get("landing_page_path") or v_attrs.get("landing_page_url")
248
+
249
+ visit_route = v_attrs.get("visit_route", [])
250
+ if visit_route and isinstance(visit_route, list):
251
+ last_step = visit_route[-1]
252
+ if not exit_p: exit_p = last_step.get("page_path") or last_step.get("page_url")
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", [])]
260
+ if pv_ids:
261
+ if not landing:
262
+ first_pv = pv_map.get(pv_ids[0])
263
+ if first_pv: landing = first_pv.get("url") or first_pv.get("path")
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
+ # =====================================================
278
+ # 4. WRAPPERS
279
+ # =====================================================
280
+ def load_preview(start, end):
281
+ df = fetch_basic_leads(start, end)
282
+ if df.empty: return df, pd.DataFrame(), "⚠️ No data found."
283
+ df_preview = df.copy()
284
+ df_preview = enrich_leads_with_visits(df_preview, start, end, max_rows=50)
285
+ df_preview = apply_business_logic(df_preview)
286
+ return df, df_preview.head(50), f"βœ… Loaded {len(df):,} companies. Preview top 50."
287
+
288
+ def download_full_excel(df, start, end):
289
+ if df is None or df.empty: return None
290
+ print("⏳ Starting full enrichment for Excel export...")
291
+ enriched_df = enrich_leads_with_visits(df.copy(), start, end)
292
+ final_df = apply_business_logic(enriched_df)
293
+ path = "/tmp/leadfeeder_campaign_data.xlsx"
294
+ final_df.to_excel(path, index=False)
295
+ return path
296
+
297
+ def inspect_raw_json(lead_id, start_date, end_date):
298
+ if not lead_id: return "Please enter a Lead ID"
299
+ try:
300
+ url = f"{BASE_URL}/accounts/{ACCOUNT_ID}/leads/{lead_id}/visits"
301
+ params = {"start_date": start_date, "end_date": end_date, "page[size]": 1, "include": "page_views"}
302
+ r = requests.get(url, headers=HEADERS, params=params)
303
+ return json.dumps(r.json(), indent=2)
304
+ except Exception as e:
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(),
315
+ int(df["total_visits"].sum()),
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
376
+ # =====================================================
377
+ with gr.Blocks(title="Leadfeeder Campaign Pro") as demo:
378
+ gr.Markdown("## πŸš€ Leadfeeder Analytics & Campaign Manager")
379
+
380
+ with gr.Row():
381
+ pwd = gr.Textbox(type="password", label="App Password")
382
+ gr.Button("Auth").click(lambda p: gr.Info("Success") if p==APP_PASSWORD else gr.Error("Invalid"), pwd, None)
383
+ df_state = gr.State()
384
+ status = gr.Markdown()
385
+
386
+ with gr.Tabs():
387
+ # --- TAB 1: DATA ---
388
+ with gr.Tab("πŸ“‹ Data & Report"):
389
+ with gr.Row():
390
+ start = gr.Textbox(label="Start Date", value=(date.today()-timedelta(days=30)).isoformat())
391
+ end = gr.Textbox(label="End Date", value=date.today().isoformat())
392
+ with gr.Row():
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():
415
+ trend_view = gr.Dropdown(choices=list(TREND_PRESETS.keys()), label="Select Trend", value="Visits Trend by Campaign")
416
+ grain = gr.Radio(["Daily", "Weekly", "Monthly"], value="Daily", label="Granularity")
417
+ filter_dropdown = gr.Dropdown(multiselect=True, visible=False, label="Filter Segments")
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":
439
+ return gr.update(visible=True, value=""), gr.update(value="")
440
+ else:
441
+ kws = rules.get(selected_camp, [])
442
+ return gr.update(visible=False), gr.update(value=", ".join(kws))
443
+
444
+ camp_dropdown.change(update_ui_on_select, inputs=[camp_dropdown], outputs=[new_camp_name, kw_input])
445
+
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
+
457
+ rules[target_camp] = clean_kws
458
+ with open(CAMPAIGN_CONFIG_FILE, "w") as f:
459
+ json.dump(rules, f, indent=4)
460
+
461
+ updated_choices = list(rules.keys()) + ["+ Create New Campaign"]
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")
469
+ dbg_btn = gr.Button("Inspect Raw JSON")
470
+ dbg_out = gr.Code(language="json")
471
+ dbg_btn.click(inspect_raw_json, [dbg_id, start, end], dbg_out)
472
+
473
+ if __name__ == "__main__":
474
+ demo.launch(server_name="0.0.0.0", server_port=7860)