update
Browse files
app.py
CHANGED
|
@@ -484,6 +484,9 @@ footer,
|
|
| 484 |
}
|
| 485 |
|
| 486 |
.ai-button-card {
|
|
|
|
|
|
|
|
|
|
| 487 |
color: var(--on-surface) !important;
|
| 488 |
text-align: left !important;
|
| 489 |
font-size: 14px !important;
|
|
@@ -504,10 +507,11 @@ button.ai-button-card {
|
|
| 504 |
justify-content: flex-start !important;
|
| 505 |
text-align: left !important;
|
| 506 |
white-space: pre-line !important;
|
| 507 |
-
background:
|
| 508 |
color: var(--on-surface) !important;
|
| 509 |
-
border:
|
| 510 |
-
|
|
|
|
| 511 |
}
|
| 512 |
|
| 513 |
.ai-button-card:hover {
|
|
@@ -566,6 +570,10 @@ button.ai-button-card {
|
|
| 566 |
color: #ffffff !important;
|
| 567 |
}
|
| 568 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 569 |
.advisor-intel-title .material-symbols-outlined {
|
| 570 |
color: #ffffff !important;
|
| 571 |
font-size: 20px;
|
|
@@ -584,12 +592,12 @@ button.ai-button-card {
|
|
| 584 |
}
|
| 585 |
|
| 586 |
.intel-label {
|
| 587 |
-
color:
|
| 588 |
font-size: 10px;
|
| 589 |
font-weight: 700;
|
| 590 |
text-transform: uppercase;
|
| 591 |
letter-spacing: 0.12em;
|
| 592 |
-
opacity:
|
| 593 |
margin-bottom: 12px;
|
| 594 |
}
|
| 595 |
|
|
@@ -603,6 +611,10 @@ button.ai-button-card {
|
|
| 603 |
line-height: 1.5;
|
| 604 |
}
|
| 605 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 606 |
.text-primary {
|
| 607 |
color: #ffffff !important;
|
| 608 |
font-weight: 700;
|
|
@@ -684,6 +696,23 @@ def format_money(value):
|
|
| 684 |
return "£0.00"
|
| 685 |
|
| 686 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 687 |
def build_hero_html(spend, leads, cpl, count):
|
| 688 |
return f"""
|
| 689 |
<section class="hero-section">
|
|
@@ -741,6 +770,59 @@ def build_kpi_html(spend, leads, cpl, count):
|
|
| 741 |
"""
|
| 742 |
|
| 743 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 744 |
SIGNAL_FLOW_HTML = """
|
| 745 |
<div class="signal-card">
|
| 746 |
<div class="signal-header">
|
|
@@ -843,9 +925,10 @@ def initial_data_load():
|
|
| 843 |
|
| 844 |
def campaign_selected(campaign_name, full_state):
|
| 845 |
if not campaign_name:
|
| 846 |
-
return None
|
| 847 |
|
| 848 |
-
|
|
|
|
| 849 |
|
| 850 |
|
| 851 |
# ==================================================
|
|
@@ -930,7 +1013,7 @@ with gr.Blocks(fill_height=True, fill_width=True, css=CSS) as demo:
|
|
| 930 |
campaign_picker.change(
|
| 931 |
fn=campaign_selected,
|
| 932 |
inputs=[campaign_picker, full_state],
|
| 933 |
-
outputs=[campaign_state],
|
| 934 |
)
|
| 935 |
|
| 936 |
ads_card.click(
|
|
|
|
| 484 |
}
|
| 485 |
|
| 486 |
.ai-button-card {
|
| 487 |
+
background: var(--custom-card-bg) !important;
|
| 488 |
+
border: 1px solid var(--custom-divider) !important;
|
| 489 |
+
border-radius: 8px !important;
|
| 490 |
color: var(--on-surface) !important;
|
| 491 |
text-align: left !important;
|
| 492 |
font-size: 14px !important;
|
|
|
|
| 507 |
justify-content: flex-start !important;
|
| 508 |
text-align: left !important;
|
| 509 |
white-space: pre-line !important;
|
| 510 |
+
background: var(--custom-card-bg) !important;
|
| 511 |
color: var(--on-surface) !important;
|
| 512 |
+
border: 1px solid var(--custom-divider) !important;
|
| 513 |
+
border-radius: 8px !important;
|
| 514 |
+
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08) !important;
|
| 515 |
}
|
| 516 |
|
| 517 |
.ai-button-card:hover {
|
|
|
|
| 570 |
color: #ffffff !important;
|
| 571 |
}
|
| 572 |
|
| 573 |
+
.advisor-intel-title * {
|
| 574 |
+
color: #ffffff !important;
|
| 575 |
+
}
|
| 576 |
+
|
| 577 |
.advisor-intel-title .material-symbols-outlined {
|
| 578 |
color: #ffffff !important;
|
| 579 |
font-size: 20px;
|
|
|
|
| 592 |
}
|
| 593 |
|
| 594 |
.intel-label {
|
| 595 |
+
color: #c7c9d1 !important;
|
| 596 |
font-size: 10px;
|
| 597 |
font-weight: 700;
|
| 598 |
text-transform: uppercase;
|
| 599 |
letter-spacing: 0.12em;
|
| 600 |
+
opacity: 1;
|
| 601 |
margin-bottom: 12px;
|
| 602 |
}
|
| 603 |
|
|
|
|
| 611 |
line-height: 1.5;
|
| 612 |
}
|
| 613 |
|
| 614 |
+
.intel-box * {
|
| 615 |
+
color: #ffffff !important;
|
| 616 |
+
}
|
| 617 |
+
|
| 618 |
.text-primary {
|
| 619 |
color: #ffffff !important;
|
| 620 |
font-weight: 700;
|
|
|
|
| 696 |
return "£0.00"
|
| 697 |
|
| 698 |
|
| 699 |
+
def format_number(value):
|
| 700 |
+
try:
|
| 701 |
+
return f"{float(value):,.0f}"
|
| 702 |
+
except (TypeError, ValueError):
|
| 703 |
+
return "0"
|
| 704 |
+
|
| 705 |
+
|
| 706 |
+
def format_percent(value):
|
| 707 |
+
try:
|
| 708 |
+
numeric = float(value)
|
| 709 |
+
if abs(numeric) <= 1:
|
| 710 |
+
numeric *= 100
|
| 711 |
+
return f"{numeric:.2f}%"
|
| 712 |
+
except (TypeError, ValueError):
|
| 713 |
+
return "0.00%"
|
| 714 |
+
|
| 715 |
+
|
| 716 |
def build_hero_html(spend, leads, cpl, count):
|
| 717 |
return f"""
|
| 718 |
<section class="hero-section">
|
|
|
|
| 770 |
"""
|
| 771 |
|
| 772 |
|
| 773 |
+
def build_campaign_kpi_html(campaign_df):
|
| 774 |
+
if campaign_df is None or campaign_df.empty:
|
| 775 |
+
return build_kpi_html(0, 0, 0, 0)
|
| 776 |
+
|
| 777 |
+
spend = campaign_df["cost"].sum() if "cost" in campaign_df else 0
|
| 778 |
+
clicks = campaign_df["clicks"].sum() if "clicks" in campaign_df else 0
|
| 779 |
+
conversions = campaign_df["conversions"].sum() if "conversions" in campaign_df else 0
|
| 780 |
+
cpl = spend / conversions if conversions else 0
|
| 781 |
+
|
| 782 |
+
if "ctr" in campaign_df:
|
| 783 |
+
ctr = campaign_df["ctr"].mean()
|
| 784 |
+
else:
|
| 785 |
+
impressions = campaign_df["impressions"].sum() if "impressions" in campaign_df else 0
|
| 786 |
+
ctr = clicks / impressions if impressions else 0
|
| 787 |
+
|
| 788 |
+
return f"""
|
| 789 |
+
<div class="kpi-grid">
|
| 790 |
+
<div class="kpi-card">
|
| 791 |
+
<div class="kpi-top">
|
| 792 |
+
<div class="kpi-label">Spend</div>
|
| 793 |
+
<div class="kpi-status">[CAMPAIGN]</div>
|
| 794 |
+
</div>
|
| 795 |
+
<div class="kpi-value">{format_money(spend)}</div>
|
| 796 |
+
<div class="kpi-note note-sky">Selected campaign spend</div>
|
| 797 |
+
</div>
|
| 798 |
+
<div class="kpi-card">
|
| 799 |
+
<div class="kpi-top">
|
| 800 |
+
<div class="kpi-label">Clicks</div>
|
| 801 |
+
<div class="kpi-status">[TRAFFIC]</div>
|
| 802 |
+
</div>
|
| 803 |
+
<div class="kpi-value">{format_number(clicks)}</div>
|
| 804 |
+
<div class="kpi-note note-brown">Campaign traffic</div>
|
| 805 |
+
</div>
|
| 806 |
+
<div class="kpi-card">
|
| 807 |
+
<div class="kpi-top">
|
| 808 |
+
<div class="kpi-label">CPL</div>
|
| 809 |
+
<div class="kpi-status">[EFFICIENCY]</div>
|
| 810 |
+
</div>
|
| 811 |
+
<div class="kpi-value">{format_money(cpl)}</div>
|
| 812 |
+
<div class="kpi-note note-olive">{format_number(conversions)} leads recorded</div>
|
| 813 |
+
</div>
|
| 814 |
+
<div class="kpi-card">
|
| 815 |
+
<div class="kpi-top">
|
| 816 |
+
<div class="kpi-label">CTR</div>
|
| 817 |
+
<div class="kpi-status">[RESPONSE]</div>
|
| 818 |
+
</div>
|
| 819 |
+
<div class="kpi-value">{format_percent(ctr)}</div>
|
| 820 |
+
<div class="kpi-note note-violet">Click-through rate</div>
|
| 821 |
+
</div>
|
| 822 |
+
</div>
|
| 823 |
+
"""
|
| 824 |
+
|
| 825 |
+
|
| 826 |
SIGNAL_FLOW_HTML = """
|
| 827 |
<div class="signal-card">
|
| 828 |
<div class="signal-header">
|
|
|
|
| 925 |
|
| 926 |
def campaign_selected(campaign_name, full_state):
|
| 927 |
if not campaign_name:
|
| 928 |
+
return None, gr.update()
|
| 929 |
|
| 930 |
+
campaign_state = on_campaign_select(full_state, campaign_name)
|
| 931 |
+
return campaign_state, build_campaign_kpi_html(campaign_state.get("campaign_df"))
|
| 932 |
|
| 933 |
|
| 934 |
# ==================================================
|
|
|
|
| 1013 |
campaign_picker.change(
|
| 1014 |
fn=campaign_selected,
|
| 1015 |
inputs=[campaign_picker, full_state],
|
| 1016 |
+
outputs=[campaign_state, kpi_html],
|
| 1017 |
)
|
| 1018 |
|
| 1019 |
ads_card.click(
|