Files
Google-Ads/app.py

876 lines
33 KiB
Python

"""
Google Ads Generator & Auditor - Interface Streamlit
Aplicação que extrai conteúdo de Landing Pages e gera automaticamente
ativos de campanha para Google Ads usando IA (OpenAI ou Gemini).
Inclui módulo de Auditoria (Google Ads Scorecard) para avaliação de qualidade.
Suporta estrutura multi-grupo (3 Ad Groups temáticos por campanha).
"""
import os
import re
import json
import time
import threading
import streamlit as st
from dotenv import load_dotenv
from src.scraper import scrape_landing_page
from src.ai_generator import generate_google_ads_assets, MODELS
from src.auditor import audit_campaign
from src.exporter import (
create_keywords_df,
create_negative_keywords_df,
create_ads_df,
create_sitelinks_df,
create_callouts_df,
export_all_to_excel,
)
# ─── Carregar variáveis de ambiente ───────────────────────────────
load_dotenv()
def _run_with_progress(fn, progress_bar, status_text, msg: str, start_pct: int = 30, end_pct: int = 95):
"""
Executa fn() em thread separada enquanto anima a barra de progresso
com indicador numérico crescente (ex: 30%, 35%, 40%...).
Não altera a lógica — apenas dá feedback visual durante chamadas longas.
"""
result = [None]
error = [None]
def _worker():
try:
result[0] = fn()
except Exception as e:
error[0] = e
thread = threading.Thread(target=_worker)
thread.start()
pct = start_pct
while thread.is_alive() and pct < end_pct:
pct = min(pct + 5, end_pct)
progress_bar.progress(pct, text=f"{msg} ({pct}%)")
status_text.info(f"🤖 {msg} ({pct}%)")
time.sleep(1.5)
thread.join()
if error[0]:
raise error[0]
return result[0]
def _build_json_export(assets: dict) -> str:
"""Monta o JSON de exportação no formato padronizado para Google Ads (multi-grupo)."""
# Ad Groups formatados
groups_formatted = []
for group in assets.get("ad_groups", []):
# Palavras-chave formatadas com tipo de correspondência
keywords_formatted = []
for kw in group.get("keywords", []):
keyword = kw.get("keyword", "")
match_type = kw.get("match_type", "Phrase")
if match_type == "Exact":
keywords_formatted.append(f"[{keyword}]")
elif match_type == "Phrase":
keywords_formatted.append(f'"{keyword}"')
else:
keywords_formatted.append(keyword)
groups_formatted.append({
"nome_grupo": group.get("name", "Grupo"),
"palavras_chave_fundo_de_funil": keywords_formatted,
"titulos_headlines": group.get("headlines", []),
"descricoes": group.get("descriptions", []),
})
# Palavras-chave negativas com motivo (nível campanha)
negative_kw_formatted = []
for nkw in assets.get("negative_keywords", []):
keyword = nkw.get("keyword", "")
reason = nkw.get("reason", "")
if reason:
negative_kw_formatted.append(f"{keyword} ({reason})")
else:
negative_kw_formatted.append(keyword)
# Sitelinks formatados (nível campanha)
sitelinks_formatted = []
for sl in assets.get("sitelinks", []):
sitelinks_formatted.append({
"titulo": sl.get("title", ""),
"desc_linha_1": sl.get("description1", ""),
"desc_linha_2": sl.get("description2", ""),
})
export_data = [
{
"campanha_google_ads": {
"ad_groups": groups_formatted,
"palavras_chave_negativas": negative_kw_formatted,
"sitelinks": sitelinks_formatted,
"callouts": assets.get("callouts", []),
}
}
]
return json.dumps(export_data, ensure_ascii=False, indent=2)
def _highlight_keywords(text: str, keywords: list[str]) -> str:
"""Destaca em negrito as palavras-chave encontradas no texto."""
result = text
for kw in keywords:
pattern = re.compile(re.escape(kw), re.IGNORECASE)
match = pattern.search(result)
if match:
result = result[:match.start()] + f"**{match.group()}**" + result[match.end():]
break # Destacar apenas a primeira keyword encontrada
return result
# ─── Configuração da Página ───────────────────────────────────────
st.set_page_config(
page_title="Google Ads Generator & Auditor",
page_icon="📊",
layout="wide",
)
# ─── CSS Customizado ──────────────────────────────────────────────
st.markdown("""
<style>
.main-header {
font-size: 2.2rem;
font-weight: 700;
color: #1a73e8;
margin-bottom: 0.2rem;
}
.sub-header {
font-size: 1.1rem;
color: #5f6368;
margin-bottom: 2rem;
}
.metric-card {
background: #f8f9fa;
border-radius: 12px;
padding: 1.2rem;
text-align: center;
border: 1px solid #e8eaed;
}
.metric-number {
font-size: 2rem;
font-weight: 700;
color: #1a73e8;
}
.metric-label {
font-size: 0.85rem;
color: #5f6368;
}
.stTabs [data-baseweb="tab-list"] {
gap: 8px;
}
.stTabs [data-baseweb="tab"] {
padding: 10px 20px;
border-radius: 8px 8px 0 0;
}
.score-big {
font-size: 4rem;
font-weight: 800;
text-align: center;
line-height: 1;
margin-bottom: 0.3rem;
}
.score-label {
font-size: 1rem;
text-align: center;
color: #5f6368;
}
.ad-preview {
background: #fff;
border: 1px solid #dadce0;
border-radius: 12px;
padding: 1.5rem;
max-width: 600px;
font-family: Arial, sans-serif;
margin-bottom: 1.5rem;
}
.ad-preview .ad-badge {
display: inline-block;
font-size: 0.75rem;
font-weight: 700;
color: #202124;
background: #f1f3f4;
border-radius: 4px;
padding: 2px 6px;
margin-bottom: 6px;
}
.ad-preview .ad-url {
font-size: 0.85rem;
color: #202124;
margin-bottom: 4px;
}
.ad-preview .ad-title {
font-size: 1.25rem;
color: #1a0dab;
text-decoration: none;
line-height: 1.3;
margin-bottom: 6px;
}
.ad-preview .ad-desc {
font-size: 0.9rem;
color: #4d5156;
line-height: 1.5;
}
.group-header {
font-size: 1rem;
font-weight: 600;
color: #1a73e8;
border-left: 4px solid #1a73e8;
padding-left: 0.8rem;
margin-bottom: 0.5rem;
}
</style>
""", unsafe_allow_html=True)
# ─── Sidebar ──────────────────────────────────────────────────────
with st.sidebar:
st.header("Navegação")
page = st.radio(
"Modo",
["Gerador de Campanha", "Auditor (Scorecard)"],
index=0,
help="Alterne entre gerar ativos e auditar campanhas existentes.",
)
st.divider()
st.header("Configurações")
provider = st.selectbox(
"Provider de IA",
["OpenAI", "Gemini"],
index=0,
help="Escolha o provider de IA. As chaves devem estar configuradas no arquivo .env",
)
model = st.selectbox(
"Modelo",
MODELS.get(provider, []),
index=0,
help="Modelo a ser utilizado para geração/auditoria.",
)
# Status das chaves configuradas
st.divider()
openai_ok = bool(os.environ.get("OPENAI_API_KEY"))
gemini_ok = bool(os.environ.get("GEMINI_API_KEY"))
st.caption("Status das API Keys (.env):")
st.markdown(f"- OpenAI: {'✅ Configurada' if openai_ok else '❌ Não encontrada'}")
st.markdown(f"- Gemini: {'✅ Configurada' if gemini_ok else '❌ Não encontrada'}")
st.divider()
campaign_name = st.text_input(
"Nome da Campanha",
value="Campanha LP",
help="Nome que aparecerá na coluna 'Campaign' do CSV.",
)
st.divider()
st.caption("Desenvolvido com Streamlit + OpenAI + Gemini")
# =====================================================================
# MODO: GERADOR DE CAMPANHA
# =====================================================================
def render_generator():
"""Renderiza a interface do Gerador de Campanha (multi-grupo)."""
st.markdown('<p class="main-header">Google Ads Generator</p>', unsafe_allow_html=True)
st.markdown(
'<p class="sub-header">Gere ativos de campanha automaticamente a partir da sua Landing Page (3 Grupos de Anúncio)</p>',
unsafe_allow_html=True,
)
# ─── Área Principal ───────────────────────────────────────────
url = st.text_input(
"URL da Landing Page",
placeholder="https://www.seusite.com.br/landing-page",
help="Cole a URL completa da sua Landing Page aqui.",
)
col_btn, col_status = st.columns([1, 3])
with col_btn:
generate_btn = st.button("Gerar Campanha", type="primary", use_container_width=True)
# ─── Lógica Principal ────────────────────────────────────────
if generate_btn:
# Validações
if not url:
st.error("Por favor, insira a URL da Landing Page.")
st.stop()
if not url.startswith(("http://", "https://")):
st.error("A URL deve começar com http:// ou https://")
st.stop()
# Verificar se a chave do provider selecionado está configurada
if provider == "OpenAI" and not os.environ.get("OPENAI_API_KEY"):
st.error("OPENAI_API_KEY não encontrada no arquivo .env. Configure antes de continuar.")
st.stop()
elif provider == "Gemini" and not os.environ.get("GEMINI_API_KEY"):
st.error("GEMINI_API_KEY não encontrada no arquivo .env. Configure antes de continuar.")
st.stop()
# Barra de progresso visual
progress_bar = st.progress(0, text="Iniciando...")
status_text = st.empty()
# Step 1: Scraping
progress_bar.progress(10, text="Extraindo conteúdo da Landing Page...")
status_text.info("🔍 Acessando a URL e extraindo conteúdo...")
try:
lp_data = scrape_landing_page(url)
except Exception as e:
progress_bar.empty()
status_text.empty()
st.error(f"Erro ao acessar a URL: {e}")
st.stop()
progress_bar.progress(30, text="Conteúdo extraído com sucesso!")
status_text.info(
f"📄 Extraído: {len(lp_data['paragraphs'])} parágrafos, "
f"{len(lp_data['h1'])} H1, {len(lp_data['h2'])} H2, "
f"{len(lp_data.get('list_items', []))} itens de lista, "
f"{len(lp_data['ctas'])} CTAs"
)
# Step 2: IA — executa em thread com progresso animado
try:
assets, prompts = _run_with_progress(
fn=lambda: generate_google_ads_assets(
lp_content=lp_data["full_text"],
provider=provider,
model=model,
),
progress_bar=progress_bar,
status_text=status_text,
msg=f"Gerando ativos com {provider} ({model})",
start_pct=35,
end_pct=95,
)
except ValueError as e:
progress_bar.empty()
status_text.empty()
st.error(str(e))
st.stop()
except Exception as e:
progress_bar.empty()
status_text.empty()
st.error(f"Erro ao gerar ativos: {e}")
st.stop()
progress_bar.progress(100, text="Campanha gerada com sucesso! (100%)")
status_text.success("✅ Campanha gerada com sucesso!")
# Salvar no session_state
st.session_state["assets"] = assets
st.session_state["prompts"] = prompts
st.session_state["lp_data"] = lp_data
st.session_state["campaign_name"] = campaign_name
st.session_state["provider_used"] = provider
st.session_state["model_used"] = model
# ─── Exibição dos Resultados ─────────────────────────────────
if "assets" in st.session_state:
assets = st.session_state["assets"]
prompts = st.session_state.get("prompts", {})
camp = st.session_state.get("campaign_name", "Campanha LP")
prov_used = st.session_state.get("provider_used", "")
model_used = st.session_state.get("model_used", "")
ad_groups = assets.get("ad_groups", [])
st.divider()
# Métricas resumo (totais agregados)
total_kw = sum(len(g.get("keywords", [])) for g in ad_groups)
total_headlines = sum(len(g.get("headlines", [])) for g in ad_groups)
total_desc = sum(len(g.get("descriptions", [])) for g in ad_groups)
col1, col2, col3, col4, col5, col6, col7 = st.columns(7)
with col1:
st.metric("Grupos", len(ad_groups))
with col2:
st.metric("Keywords", total_kw)
with col3:
st.metric("Negativas", len(assets.get("negative_keywords", [])))
with col4:
st.metric("Títulos", total_headlines)
with col5:
st.metric("Descrições", total_desc)
with col6:
st.metric("Sitelinks", len(assets.get("sitelinks", [])))
with col7:
st.metric("Callouts", len(assets.get("callouts", [])))
st.divider()
# Abas com resultados
tab_groups, tab_neg, tab_sl, tab_co, tab_prompts, tab_raw = st.tabs([
"Grupos de Anúncio",
"Negativas",
"Sitelinks",
"Callouts",
"Prompts Utilizados",
"Dados da LP",
])
with tab_groups:
st.subheader("Grupos de Anúncio (Ad Groups)")
if not ad_groups:
st.info("Nenhum grupo de anúncio gerado.")
else:
# Sub-abas para cada grupo
group_tab_names = [f"Grupo {i+1}: {g.get('name', '')}" for i, g in enumerate(ad_groups)]
group_tabs = st.tabs(group_tab_names)
for idx, (gtab, group) in enumerate(zip(group_tabs, ad_groups)):
with gtab:
group_name = group.get("name", f"Grupo {idx+1}")
st.markdown(f'<p class="group-header">{group_name}</p>', unsafe_allow_html=True)
# Keywords do grupo
st.write("**Palavras-chave:**")
group_kw = group.get("keywords", [])
if group_kw:
for i, kw in enumerate(group_kw, 1):
keyword = kw.get("keyword", "")
match_type = kw.get("match_type", "Phrase")
if match_type == "Exact":
formatted = f"[{keyword}]"
elif match_type == "Phrase":
formatted = f'"{keyword}"'
else:
formatted = keyword
st.markdown(f"{i}. `{formatted}` ({match_type})")
else:
st.info("Nenhuma keyword neste grupo.")
st.markdown("---")
# Headlines do grupo
st.write("**Títulos (Headlines):**")
kw_list = sorted(
[kw["keyword"].lower() for kw in group_kw if kw.get("keyword")],
key=len, reverse=True,
)
for i, h in enumerate(group.get("headlines", []), 1):
char_count = len(h)
color = "green" if char_count <= 30 else "red"
h_highlighted = _highlight_keywords(h, kw_list)
st.markdown(f"{i}. {h_highlighted} — :{color}[{char_count} chars]")
st.markdown("---")
# Descriptions do grupo
st.write("**Descrições:**")
for i, d in enumerate(group.get("descriptions", []), 1):
char_count = len(d)
color = "green" if char_count <= 90 else "red"
st.markdown(f"{i}. {d} — :{color}[{char_count} chars]")
st.markdown("---")
# DataFrame RSA do grupo
st.write("**Tabela RSA (formato Google Ads Editor):**")
# Montar assets temporário para o grupo individual
single_group_assets = {
"ad_groups": [group],
"negative_keywords": [],
"sitelinks": [],
"callouts": [],
}
ads_df = create_ads_df(single_group_assets, camp)
if not ads_df.empty:
st.dataframe(ads_df, use_container_width=True, hide_index=True)
# DataFrame consolidado de todas as keywords
st.markdown("---")
st.write("**Tabela Consolidada de Keywords (todos os grupos):**")
kw_df = create_keywords_df(assets, camp)
if not kw_df.empty:
st.dataframe(kw_df, use_container_width=True, hide_index=True)
with tab_neg:
st.subheader("Palavras-chave Negativas (Nível de Campanha)")
nkw_df = create_negative_keywords_df(assets, camp)
if not nkw_df.empty:
st.dataframe(nkw_df, use_container_width=True, hide_index=True)
else:
st.info("Nenhuma palavra-chave negativa gerada.")
with tab_sl:
st.subheader("Sitelinks (Nível de Campanha)")
sl_df = create_sitelinks_df(assets, camp)
if not sl_df.empty:
st.dataframe(sl_df, use_container_width=True, hide_index=True)
else:
st.info("Nenhum sitelink gerado.")
with tab_co:
st.subheader("Callouts — Frases de Destaque (Nível de Campanha)")
co_df = create_callouts_df(assets, camp)
if not co_df.empty:
st.dataframe(co_df, use_container_width=True, hide_index=True)
else:
st.info("Nenhum callout gerado.")
with tab_prompts:
st.subheader("Prompts Utilizados")
st.caption(f"Provider: **{prov_used}** | Modelo: **{model_used}**")
st.markdown("#### Prompt de Sistema (System Prompt)")
st.code(prompts.get("system_prompt", "N/A"), language=None)
st.markdown("#### Prompt do Usuário (User Prompt)")
st.code(prompts.get("user_prompt", "N/A"), language=None)
with tab_raw:
st.subheader("Dados Extraídos da Landing Page")
lp_data = st.session_state.get("lp_data", {})
if lp_data:
st.write(f"**URL:** {lp_data.get('url', '')}")
st.write(f"**Título:** {lp_data.get('title', '')}")
st.write(f"**Meta Description:** {lp_data.get('meta_description', '')}")
with st.expander("Headings (H1, H2, H3)"):
for h in lp_data.get("h1", []):
st.write(f"**H1:** {h}")
for h in lp_data.get("h2", []):
st.write(f"**H2:** {h}")
for h in lp_data.get("h3", []):
st.write(f"**H3:** {h}")
with st.expander("Itens de Lista / Benefícios"):
for li in lp_data.get("list_items", []):
st.write(f"- {li}")
with st.expander("CTAs Encontrados"):
for c in lp_data.get("ctas", []):
st.write(f"- {c}")
with st.expander("Texto Completo Enviado à IA"):
st.code(lp_data.get("full_text", ""), language=None)
# ─── Botões de Download ───────────────────────────────────
st.divider()
st.subheader("Download dos Ativos")
col_dl1, col_dl2, col_dl3 = st.columns(3)
with col_dl1:
excel_data = export_all_to_excel(assets, camp)
st.download_button(
label="Baixar Excel (.xlsx)",
data=excel_data,
file_name="google_ads_assets.xlsx",
mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
use_container_width=True,
)
with col_dl2:
kw_csv = create_keywords_df(assets, camp).to_csv(index=False)
st.download_button(
label="Baixar Keywords (.csv)",
data=kw_csv,
file_name="google_ads_keywords.csv",
mime="text/csv",
use_container_width=True,
)
with col_dl3:
json_export = _build_json_export(assets)
st.download_button(
label="Baixar JSON (.json)",
data=json_export,
file_name="google_ads_assets.json",
mime="application/json",
use_container_width=True,
)
# =====================================================================
# MODO: AUDITOR (SCORECARD)
# =====================================================================
def _get_score_color(score: float) -> str:
"""Retorna a cor com base na nota."""
if score >= 8.0:
return "#34a853" # verde
elif score >= 5.0:
return "#fbbc04" # amarelo
else:
return "#ea4335" # vermelho
def _get_criterion_emoji(nota: float, nota_maxima: float) -> str:
"""Retorna emoji com base na porcentagem atingida do critério."""
if nota_maxima == 0:
return ""
pct = nota / nota_maxima
if pct >= 0.9:
return ""
elif pct >= 0.6:
return "⚠️"
else:
return ""
def render_auditor():
"""Renderiza a interface do Auditor de Campanha (Google Ads Scorecard)."""
st.markdown('<p class="main-header">Google Ads Scorecard</p>', unsafe_allow_html=True)
st.markdown(
'<p class="sub-header">Audite a qualidade dos seus ativos de campanha com IA</p>',
unsafe_allow_html=True,
)
# ─── Fonte do JSON ────────────────────────────────────────────
has_session_json = "assets" in st.session_state
json_source = st.radio(
"Fonte do JSON para auditoria",
[
"Usar campanha da sessão atual" if has_session_json else "Usar campanha da sessão atual (nenhuma disponível)",
"Colar JSON manualmente",
],
index=0 if has_session_json else 1,
horizontal=True,
help="Escolha de onde vem o JSON da campanha a ser auditada.",
)
campaign_json = ""
if "sessão atual" in json_source and has_session_json:
# Construir o JSON a partir dos assets na sessão
campaign_json = _build_json_export(st.session_state["assets"])
with st.expander("Ver JSON da sessão atual", expanded=False):
st.code(campaign_json, language="json")
elif "sessão atual" in json_source and not has_session_json:
st.warning("Nenhuma campanha gerada na sessão atual. Gere uma campanha primeiro no modo 'Gerador' ou cole o JSON manualmente.")
return
else:
campaign_json = st.text_area(
"Cole o JSON da campanha aqui",
height=300,
placeholder='[{"campanha_google_ads": {"ad_groups": [...], "palavras_chave_negativas": [...], ...}}]',
help="Cole o JSON completo da campanha no formato de exportação.",
)
# ─── Botão de Auditoria ───────────────────────────────────────
col_audit_btn, _ = st.columns([1, 3])
with col_audit_btn:
audit_btn = st.button("Auditar Campanha", type="primary", use_container_width=True)
# ─── Lógica de Auditoria ─────────────────────────────────────
if audit_btn:
if not campaign_json or not campaign_json.strip():
st.error("Por favor, forneça o JSON da campanha para auditoria.")
st.stop()
# Validar JSON
try:
json.loads(campaign_json)
except json.JSONDecodeError:
st.error("O JSON fornecido é inválido. Verifique a formatação e tente novamente.")
st.stop()
# Verificar chave da API
if provider == "OpenAI" and not os.environ.get("OPENAI_API_KEY"):
st.error("OPENAI_API_KEY não encontrada no arquivo .env. Configure antes de continuar.")
st.stop()
elif provider == "Gemini" and not os.environ.get("GEMINI_API_KEY"):
st.error("GEMINI_API_KEY não encontrada no arquivo .env. Configure antes de continuar.")
st.stop()
# Barra de progresso visual
progress_bar = st.progress(0, text="Iniciando auditoria...")
status_text = st.empty()
progress_bar.progress(15, text="Validando JSON da campanha...")
status_text.info("📋 JSON validado. Preparando envio para IA...")
# Executa auditoria em thread com progresso animado
try:
audit_result, audit_prompts = _run_with_progress(
fn=lambda: audit_campaign(
campaign_json=campaign_json,
provider=provider,
model=model,
),
progress_bar=progress_bar,
status_text=status_text,
msg=f"Analisando campanha com {provider} ({model})",
start_pct=25,
end_pct=95,
)
except ValueError as e:
progress_bar.empty()
status_text.empty()
st.error(str(e))
st.stop()
except json.JSONDecodeError:
progress_bar.empty()
status_text.empty()
st.error("A IA retornou uma resposta inválida. Tente novamente ou troque de modelo.")
st.stop()
except Exception as e:
progress_bar.empty()
status_text.empty()
st.error(f"Erro durante auditoria: {e}")
st.stop()
progress_bar.progress(100, text="Auditoria concluída! (100%)")
status_text.success("✅ Auditoria concluída com sucesso!")
# Salvar no session_state
st.session_state["audit_result"] = audit_result
st.session_state["audit_prompts"] = audit_prompts
# ─── Exibição dos Resultados da Auditoria ────────────────────
if "audit_result" in st.session_state:
audit_result = st.session_state["audit_result"]
audit_prompts = st.session_state.get("audit_prompts", {})
st.divider()
# ─── Score Principal ──────────────────────────────────────
nota = audit_result["nota_final"]
cor = _get_score_color(nota)
col_score, col_resumo = st.columns([1, 3])
with col_score:
st.markdown(
f'<div style="text-align:center; padding:1rem;">'
f'<p class="score-big" style="color:{cor}">{nota:.1f}</p>'
f'<p class="score-label">de 10.0 pontos</p>'
f'</div>',
unsafe_allow_html=True,
)
with col_resumo:
st.markdown("#### Resumo da Auditoria")
st.info(audit_result.get("resumo", "Sem resumo disponível."))
st.divider()
# ─── Abas de Resultado ────────────────────────────────────
tab_criterios, tab_simulacao, tab_audit_prompts = st.tabs([
"Critérios de Avaliação",
"Simulação de Anúncio (CTR)",
"Prompts da Auditoria",
])
with tab_criterios:
st.subheader("Detalhamento por Critério")
for criterio in audit_result.get("criterios", []):
nome = criterio.get("nome", "")
nota_c = criterio.get("nota", 0)
nota_max = criterio.get("nota_maxima", 0)
peso = criterio.get("peso", 0)
falhas = criterio.get("falhas", [])
sugestoes = criterio.get("sugestoes", [])
emoji = _get_criterion_emoji(nota_c, nota_max)
with st.expander(f"{emoji} {nome}{nota_c:.1f} / {nota_max:.1f} (peso {peso:.1f})", expanded=True):
# Barra de progresso visual
pct = nota_c / nota_max if nota_max > 0 else 0
st.progress(min(pct, 1.0))
if falhas:
st.markdown("**Falhas encontradas:**")
for f in falhas:
st.markdown(f"- :red[{f}]")
if sugestoes:
st.markdown("**Sugestões de melhoria:**")
for s in sugestoes:
st.markdown(f"- :blue[{s}]")
if not falhas and not sugestoes:
st.success("Nenhuma falha encontrada neste critério.")
with tab_simulacao:
st.subheader("Simulação Visual dos Anúncios")
st.caption("Como os anúncios de cada grupo apareceriam nos resultados de pesquisa do Google:")
simulacoes = audit_result.get("simulacoes_anuncio", [])
if not simulacoes:
st.info("Nenhuma simulação de anúncio disponível.")
else:
for sim_idx, sim in enumerate(simulacoes):
grupo_nome = sim.get("grupo", f"Grupo {sim_idx + 1}")
titulo1 = sim.get("titulo_linha_1", "")
titulo2 = sim.get("titulo_linha_2", "")
titulo3 = sim.get("titulo_linha_3", "")
url_display = sim.get("url_display", "www.exemplo.com.br")
descricao = sim.get("descricao", "")
# Montar título composto
titulos = [t for t in [titulo1, titulo2, titulo3] if t]
titulo_completo = " | ".join(titulos)
st.markdown(f'<p class="group-header">{grupo_nome}</p>', unsafe_allow_html=True)
st.markdown(
f"""
<div class="ad-preview">
<div class="ad-badge">Patrocinado</div>
<div class="ad-url">{url_display}</div>
<div class="ad-title">{titulo_completo}</div>
<div class="ad-desc">{descricao}</div>
</div>
""",
unsafe_allow_html=True,
)
# Métricas dos títulos individuais
st.markdown("**Títulos selecionados para simulação:**")
for i, t in enumerate(titulos, 1):
char_count = len(t)
color = "green" if char_count <= 30 else "red"
st.markdown(f"{i}. {t} — :{color}[{char_count} chars]")
if descricao:
char_desc = len(descricao)
color_desc = "green" if char_desc <= 90 else "red"
st.markdown(f"**Descrição:** {descricao} — :{color_desc}[{char_desc} chars]")
if sim_idx < len(simulacoes) - 1:
st.markdown("---")
with tab_audit_prompts:
st.subheader("Prompts Utilizados na Auditoria")
st.caption(f"Provider: **{provider}** | Modelo: **{model}**")
st.markdown("#### Prompt de Sistema (System Prompt)")
st.code(audit_prompts.get("system_prompt", "N/A"), language=None)
st.markdown("#### Prompt do Usuário (User Prompt)")
st.code(audit_prompts.get("user_prompt", "N/A"), language=None)
# =====================================================================
# ROTEAMENTO DE PÁGINA
# =====================================================================
if page == "Gerador de Campanha":
render_generator()
else:
render_auditor()