Clicks trend graph added
Browse files
app.py
CHANGED
|
@@ -2,6 +2,7 @@ print("APP STARTED", flush=True)
|
|
| 2 |
|
| 3 |
import html
|
| 4 |
import gradio as gr
|
|
|
|
| 5 |
import spaces
|
| 6 |
|
| 7 |
from app.db.repo import init_db
|
|
@@ -937,6 +938,132 @@ SIGNAL_FLOW_HTML = """
|
|
| 937 |
"""
|
| 938 |
|
| 939 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 940 |
def build_right_panel_html(doctor_result=None):
|
| 941 |
doctor_result = doctor_result or {}
|
| 942 |
|
|
@@ -1028,13 +1155,18 @@ def initial_data_load():
|
|
| 1028 |
)
|
| 1029 |
hero_html = build_hero_html(spend, leads, cpl, count)
|
| 1030 |
kpi_html = build_kpi_html(spend, leads, cpl, count)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1031 |
try:
|
| 1032 |
doctor_result = run_campaign_doctor(dfs)
|
| 1033 |
except Exception as e:
|
| 1034 |
print(f"CAMPAIGN DOCTOR FAILED: {e}", flush=True)
|
| 1035 |
doctor_result = None
|
| 1036 |
right_panel_html = build_right_panel_html(doctor_result)
|
| 1037 |
-
return dfs, gr.update(choices=campaign_choices, value=None), hero_html, kpi_html, right_panel_html
|
| 1038 |
|
| 1039 |
|
| 1040 |
# ==================================================
|
|
@@ -1175,7 +1307,7 @@ with gr.Blocks(fill_height=True, fill_width=True, css=CSS) as demo:
|
|
| 1175 |
|
| 1176 |
with gr.Column(scale=1, elem_classes=["center-workspace"]):
|
| 1177 |
kpi_html = gr.HTML()
|
| 1178 |
-
gr.HTML(SIGNAL_FLOW_HTML)
|
| 1179 |
|
| 1180 |
with gr.Row(elem_classes=["ai-row"]):
|
| 1181 |
analyze_ads_card = gr.Button(
|
|
@@ -1247,7 +1379,7 @@ with gr.Blocks(fill_height=True, fill_width=True, css=CSS) as demo:
|
|
| 1247 |
|
| 1248 |
demo.load(
|
| 1249 |
fn=initial_data_load,
|
| 1250 |
-
outputs=[full_state, campaign_picker, hero_html, kpi_html, right_panel_html],
|
| 1251 |
)
|
| 1252 |
|
| 1253 |
demo.load(fn=startup)
|
|
|
|
| 2 |
|
| 3 |
import html
|
| 4 |
import gradio as gr
|
| 5 |
+
import pandas as pd
|
| 6 |
import spaces
|
| 7 |
|
| 8 |
from app.db.repo import init_db
|
|
|
|
| 938 |
"""
|
| 939 |
|
| 940 |
|
| 941 |
+
def build_signal_flow_html(dfs=None):
|
| 942 |
+
if not dfs or "hourly" not in dfs or dfs["hourly"] is None or dfs["hourly"].empty:
|
| 943 |
+
return """
|
| 944 |
+
<div class="signal-card">
|
| 945 |
+
<div class="signal-header">
|
| 946 |
+
<div>
|
| 947 |
+
<span class="signal-title">Traffic Trend</span>
|
| 948 |
+
<span class="kpi-status"> [WAITING]</span>
|
| 949 |
+
</div>
|
| 950 |
+
</div>
|
| 951 |
+
<div class="signal-box" style="display:flex;align-items:center;justify-content:center;color:var(--custom-text-muted);font-size:14px;">
|
| 952 |
+
Waiting for Google Ads API data streaming...
|
| 953 |
+
</div>
|
| 954 |
+
</div>
|
| 955 |
+
"""
|
| 956 |
+
|
| 957 |
+
hourly_df = dfs["hourly"].copy()
|
| 958 |
+
required_columns = {"date", "clicks", "cost"}
|
| 959 |
+
if not required_columns.issubset(hourly_df.columns):
|
| 960 |
+
return """
|
| 961 |
+
<div class="signal-card">
|
| 962 |
+
<div class="signal-header">
|
| 963 |
+
<div>
|
| 964 |
+
<span class="signal-title">Traffic Trend</span>
|
| 965 |
+
<span class="kpi-status"> [DATA CHECK]</span>
|
| 966 |
+
</div>
|
| 967 |
+
</div>
|
| 968 |
+
<div class="signal-box" style="display:flex;align-items:center;justify-content:center;color:var(--custom-text-muted);font-size:14px;">
|
| 969 |
+
Hourly data is missing date, clicks, or cost columns.
|
| 970 |
+
</div>
|
| 971 |
+
</div>
|
| 972 |
+
"""
|
| 973 |
+
|
| 974 |
+
hourly_df["date"] = pd.to_datetime(hourly_df["date"], errors="coerce").dt.date
|
| 975 |
+
hourly_df["clicks"] = pd.to_numeric(hourly_df["clicks"], errors="coerce").fillna(0)
|
| 976 |
+
hourly_df["cost"] = pd.to_numeric(hourly_df["cost"], errors="coerce").fillna(0)
|
| 977 |
+
hourly_df = hourly_df.dropna(subset=["date"])
|
| 978 |
+
|
| 979 |
+
df_daily = (
|
| 980 |
+
hourly_df.groupby("date", as_index=False)
|
| 981 |
+
.agg({"clicks": "sum", "cost": "sum"})
|
| 982 |
+
.sort_values("date")
|
| 983 |
+
.tail(14)
|
| 984 |
+
.reset_index(drop=True)
|
| 985 |
+
)
|
| 986 |
+
|
| 987 |
+
if df_daily.empty:
|
| 988 |
+
return build_signal_flow_html(None)
|
| 989 |
+
|
| 990 |
+
total_days = len(df_daily)
|
| 991 |
+
total_clicks = int(df_daily["clicks"].sum())
|
| 992 |
+
total_cost = float(df_daily["cost"].sum())
|
| 993 |
+
|
| 994 |
+
if (
|
| 995 |
+
"campaigns" in dfs
|
| 996 |
+
and dfs["campaigns"] is not None
|
| 997 |
+
and not dfs["campaigns"].empty
|
| 998 |
+
and "conversions" in dfs["campaigns"].columns
|
| 999 |
+
):
|
| 1000 |
+
total_tours = int(dfs["campaigns"]["conversions"].fillna(0).sum())
|
| 1001 |
+
else:
|
| 1002 |
+
total_tours = int(total_clicks * 0.06)
|
| 1003 |
+
|
| 1004 |
+
max_clicks = df_daily["clicks"].max()
|
| 1005 |
+
max_clicks = max_clicks if max_clicks > 0 else 1
|
| 1006 |
+
|
| 1007 |
+
points = []
|
| 1008 |
+
for idx, row in df_daily.iterrows():
|
| 1009 |
+
x = (idx / (total_days - 1)) * 100 if total_days > 1 else 50
|
| 1010 |
+
y = 100 - ((row["clicks"] / max_clicks) * 75 + 15)
|
| 1011 |
+
points.append((x, y))
|
| 1012 |
+
|
| 1013 |
+
line_path = "M " + " L ".join(f"{x:.1f},{y:.1f}" for x, y in points)
|
| 1014 |
+
first_x = points[0][0]
|
| 1015 |
+
last_x = points[-1][0]
|
| 1016 |
+
area_path = f"{line_path} L {last_x:.1f},100 L {first_x:.1f},100 Z"
|
| 1017 |
+
|
| 1018 |
+
peak_idx = int(df_daily["clicks"].idxmax())
|
| 1019 |
+
dot_indices = sorted({0, peak_idx, total_days - 1})
|
| 1020 |
+
dots_html = "".join(
|
| 1021 |
+
f'<circle cx="{points[i][0]:.1f}" cy="{points[i][1]:.1f}" fill="#5E6BFF" r="1.8" stroke="white" stroke-width="0.6"></circle>'
|
| 1022 |
+
for i in dot_indices
|
| 1023 |
+
if i < len(points)
|
| 1024 |
+
)
|
| 1025 |
+
|
| 1026 |
+
return f"""
|
| 1027 |
+
<div class="signal-card">
|
| 1028 |
+
<div class="signal-header">
|
| 1029 |
+
<div>
|
| 1030 |
+
<span class="signal-title">Traffic Trend</span>
|
| 1031 |
+
<span class="kpi-status"></span>
|
| 1032 |
+
</div>
|
| 1033 |
+
<div class="kpi-status">
|
| 1034 |
+
<span style="color: var(--primary);">{format_number(total_clicks)} Clicks</span>
|
| 1035 |
+
<span style="margin-left: 16px; color: var(--secondary);">{format_number(total_tours)} Tours Booked</span>
|
| 1036 |
+
<span style="margin-left: 16px; color: var(--custom-text-muted);">{format_money(total_cost)} Spent</span>
|
| 1037 |
+
</div>
|
| 1038 |
+
</div>
|
| 1039 |
+
<div class="signal-box">
|
| 1040 |
+
<div class="grid-lines"><span></span><span></span><span></span><span></span></div>
|
| 1041 |
+
<div class="vertical-lines"><span></span><span></span><span></span><span></span><span></span><span></span></div>
|
| 1042 |
+
<svg preserveAspectRatio="none" viewBox="0 0 100 100">
|
| 1043 |
+
<defs>
|
| 1044 |
+
<linearGradient id="gradient-flow-daily" x1="0%" x2="0%" y1="0%" y2="100%">
|
| 1045 |
+
<stop offset="0%" stop-color="#5E6BFF" stop-opacity="0.25"></stop>
|
| 1046 |
+
<stop offset="100%" stop-color="#5E6BFF" stop-opacity="0"></stop>
|
| 1047 |
+
</linearGradient>
|
| 1048 |
+
<filter height="140%" id="glow-daily" width="140%" x="-20%" y="-20%">
|
| 1049 |
+
<feGaussianBlur result="blur" stdDeviation="1"></feGaussianBlur>
|
| 1050 |
+
<feComposite in="SourceGraphic" in2="blur" operator="over"></feComposite>
|
| 1051 |
+
</filter>
|
| 1052 |
+
</defs>
|
| 1053 |
+
<path d="{area_path}" fill="url(#gradient-flow-daily)"></path>
|
| 1054 |
+
<path d="{line_path}" fill="none" filter="url(#glow-daily)" stroke="#5E6BFF" stroke-linecap="round" stroke-width="1.2"></path>
|
| 1055 |
+
{dots_html}
|
| 1056 |
+
</svg>
|
| 1057 |
+
<div style="position: absolute; bottom: 6px; right: 10px; color: rgba(154,157,163,0.4); font-size: 10px; font-weight: 500;">Last 14 days</div>
|
| 1058 |
+
<div style="position: absolute; top: 6px; left: 10px; color: rgba(154,157,163,0.4); font-size: 10px; font-weight: 500;">Daily Clicks (Last 14 Days)</div>
|
| 1059 |
+
</div>
|
| 1060 |
+
</div>
|
| 1061 |
+
"""
|
| 1062 |
+
|
| 1063 |
+
|
| 1064 |
+
SIGNAL_FLOW_HTML = build_signal_flow_html()
|
| 1065 |
+
|
| 1066 |
+
|
| 1067 |
def build_right_panel_html(doctor_result=None):
|
| 1068 |
doctor_result = doctor_result or {}
|
| 1069 |
|
|
|
|
| 1155 |
)
|
| 1156 |
hero_html = build_hero_html(spend, leads, cpl, count)
|
| 1157 |
kpi_html = build_kpi_html(spend, leads, cpl, count)
|
| 1158 |
+
try:
|
| 1159 |
+
signal_flow_html = build_signal_flow_html(dfs)
|
| 1160 |
+
except Exception as e:
|
| 1161 |
+
print(f"SIGNAL FLOW CHART FAILED: {e}", flush=True)
|
| 1162 |
+
signal_flow_html = build_signal_flow_html()
|
| 1163 |
try:
|
| 1164 |
doctor_result = run_campaign_doctor(dfs)
|
| 1165 |
except Exception as e:
|
| 1166 |
print(f"CAMPAIGN DOCTOR FAILED: {e}", flush=True)
|
| 1167 |
doctor_result = None
|
| 1168 |
right_panel_html = build_right_panel_html(doctor_result)
|
| 1169 |
+
return dfs, gr.update(choices=campaign_choices, value=None), hero_html, kpi_html, signal_flow_html, right_panel_html
|
| 1170 |
|
| 1171 |
|
| 1172 |
# ==================================================
|
|
|
|
| 1307 |
|
| 1308 |
with gr.Column(scale=1, elem_classes=["center-workspace"]):
|
| 1309 |
kpi_html = gr.HTML()
|
| 1310 |
+
signal_flow_html = gr.HTML(SIGNAL_FLOW_HTML)
|
| 1311 |
|
| 1312 |
with gr.Row(elem_classes=["ai-row"]):
|
| 1313 |
analyze_ads_card = gr.Button(
|
|
|
|
| 1379 |
|
| 1380 |
demo.load(
|
| 1381 |
fn=initial_data_load,
|
| 1382 |
+
outputs=[full_state, campaign_picker, hero_html, kpi_html, signal_flow_html, right_panel_html],
|
| 1383 |
)
|
| 1384 |
|
| 1385 |
demo.load(fn=startup)
|