ps1811 commited on
Commit
75e9076
Β·
1 Parent(s): b536db8

app.py restored

Browse files
Files changed (1) hide show
  1. app.py +201 -409
app.py CHANGED
@@ -1,5 +1,4 @@
1
  import gradio as gr
2
- import pandas as pd
3
  import spaces
4
 
5
  from app.db.repo import init_db
@@ -8,432 +7,187 @@ from app.controller.session_loader import load_google_ads_data
8
  from app.ads1.ads_analyst import run_ads_analyst_card
9
 
10
 
11
- CSS = """
12
- @import url('https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700&family=Inter:wght@400;500;600;700&display=swap');
13
-
14
- :root {
15
- --advisor-bg: #070708;
16
- --advisor-panel: #101112;
17
- --advisor-panel-soft: rgba(16, 17, 18, 0.82);
18
- --advisor-border: #232426;
19
- --advisor-border-soft: rgba(255, 255, 255, 0.05);
20
- --advisor-text: #e5e2e3;
21
- --advisor-muted: #9a9da3;
22
- --advisor-primary: #5e6bff;
23
- }
24
-
25
- body,
26
- .gradio-container {
27
- background:
28
- radial-gradient(circle at top left, rgba(94, 107, 255, 0.14), transparent 25%),
29
- radial-gradient(circle at top right, rgba(80, 216, 233, 0.08), transparent 20%),
30
- var(--advisor-bg) !important;
31
- color: var(--advisor-text) !important;
32
- font-family: 'Inter', sans-serif !important;
33
- }
34
 
 
35
  .gradio-container {
36
- max-width: 1728px !important;
37
- margin: 0 auto !important;
38
- }
39
-
40
- #advisor-shell {
41
- padding: 24px 32px 40px;
42
  }
43
 
44
  #advisor-header {
45
- display: flex;
46
- align-items: center;
47
- justify-content: space-between;
48
- gap: 16px;
49
- margin-bottom: 24px;
50
- padding-bottom: 18px;
51
- border-bottom: 1px solid var(--advisor-border);
52
- }
53
-
54
- .brand {
55
- display: flex;
56
- align-items: center;
57
- gap: 12px;
58
- }
59
-
60
- .brand-badge {
61
- width: 36px;
62
- height: 36px;
63
- border-radius: 999px;
64
- border: 1px solid #454655;
65
- background: rgba(255, 255, 255, 0.02);
66
- display: flex;
67
- align-items: center;
68
- justify-content: center;
69
- color: var(--advisor-text);
70
- font-weight: 700;
71
  }
72
 
73
  #advisor-title {
74
- font-size: 28px;
75
- font-weight: 700;
76
- letter-spacing: -0.04em;
77
- color: var(--advisor-text);
78
  }
79
 
80
  #advisor-subtitle {
81
- font-size: 14px;
82
- color: var(--advisor-muted);
83
  }
84
 
85
- .layout-shell {
86
- display: grid;
87
- grid-template-columns: minmax(280px, 320px) minmax(0, 1fr);
88
- gap: 20px;
89
  }
90
 
91
- .sidebar-panel,
92
- .main-panel,
93
- .selected-panel,
94
- .output-panel,
95
- .ai-card,
96
- .metric-card {
97
- background: rgba(16, 17, 18, 0.92);
98
- border: 1px solid var(--advisor-border-soft);
99
- border-radius: 16px;
100
- box-shadow: inset 0 1px 0 0 rgba(255, 255, 255, 0.06);
101
  }
102
 
103
- .sidebar-panel {
104
- padding: 18px;
105
- position: sticky;
106
- top: 18px;
107
- height: fit-content;
108
  }
109
 
110
- .panel-title {
111
  font-size: 11px;
112
- text-transform: uppercase;
113
- letter-spacing: 0.18em;
114
- color: rgba(154, 157, 163, 0.72);
115
- margin-bottom: 14px;
116
- }
117
-
118
- #campaign-selector label {
119
- background: transparent !important;
120
- border: 1px solid transparent;
121
- border-radius: 12px;
122
- padding: 10px 12px;
123
- margin-bottom: 6px;
124
- transition: background 0.16s ease, border-color 0.16s ease, color 0.16s ease;
125
  }
126
 
127
- #campaign-selector label:hover {
128
- background: rgba(255, 255, 255, 0.04) !important;
129
- color: var(--advisor-text) !important;
 
130
  }
131
 
132
- #campaign-selector input[type="radio"]:checked + label {
133
- background: rgba(94, 107, 255, 0.16) !important;
134
- border-color: rgba(94, 107, 255, 0.65);
135
- color: #ffffff !important;
 
 
136
  }
137
 
138
- .main-panel {
139
- padding: 22px;
140
- min-height: 720px;
141
- }
142
-
143
- .hero-grid,
144
- .selected-metrics,
145
  .ai-grid {
146
  display: grid;
 
147
  gap: 12px;
 
148
  }
149
 
150
- .hero-grid {
151
- grid-template-columns: repeat(4, minmax(0, 1fr));
152
- margin-bottom: 18px;
153
- }
154
-
155
- .metric-card {
156
- background: rgba(7, 7, 8, 0.72);
157
- padding: 16px;
158
- }
159
-
160
- .metric-label,
161
- .selected-eyebrow,
162
- .panel-kicker {
163
- font-size: 10px;
164
- letter-spacing: 0.18em;
165
- text-transform: uppercase;
166
- color: rgba(154, 157, 163, 0.72);
167
- }
168
-
169
- .metric-label {
170
- margin-bottom: 10px;
171
- }
172
-
173
- .metric-value {
174
- font-family: 'Manrope', sans-serif;
175
- font-size: 26px;
176
- font-weight: 700;
177
- letter-spacing: -0.05em;
178
- color: #e5e2e3;
179
- }
180
-
181
- .metric-footnote {
182
- margin-top: 8px;
183
- font-size: 12px;
184
- color: var(--advisor-muted);
185
- }
186
-
187
- .selected-panel {
188
- padding: 18px;
189
- margin-bottom: 18px;
190
- }
191
-
192
- .selected-title {
193
- font-size: 24px;
194
- font-weight: 700;
195
- letter-spacing: -0.05em;
196
- color: #ffffff;
197
- margin-top: 8px;
198
- }
199
-
200
- .selected-status {
201
- margin-top: 6px;
202
- color: var(--advisor-muted);
203
- font-size: 13px;
204
- }
205
-
206
- .selected-metrics {
207
- grid-template-columns: repeat(4, minmax(0, 1fr));
208
- margin-top: 18px;
209
- }
210
-
211
- .selected-metric {
212
- background: rgba(7, 7, 8, 0.72);
213
- border: 1px solid rgba(255, 255, 255, 0.05);
214
  border-radius: 12px;
215
  padding: 14px;
 
 
 
216
  }
217
 
218
- .selected-metric .label {
219
- font-size: 10px;
220
- letter-spacing: 0.18em;
221
- text-transform: uppercase;
222
- color: rgba(154, 157, 163, 0.72);
223
- }
224
-
225
- .selected-metric .value {
226
- margin-top: 8px;
227
- font-size: 20px;
228
- font-weight: 700;
229
- color: #e5e2e3;
230
- }
231
-
232
- .selected-metric .hint {
233
- margin-top: 4px;
234
- font-size: 12px;
235
- color: var(--advisor-muted);
236
- }
237
-
238
- .ai-grid {
239
- grid-template-columns: repeat(3, minmax(0, 1fr));
240
- margin-top: 12px;
241
  }
242
 
243
  .ai-card {
244
- background: rgba(7, 7, 8, 0.72);
 
 
245
  padding: 14px;
246
- min-height: 96px;
247
  }
248
 
249
  .ai-card h3 {
250
  font-size: 14px;
251
  margin-bottom: 6px;
252
- color: #ffffff;
253
  }
254
 
255
  .ai-card p {
256
  font-size: 12px;
257
- color: var(--advisor-muted);
258
- }
259
-
260
- .output-panel {
261
- margin-top: 16px;
262
- padding: 16px;
263
  }
264
 
 
265
  #ads-output {
266
- color: #e5e2e3;
267
  margin-top: 16px;
268
  padding: 16px;
269
- border: 1px solid var(--advisor-border-soft);
270
- border-radius: 16px;
271
- background: rgba(7, 7, 8, 0.72);
272
- }
273
-
274
- button.primary,
275
- .primary {
276
- background: var(--advisor-primary) !important;
277
- border-color: var(--advisor-primary) !important;
278
- color: #f0f1f2 !important;
279
  }
280
 
281
- @media (max-width: 1100px) {
282
- .layout-shell {
283
- grid-template-columns: 1fr;
284
- }
285
 
286
- .hero-grid,
287
- .selected-metrics,
288
- .ai-grid {
289
- grid-template-columns: repeat(2, minmax(0, 1fr));
290
- }
291
-
292
- .sidebar-panel {
293
- position: static;
294
- }
295
- }
296
-
297
- @media (max-width: 720px) {
298
- #advisor-shell {
299
- padding: 18px;
300
- }
301
-
302
- #advisor-header {
303
- flex-direction: column;
304
- align-items: flex-start;
305
- }
306
-
307
- .hero-grid,
308
- .selected-metrics,
309
- .ai-grid {
310
- grid-template-columns: 1fr;
311
- }
312
  }
313
  """
314
 
315
 
 
 
 
 
316
  def startup():
317
  try:
318
  init_db()
319
- print("DB initialized successfully", flush=True)
320
- except Exception as exc:
321
- print("DB failed:", exc, flush=True)
322
-
323
-
324
- def _format_money(value):
325
- return f"Β£{value:,.2f}"
326
-
327
-
328
- def _campaign_frame(dfs):
329
- campaigns = dfs.get("campaigns") if dfs else None
330
- if campaigns is None or campaigns.empty:
331
- return pd.DataFrame(columns=["id", "name", "status", "impressions", "clicks", "cost", "ctr", "conversions"])
332
-
333
- frame = campaigns.copy()
334
- for column in ["impressions", "clicks", "cost", "ctr", "conversions"]:
335
- if column not in frame:
336
- frame[column] = 0
337
- frame[column] = frame[column].fillna(0)
338
- return frame
339
-
340
-
341
- def _kpi_html(frame):
342
- if frame.empty:
343
- spend = leads = cpl = ctr = 0
344
- else:
345
- spend = float(frame["cost"].sum())
346
- leads = int(frame["conversions"].sum())
347
- total_clicks = float(frame["clicks"].sum())
348
- total_impressions = float(frame["impressions"].sum())
349
- ctr = (total_clicks / total_impressions * 100) if total_impressions else 0
350
- cpl = (spend / leads) if leads else 0
351
-
352
- return f"""
353
- <div class="hero-grid">
354
- <div class="metric-card"><div class="metric-label">Spend</div><div class="metric-value">{_format_money(spend)}</div><div class="metric-footnote">Live Google Ads totals</div></div>
355
- <div class="metric-card"><div class="metric-label">Leads</div><div class="metric-value">{leads}</div><div class="metric-footnote">Attributed conversions</div></div>
356
- <div class="metric-card"><div class="metric-label">Avg CPL</div><div class="metric-value">{_format_money(cpl)}</div><div class="metric-footnote">Across all campaigns</div></div>
357
- <div class="metric-card"><div class="metric-label">CTR</div><div class="metric-value">{ctr:.2f}%</div><div class="metric-footnote">Click-through rate</div></div>
358
- </div>
359
- """
360
 
 
 
 
361
 
362
- def _selected_campaign_html(campaign_state):
363
- if not campaign_state:
364
- return """
365
- <div class="selected-panel">
366
- <div class="selected-eyebrow">Selected campaign</div>
367
- <div class="selected-title">Select a campaign to begin analysis</div>
368
- <div class="selected-status">The live Google Ads campaign data will appear here.</div>
369
- </div>
370
- """
371
-
372
- campaign_df = campaign_state.get("campaign_df")
373
- if campaign_df is None or campaign_df.empty:
374
- return """
375
- <div class="selected-panel">
376
- <div class="selected-eyebrow">Selected campaign</div>
377
- <div class="selected-title">No campaign data available</div>
378
- <div class="selected-status">The API returned an empty campaign slice.</div>
379
- </div>
380
- """
381
-
382
- row = campaign_df.iloc[0]
383
- campaign_name = campaign_state.get("campaign_name", row.get("name", "Campaign"))
384
- spend = float(row.get("cost", 0) or 0)
385
- leads = int(row.get("conversions", 0) or 0)
386
- cpl = spend / leads if leads else 0
387
- ctr = float(row.get("ctr", 0) or 0)
388
- impressions = int(row.get("impressions", 0) or 0)
389
- clicks = int(row.get("clicks", 0) or 0)
390
- status = row.get("status", "UNKNOWN")
391
-
392
- return f"""
393
- <div class="selected-panel">
394
- <div class="selected-eyebrow">Selected campaign</div>
395
- <div class="selected-title">{campaign_name}</div>
396
- <div class="selected-status">Status: {status} | Live API data is scoped to this campaign.</div>
397
- <div class="selected-metrics">
398
- <div class="selected-metric"><div class="label">Spend</div><div class="value">{_format_money(spend)}</div><div class="hint">Selected campaign spend</div></div>
399
- <div class="selected-metric"><div class="label">Leads</div><div class="value">{leads}</div><div class="hint">Conversions recorded</div></div>
400
- <div class="selected-metric"><div class="label">CPL</div><div class="value">{_format_money(cpl)}</div><div class="hint">Cost per lead</div></div>
401
- <div class="selected-metric"><div class="label">CTR</div><div class="value">{ctr:.4f}</div><div class="hint">Click-through rate</div></div>
402
- </div>
403
- <div class="selected-metrics" style="margin-top: 12px;">
404
- <div class="selected-metric"><div class="label">Impressions</div><div class="value">{impressions:,}</div><div class="hint">Visibility in the last 30 days</div></div>
405
- <div class="selected-metric"><div class="label">Clicks</div><div class="value">{clicks:,}</div><div class="hint">Traffic driven by the campaign</div></div>
406
- <div class="selected-metric"><div class="label">Selected</div><div class="value">Active</div><div class="hint">This campaign is in focus</div></div>
407
- <div class="selected-metric"><div class="label">Analytics</div><div class="value">Ready</div><div class="hint">Run the Ads Analyst card below</div></div>
408
- </div>
409
  </div>
410
  """
411
 
 
412
 
413
- def initial_data_load():
414
- dfs = load_google_ads_data()
415
- frame = _campaign_frame(dfs)
416
- campaign_names = frame["name"].tolist() if not frame.empty else []
417
- selected_campaign = campaign_names[0] if campaign_names else None
418
- selected_state = on_campaign_select(dfs, selected_campaign) if selected_campaign else None
419
-
420
- return (
421
- dfs,
422
- selected_state,
423
- gr.update(choices=campaign_names, value=selected_campaign),
424
- _kpi_html(frame),
425
- _selected_campaign_html(selected_state),
426
- _selected_campaign_html(selected_state),
427
- )
428
 
 
 
 
429
 
430
- def campaign_row_selected(campaign_name, full_state):
431
- if not full_state or not campaign_name:
432
- return {}, _selected_campaign_html(None), _selected_campaign_html(None)
433
 
434
  campaign_state = on_campaign_select(full_state, campaign_name)
435
- return campaign_state, _selected_campaign_html(campaign_state), _selected_campaign_html(campaign_state)
436
 
 
 
 
 
 
 
 
 
 
 
 
 
 
437
 
438
  @spaces.GPU(duration=120)
439
  def run_ads_card(state):
@@ -448,91 +202,129 @@ def run_ads_card(state):
448
  return "⚠️ Campaign state not properly initialized"
449
 
450
  return run_ads_analyst_card(dfs, campaign_name=campaign_name)
451
- except Exception as exc:
452
- return f"⚠️ Analysis failed: {exc}"
453
 
 
 
 
 
 
 
 
454
 
455
  with gr.Blocks(css=CSS) as demo:
 
456
  full_state = gr.State()
457
  campaign_state = gr.State()
458
 
459
- gr.HTML(
460
- """
461
- <div id="advisor-shell">
462
- <div id="advisor-header">
463
- <div class="brand">
464
- <div class="brand-badge">A</div>
465
- <div>
466
- <div id="advisor-title">Advisor</div>
467
- <div id="advisor-subtitle">AI-powered Google Ads optimization</div>
468
- </div>
469
- </div>
470
- <div class="selected-eyebrow">Live Google Ads data</div>
471
- </div>
472
- <div class="panel-kicker">AI-powered control room</div>
473
- <h1 style="font-family: Manrope, sans-serif; font-size: clamp(42px, 5vw, 76px); line-height: 1.05; letter-spacing: -0.055em; font-weight: 600; margin: 6px 0 10px;">AI-powered Google Ads optimization</h1>
474
- <p style="max-width: 900px; color: #9a9da3; font-size: 18px; line-height: 1.6; margin: 0 0 24px;">Live campaign data, a selected-campaign focus rail, and the same LLM-backed Ads Analyst flow you already have in app.py.</p>
475
- </div>
476
- """
477
- )
478
 
479
- with gr.Row(elem_classes=["layout-shell"]):
480
- with gr.Column(scale=1, elem_classes=["sidebar-panel"]):
481
- gr.HTML("<div class='panel-title'>Campaigns</div>")
482
- campaign_selector = gr.Radio(
483
- choices=[],
484
- value=None,
 
 
485
  show_label=False,
486
- interactive=True,
487
- elem_id="campaign-selector",
488
  )
489
 
490
- with gr.Column(scale=3, elem_classes=["main-panel"]):
 
 
 
491
  kpi_html = gr.HTML()
492
- campaign_banner = gr.HTML()
493
- campaign_detail = gr.HTML(elem_classes=["output-panel"])
494
-
495
- gr.HTML(
496
- """
497
- <div class="panel-kicker">AI Insights</div>
498
- <p style="color: #9a9da3; margin: 8px 0 0;">The Ads Analyst card still invokes the same LLM-backed workflow. The other cards remain presentation placeholders.</p>
499
- """
500
- )
501
 
502
- with gr.Row(elem_classes=["ai-grid"]):
 
 
 
 
 
 
 
 
 
 
 
 
 
503
  ads_card = gr.Button(
504
- value="Run ad analytics\nAds Analyst",
505
- variant="primary",
506
- elem_classes=["ai-button"],
507
  )
508
- gr.HTML("<div class='ai-card'><h3>Budget Optimizer</h3><p>Ready</p></div>")
509
- gr.HTML("<div class='ai-card'><h3>Keyword Intelligence</h3><p>Ready</p></div>")
510
 
511
- with gr.Row(elem_classes=["ai-grid"]):
512
- gr.HTML("<div class='ai-card'><h3>Risk Detector</h3><p>Ready</p></div>")
513
- gr.HTML("<div class='ai-card'><h3>Growth Finder</h3><p>Ready</p></div>")
514
- gr.HTML("<div class='ai-card'><h3>Experiment Ideas</h3><p>Ready</p></div>")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
515
 
 
516
  ads_output = gr.Markdown(
517
- value="Select a campaign and click Run ad analytics.",
518
- elem_id="ads-output",
519
  )
520
 
521
- campaign_selector.change(
 
 
 
522
  fn=campaign_row_selected,
523
- inputs=[campaign_selector, full_state],
524
- outputs=[campaign_state, campaign_banner, campaign_detail],
525
  )
526
 
 
527
  ads_card.click(
528
  fn=run_ads_card,
529
  inputs=campaign_state,
530
- outputs=ads_output,
531
  )
532
 
 
 
533
  demo.load(
534
  fn=initial_data_load,
535
- outputs=[full_state, campaign_state, campaign_selector, kpi_html, campaign_banner, campaign_detail],
536
  )
537
 
538
  demo.load(fn=startup)
 
1
  import gradio as gr
 
2
  import spaces
3
 
4
  from app.db.repo import init_db
 
7
  from app.ads1.ads_analyst import run_ads_analyst_card
8
 
9
 
10
+ # ==================================================
11
+ # CURSOR-STYLE THEME
12
+ # ==================================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
+ CSS = """
15
  .gradio-container {
16
+ background: #f7f7f4 !important;
17
+ max-width: 1500px !important;
 
 
 
 
18
  }
19
 
20
  #advisor-header {
21
+ margin-bottom: 20px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  }
23
 
24
  #advisor-title {
25
+ font-size: 26px;
26
+ font-weight: 600;
27
+ color: #26251e;
 
28
  }
29
 
30
  #advisor-subtitle {
31
+ font-size: 13px;
32
+ color: #807d72;
33
  }
34
 
35
+ .sidebar {
36
+ background: #f7f7f4;
37
+ padding-right: 12px;
 
38
  }
39
 
40
+ /* KPI */
41
+ .kpi-strip {
42
+ display: grid;
43
+ grid-template-columns: repeat(4, 1fr);
44
+ gap: 10px;
45
+ margin-bottom: 16px;
 
 
 
 
46
  }
47
 
48
+ .kpi {
49
+ background: #fafaf7;
50
+ border: 1px solid #e6e5e0;
51
+ border-radius: 10px;
52
+ padding: 10px;
53
  }
54
 
55
+ .kpi .label {
56
  font-size: 11px;
57
+ color: #8a877c;
 
 
 
 
 
 
 
 
 
 
 
 
58
  }
59
 
60
+ .kpi .value {
61
+ font-size: 16px;
62
+ font-weight: 600;
63
+ color: #26251e;
64
  }
65
 
66
+ /* SNAPSHOT */
67
+ .snapshot {
68
+ border-top: 1px solid #e6e5e0;
69
+ border-bottom: 1px solid #e6e5e0;
70
+ padding: 14px 0;
71
+ margin: 14px 0;
72
  }
73
 
74
+ /* AI GRID */
 
 
 
 
 
 
75
  .ai-grid {
76
  display: grid;
77
+ grid-template-columns: repeat(3, 1fr);
78
  gap: 12px;
79
+ margin-top: 12px;
80
  }
81
 
82
+ /* REAL INTERACTIVE CARD */
83
+ .ai-button {
84
+ all: unset;
85
+ display: block;
86
+ background: #ffffff;
87
+ border: 1px solid #e6e5e0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  border-radius: 12px;
89
  padding: 14px;
90
+ min-height: 90px;
91
+ cursor: pointer;
92
+ transition: all 0.15s ease;
93
  }
94
 
95
+ .ai-button:hover {
96
+ border-color: #d6d3cc;
97
+ transform: translateY(-1px);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  }
99
 
100
  .ai-card {
101
+ background: #ffffff;
102
+ border: 1px solid #e6e5e0;
103
+ border-radius: 12px;
104
  padding: 14px;
105
+ min-height: 90px;
106
  }
107
 
108
  .ai-card h3 {
109
  font-size: 14px;
110
  margin-bottom: 6px;
 
111
  }
112
 
113
  .ai-card p {
114
  font-size: 12px;
115
+ color: #807d72;
 
 
 
 
 
116
  }
117
 
118
+ /* OUTPUT */
119
  #ads-output {
 
120
  margin-top: 16px;
121
  padding: 16px;
122
+ border: 1px solid #e6e5e0;
123
+ border-radius: 12px;
124
+ background: white;
 
 
 
 
 
 
 
125
  }
126
 
 
 
 
 
127
 
128
+ button.primary {
129
+ background: #f54e00 !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  }
131
  """
132
 
133
 
134
+ # ==================================================
135
+ # INIT
136
+ # ==================================================
137
+
138
  def startup():
139
  try:
140
  init_db()
141
+ print("βœ… DB initialized successfully", flush=True)
142
+ except Exception as e:
143
+ print("⚠️ DB failed:", e, flush=True)
144
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
 
146
+ # ==================================================
147
+ # DATA
148
+ # ==================================================
149
 
150
+ def initial_data_load():
151
+ dfs = load_google_ads_data()
152
+ from app.ui.dashboard import get_dashboard_data
153
+
154
+ spend, leads, cpl, count, formatted_df = get_dashboard_data()
155
+
156
+ kpi_html = f"""
157
+ <div class="kpi-strip">
158
+ <div class="kpi"><div class="label">Spend</div><div class="value">Β£{spend:.2f}</div></div>
159
+ <div class="kpi"><div class="label">Leads</div><div class="value">{leads}</div></div>
160
+ <div class="kpi"><div class="label">Avg CPL</div><div class="value">Β£{cpl:.2f}</div></div>
161
+ <div class="kpi"><div class="label">Campaigns</div><div class="value">{count}</div></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  </div>
163
  """
164
 
165
+ return dfs, formatted_df, kpi_html
166
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167
 
168
+ # ==================================================
169
+ # CAMPAIGN SELECT
170
+ # ==================================================
171
 
172
+ def campaign_row_selected(df, full_state, evt: gr.SelectData):
173
+ row_index = evt.index[0]
174
+ campaign_name = df.iloc[row_index]["Campaign"]
175
 
176
  campaign_state = on_campaign_select(full_state, campaign_name)
 
177
 
178
+ banner_html = f"""
179
+ <div class="snapshot">
180
+ <h3>πŸ“Š {campaign_name}</h3>
181
+ <p>Campaign selected. AI insights are now available.</p>
182
+ </div>
183
+ """
184
+
185
+ return campaign_state, banner_html
186
+
187
+
188
+ # ==================================================
189
+ # ADS ANALYST
190
+ # ==================================================
191
 
192
  @spaces.GPU(duration=120)
193
  def run_ads_card(state):
 
202
  return "⚠️ Campaign state not properly initialized"
203
 
204
  return run_ads_analyst_card(dfs, campaign_name=campaign_name)
 
 
205
 
206
+ except Exception as e:
207
+ return f"⚠️ Analysis failed: {e}"
208
+
209
+
210
+ # ==================================================
211
+ # UI
212
+ # ==================================================
213
 
214
  with gr.Blocks(css=CSS) as demo:
215
+
216
  full_state = gr.State()
217
  campaign_state = gr.State()
218
 
219
+ # ---------------- HEADER ----------------
220
+ gr.HTML("""
221
+ <div id="advisor-header">
222
+ <div id="advisor-title">🧠 Advisor</div>
223
+ <div id="advisor-subtitle">AI-powered Google Ads optimization</div>
224
+ </div>
225
+ """)
 
 
 
 
 
 
 
 
 
 
 
 
226
 
227
+ # ---------------- MAIN ----------------
228
+ with gr.Row():
229
+
230
+ # LEFT SIDEBAR
231
+ with gr.Column(scale=1, elem_classes=["sidebar"]):
232
+ gr.Markdown("### Campaigns")
233
+
234
+ campaign_table = gr.Dataframe(
235
  show_label=False,
236
+ interactive=True
 
237
  )
238
 
239
+ # RIGHT WORKSPACE
240
+ with gr.Column(scale=3):
241
+
242
+ # KPI STRIP
243
  kpi_html = gr.HTML()
 
 
 
 
 
 
 
 
 
244
 
245
+ # SNAPSHOT
246
+ campaign_banner = gr.HTML("""
247
+ <div class="snapshot">
248
+ <h3>Performance Snapshot</h3>
249
+ <p>Select a campaign to begin analysis.</p>
250
+ </div>
251
+ """)
252
+
253
+ # AI INSIGHTS
254
+ gr.Markdown("### AI Insights")
255
+
256
+ with gr.Row():
257
+
258
+ # βœ… CLICKABLE ADS ANALYST CARD
259
  ads_card = gr.Button(
260
+ value="πŸ“Š Ads Analyst\nClick to analyze campaign",
261
+ elem_classes=["ai-button"]
 
262
  )
 
 
263
 
264
+ gr.HTML("""
265
+ <div class="ai-card">
266
+ <h3>πŸ’° Budget Optimizer</h3>
267
+ <p>Ready</p>
268
+ </div>
269
+ """)
270
+
271
+ gr.HTML("""
272
+ <div class="ai-card">
273
+ <h3>🎯 Keyword Intelligence</h3>
274
+ <p>Ready</p>
275
+ </div>
276
+ """)
277
+
278
+ with gr.Row():
279
+
280
+ gr.HTML("""
281
+ <div class="ai-card">
282
+ <h3>⚠️ Risk Detector</h3>
283
+ <p>Ready</p>
284
+ </div>
285
+ """)
286
+
287
+ gr.HTML("""
288
+ <div class="ai-card">
289
+ <h3>πŸ“ˆ Growth Finder</h3>
290
+ <p>Ready</p>
291
+ </div>
292
+ """)
293
+
294
+ gr.HTML("""
295
+ <div class="ai-card">
296
+ <h3>πŸ§ͺ Experiment Ideas</h3>
297
+ <p>Ready</p>
298
+ </div>
299
+ """)
300
 
301
+ # OUTPUT (BELOW CARDS)
302
  ads_output = gr.Markdown(
303
+ value="Select a campaign and click Ads Analyst.",
304
+ elem_id="ads-output"
305
  )
306
 
307
+
308
+ # ---------------- EVENTS ----------------
309
+
310
+ campaign_table.select(
311
  fn=campaign_row_selected,
312
+ inputs=[campaign_table, full_state],
313
+ outputs=[campaign_state, campaign_banner],
314
  )
315
 
316
+ # βœ… RESTORED CLICK FUNCTIONALITY
317
  ads_card.click(
318
  fn=run_ads_card,
319
  inputs=campaign_state,
320
+ outputs=ads_output
321
  )
322
 
323
+ # ---------------- LOAD ----------------
324
+
325
  demo.load(
326
  fn=initial_data_load,
327
+ outputs=[full_state, campaign_table, kpi_html],
328
  )
329
 
330
  demo.load(fn=startup)