print("APP STARTED", flush=True)
import html
import gradio as gr
import pandas as pd
import spaces
from app.db.repo import init_db
print("IMPORT 1 OK", flush=True)
from app.controller.campaign_controller import on_campaign_select
print("IMPORT 2 OK", flush=True)
from app.controller.session_loader import load_google_ads_data
print("IMPORT 3 OK", flush=True)
from app.ads1.ads_analyst import run_ads_analyst_card
print("IMPORT 4 OK", flush=True)
from app.ads1.search_term_optimizer import run_search_term_optimizer
from app.ads1.keyword_inspector import run_keyword_inspector
from app.ads1.budget_optimizer import run_budget_optimizer
from app.ads1.growth_finder import run_growth_finder
from app.ads1.campaign_doctor import run_campaign_doctor
# ==================================================
# ROMER / ADVISOR DASHBOARD THEME
# ==================================================
CSS = """
@import url('https://fonts.googleapis.com/css2?family=Manrope:wght@400;520;600;700&family=Inter:wght@400;500;600&family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap');
:root {
--custom-bg: #070708;
--custom-divider: #232426;
--custom-card-bg: #101112;
--custom-sidebar: #0d0e0f;
--custom-panel: #111214;
--custom-text-muted: #9A9DA3;
--custom-btn-primary: #5E6BFF;
--on-surface: #e5e2e3;
--on-surface-variant: #c6c5d8;
--secondary: #50d8e9;
--tertiary: #ffb689;
--primary: #bec2ff;
}
html,
body,
.gradio-container {
background: var(--custom-bg) !important;
color: var(--on-surface) !important;
font-family: Inter, sans-serif !important;
color-scheme: dark !important;
}
.gradio-container {
max-width: 100% !important;
padding: 0 !important;
}
.main,
.contain {
max-width: none !important;
}
footer,
.api-docs,
.built-with {
display: none !important;
}
.gradio-container,
.gradio-container * {
color: var(--on-surface);
}
/* ================================
HEADER
================================ */
#advisor-header {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 80px;
width: 100%;
background: var(--custom-bg);
border-bottom: 1px solid var(--custom-divider);
z-index: 50;
}
#advisor-header .header-inner {
max-width: 1728px;
height: 100%;
margin: 0 auto;
padding: 0 32px;
display: flex;
align-items: center;
justify-content: space-between;
}
#advisor-header .brand {
display: flex;
align-items: center;
gap: 8px;
}
#advisor-header .avatar {
width: 32px;
height: 32px;
border-radius: 8px;
border: 1px solid var(--custom-text-muted);
display: flex;
align-items: center;
justify-content: center;
font-family: Manrope, sans-serif;
font-weight: 700;
font-size: 14px;
color: var(--on-surface);
}
#advisor-header .title {
font-family: Manrope, sans-serif;
font-size: 24px;
line-height: 1.2;
font-weight: 700;
letter-spacing: 0;
color: var(--on-surface);
}
/* ================================
HERO
================================ */
#advisor-shell {
max-width: 1728px;
margin: 80px auto 0;
padding: 0 32px 56px;
}
.hero-section {
position: relative;
padding: 128px 0 80px;
}
.hero-inner {
max-width: 1516px;
margin: 0 auto;
width: 100%;
}
.hero-row {
display: flex;
justify-content: space-between;
align-items: flex-end;
margin-bottom: 40px;
}
.hero-title {
font-family: Manrope, sans-serif;
font-size: 76px !important;
line-height: 1.1 !important;
letter-spacing: 0;
font-weight: 520 !important;
color: var(--on-surface) !important;
max-width: 1000px;
margin-bottom: 16px;
}
.hero-subtitle {
font-family: Inter, sans-serif;
font-size: 19px;
line-height: 1.6;
color: var(--custom-text-muted) !important;
margin-bottom: 24px;
}
/* ================================
DASHBOARD SHELL
================================ */
#dashboard-shell {
width: 100%;
min-height: 620px;
display: flex;
overflow: hidden;
position: relative;
background: var(--custom-card-bg);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 8px;
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.45), inset 0 1px 0 rgba(255, 255, 255, 0.10);
}
#dashboard-shell > .form,
#dashboard-shell > .block,
#dashboard-shell > div {
border: 0 !important;
}
.dashboard-row {
gap: 0 !important;
width: 100%;
}
.campaign-sidebar {
flex: 0 0 256px !important;
min-width: 256px !important;
max-width: 256px !important;
background: var(--custom-sidebar);
border-right: 1px solid var(--custom-divider);
padding: 16px !important;
}
.center-workspace {
background: #0a0a0b;
padding: 24px !important;
gap: 24px !important;
min-width: 0;
}
.right-panel {
flex: 0 0 320px !important;
min-width: 320px !important;
max-width: 320px !important;
background: var(--custom-panel);
border-left: 1px solid var(--custom-divider);
padding: 16px !important;
}
.panel-label {
color: #ffffff !important;
font-size: 10px;
line-height: 1;
letter-spacing: 0.2em;
text-transform: uppercase;
font-weight: 700;
opacity: 1;
margin: 0 0 24px;
padding: 0 16px;
}
/* ================================
GRADIO COMPONENT RESET
================================ */
.campaign-sidebar .block,
.center-workspace .block,
.right-panel .block {
background: transparent !important;
border: 0 !important;
box-shadow: none !important;
}
.campaign-radio,
.campaign-radio > div,
.campaign-radio fieldset,
.campaign-radio .wrap {
background: transparent !important;
border: 0 !important;
box-shadow: none !important;
padding: 0 !important;
margin: 0 !important;
}
.campaign-radio .container,
.campaign-radio .options,
.campaign-radio [role="radiogroup"] {
display: flex !important;
flex-direction: column !important;
gap: 4px !important;
overflow: visible !important;
max-height: none !important;
}
.campaign-radio label {
display: flex !important;
align-items: center !important;
gap: 12px !important;
width: 100% !important;
min-height: 36px !important;
margin: 0 !important;
padding: 8px 12px !important;
border: 1px solid transparent !important;
border-radius: 6px !important;
background: transparent !important;
color: var(--custom-text-muted) !important;
font-size: 14px !important;
line-height: 1.35 !important;
cursor: pointer !important;
}
.campaign-radio label::before {
content: "grid_view";
font-family: "Material Symbols Outlined";
font-size: 18px;
line-height: 1;
color: rgba(154, 157, 163, 0.55);
flex: 0 0 auto;
}
.campaign-radio label:hover,
.campaign-radio label:has(input:checked) {
background: rgba(255, 255, 255, 0.04) !important;
border-color: rgba(255, 255, 255, 0.05) !important;
color: var(--on-surface) !important;
}
.campaign-radio label:has(input:checked)::before {
color: var(--secondary);
}
.campaign-radio input {
display: none !important;
}
.campaign-radio span {
color: inherit !important;
white-space: normal !important;
}
/* ================================
KPI / CHART / CARDS
================================ */
.kpi-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 16px;
}
.kpi-card {
background: var(--custom-card-bg);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 8px;
padding: 20px;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.10);
}
.kpi-top {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 12px;
margin-bottom: 12px;
}
.kpi-label {
color: #ffffff !important;
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.12em;
font-weight: 700;
opacity: 0.6;
}
.kpi-status {
font-size: 9px;
color: rgba(255, 255, 255, 0.68) !important;
text-transform: uppercase;
letter-spacing: 0;
white-space: nowrap;
}
.kpi-value {
font-family: Manrope, sans-serif;
font-size: 24px;
line-height: 1.2;
color: #ffffff !important;
font-weight: 700;
}
.kpi-note {
margin-top: 12px;
font-size: 11px;
font-weight: 600;
}
.note-sky {
color: #50d8e9 !important;
}
.note-brown {
color: #c88a5a !important;
}
.note-olive {
color: #a9b75f !important;
}
.note-violet {
color: #bec2ff !important;
}
.signal-card {
background: var(--custom-card-bg);
border: 1px solid var(--custom-divider);
border-radius: 8px;
padding: 16px;
height: 220px;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.10);
display: flex;
flex-direction: column;
}
.signal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.signal-title {
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.12em;
color: #ffffff !important;
}
.signal-box {
position: relative;
flex: 1;
overflow: hidden;
border-radius: 6px;
background: rgba(7, 7, 8, 0.5);
border: 1px solid rgba(255, 255, 255, 0.02);
}
.grid-lines,
.vertical-lines {
position: absolute;
inset: 0;
pointer-events: none;
}
.grid-lines {
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 8px 0;
opacity: 0.12;
}
.grid-lines span {
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.vertical-lines {
display: grid;
grid-template-columns: repeat(6, 1fr);
}
.vertical-lines span {
border-right: 1px solid rgba(255, 255, 255, 0.05);
}
.signal-card svg {
position: absolute;
bottom: 0;
width: 100%;
height: 80%;
}
.ai-row {
gap: 12px !important;
align-items: stretch !important;
}
.ai-card-html,
.ai-button-card {
background: var(--custom-card-bg) !important;
border: 1px solid var(--custom-divider) !important;
border-radius: 8px !important;
padding: 16px !important;
min-height: 116px !important;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08) !important;
}
.ai-row > *,
.ai-row .block {
align-self: stretch !important;
}
.ai-card-html h3 {
margin: 0 0 12px;
padding-bottom: 12px;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
color: #ffffff !important;
font-size: 14px;
font-weight: 700;
}
.ai-card-html p {
margin: 0;
color: #ffffff !important;
font-size: 14px;
line-height: 1.5;
}
.ai-button-card {
background: var(--custom-card-bg) !important;
border: 1px solid var(--custom-divider) !important;
border-radius: 8px !important;
color: var(--on-surface) !important;
text-align: left !important;
font-size: 14px !important;
font-weight: 700 !important;
transition: border-color 0.15s ease, transform 0.15s ease, background 0.15s ease;
white-space: pre-line !important;
margin: 0 !important;
height: 116px !important;
position: relative !important;
overflow: hidden !important;
}
.ai-button-card button,
button.ai-button-card {
height: 100% !important;
min-height: 116px !important;
margin: 0 !important;
padding: 16px !important;
align-items: flex-start !important;
justify-content: flex-start !important;
text-align: left !important;
white-space: pre-line !important;
background: var(--custom-card-bg) !important;
color: var(--on-surface) !important;
border: 1px solid var(--custom-divider) !important;
border-radius: 8px !important;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08) !important;
line-height: 1.55 !important;
position: relative !important;
font-size: 14px !important;
font-weight: 700 !important;
}
.ai-button-card button::after,
button.ai-button-card::after {
content: "";
position: absolute;
left: 16px;
right: 16px;
top: 48px;
height: 1px;
background: rgba(255, 255, 255, 0.05);
}
.ai-button-card button::before,
button.ai-button-card::before {
position: absolute;
left: 16px;
right: 16px;
top: 64px;
color: #ffffff !important;
font-size: 14px;
font-weight: 400;
line-height: 1.5;
}
.ads-analyst-card button::before,
button.ads-analyst-card::before {
content: "Campaign insights.";
}
.budget-optimizer-card button::before,
button.budget-optimizer-card::before {
content: "Where to adjust spend?";
}
.keyword-inspector-card button::before,
button.keyword-inspector-card::before {
content: "Winning versus wasting keywords.";
}
.search-term-cleaner-card button::before,
button.search-term-cleaner-card::before {
content: "Optimize search terms.";
}
.ai-button-card:hover,
.ai-button-card:hover button,
button.ai-button-card:hover {
border-color: rgba(94, 107, 255, 0.7) !important;
background: #141519 !important;
transform: translateY(-1px);
}
.clickable-card {
cursor: pointer;
transition: border-color 0.15s ease,
transform 0.15s ease,
background 0.15s ease;
}
.clickable-card:hover {
border-color: rgba(94, 107, 255, 0.7) !important;
background: #141519 !important;
transform: translateY(-1px);
}
.snapshot {
background: var(--custom-card-bg);
border: 1px solid var(--custom-divider);
border-radius: 8px;
padding: 16px;
color: var(--on-surface);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
}
.snapshot h3 {
margin: 0 0 8px;
font-size: 14px;
font-weight: 700;
}
.snapshot p {
margin: 0;
color: var(--custom-text-muted);
font-size: 14px;
}
#ads-output {
margin-top: 0;
padding: 16px;
min-height: 140px;
border: 1px solid var(--custom-divider);
border-radius: 8px;
background: var(--custom-card-bg);
color: var(--on-surface);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
}
#ads-output,
#ads-output * {
color: var(--on-surface) !important;
}
.advisor-intel-title {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
padding: 0 4px 20px;
margin-bottom: 8px;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
font-size: 14px;
font-weight: 700;
color: #ffffff !important;
}
.advisor-intel-title * {
color: #ffffff !important;
}
.advisor-intel-title .material-symbols-outlined {
color: #ffffff !important;
font-size: 20px;
}
.pulse-dot {
width: 6px;
height: 6px;
border-radius: 999px;
background: var(--secondary);
display: inline-block;
}
.intel-section {
margin-top: 28px;
}
.intel-label {
color: #c7c9d1 !important;
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.12em;
opacity: 1;
margin-bottom: 12px;
}
.intel-box {
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 8px;
padding: 16px;
color: #ffffff !important;
font-size: 14px;
line-height: 1.5;
}
.intel-box * {
color: #ffffff !important;
}
.text-primary {
color: #ffffff !important;
font-weight: 700;
}
.text-secondary {
color: #ffffff !important;
font-weight: 600;
}
@media (max-width: 1200px) {
#dashboard-shell,
.dashboard-row {
flex-direction: column !important;
}
.campaign-sidebar,
.right-panel {
flex: auto !important;
min-width: 100% !important;
max-width: 100% !important;
border-left: 0;
}
.campaign-sidebar {
border-right: 0;
border-bottom: 1px solid var(--custom-divider);
}
.right-panel {
border-top: 1px solid var(--custom-divider);
}
}
@media (max-width: 800px) {
#advisor-shell {
padding: 0 16px 32px;
}
#advisor-header .header-inner {
padding: 0 16px;
}
.hero-section {
padding: 80px 0 48px;
}
.hero-title {
font-size: 42px !important;
}
.kpi-grid {
grid-template-columns: 1fr;
}
}
"""
# ==================================================
# INIT
# ==================================================
def startup():
try:
init_db()
print("DB initialized successfully", flush=True)
except Exception as e:
print("DB failed:", e, flush=True)
# ==================================================
# HTML HELPERS
# ==================================================
def format_money(value):
try:
return f"£{float(value):,.2f}"
except (TypeError, ValueError):
return "£0.00"
def format_number(value):
try:
return f"{float(value):,.0f}"
except (TypeError, ValueError):
return "0"
def format_percent(value):
try:
numeric = float(value)
if abs(numeric) <= 1:
numeric *= 100
return f"{numeric:.2f}%"
except (TypeError, ValueError):
return "0.00%"
def build_hero_html(spend, leads, cpl, count):
return f"""
Spend {format_money(spend)} | Leads {leads} | Avg CPL {format_money(cpl)} | Campaigns {count}
AI-powered Google Ads optimization
{body}