@danhachuel/thunderbolt 0.5.40 → 0.5.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/main.py +83 -1
- package/package.json +1 -1
package/app/main.py
CHANGED
|
@@ -1442,6 +1442,88 @@ def render_home_update_controls() -> None:
|
|
|
1442
1442
|
st.rerun()
|
|
1443
1443
|
|
|
1444
1444
|
|
|
1445
|
+
def _growth_score_color(score: float) -> str:
|
|
1446
|
+
if score <= 30:
|
|
1447
|
+
return "#ef4444"
|
|
1448
|
+
if score < 70:
|
|
1449
|
+
return "#f59e0b"
|
|
1450
|
+
return "#22c55e"
|
|
1451
|
+
|
|
1452
|
+
|
|
1453
|
+
def _growth_channel_metrics(channel: dict[str, Any]) -> tuple[list[dict[str, Any]], float]:
|
|
1454
|
+
"""Calculate transparent local growth notes without fabricating Studio metrics."""
|
|
1455
|
+
channel_id = str(channel.get("id") or "")
|
|
1456
|
+
tasks = [item for item in read_json("tasks.json", []) if isinstance(item, dict) and str(item.get("channel_id") or "") == channel_id]
|
|
1457
|
+
completed = [item for item in tasks if str(item.get("state") or item.get("status") or "").lower() == "done"]
|
|
1458
|
+
with_thumbnail = [item for item in tasks if str(item.get("thumbnail_path") or (item.get("artifacts") or {}).get("thumbnail") or "").strip()]
|
|
1459
|
+
with_title = [item for item in tasks if str(item.get("title") or "").strip()]
|
|
1460
|
+
subscriber_count = channel.get("subscriber_count")
|
|
1461
|
+
view_count = channel.get("view_count")
|
|
1462
|
+
video_count = channel.get("video_count")
|
|
1463
|
+
|
|
1464
|
+
def number(value: Any) -> float | None:
|
|
1465
|
+
try:
|
|
1466
|
+
return float(value) if value is not None and str(value).strip() else None
|
|
1467
|
+
except (TypeError, ValueError):
|
|
1468
|
+
return None
|
|
1469
|
+
|
|
1470
|
+
subscribers, views, videos = number(subscriber_count), number(view_count), number(video_count)
|
|
1471
|
+
demand = 70 if views is not None and views >= 1_000_000 else 55 if views is not None and views >= 100_000 else 50
|
|
1472
|
+
thumbnail = round((len(with_thumbnail) / len(tasks)) * 100) if tasks else 50
|
|
1473
|
+
title = round((len(with_title) / len(tasks)) * 100) if tasks else 50
|
|
1474
|
+
conversion = round(min(100, (subscribers / views) * 10000)) if subscribers is not None and views and views > 0 else 50
|
|
1475
|
+
cadence = round(min(100, len(tasks) * 12.5)) if tasks else 50
|
|
1476
|
+
metrics = [
|
|
1477
|
+
{"label": "Demanda validada", "score": demand, "value": f"{views:,.0f} visualizações" if views is not None else "Sem dados de visualizações", "source": "Canal YouTube" if views is not None else "Requer dados do canal"},
|
|
1478
|
+
{"label": "Qualidade da thumbnail", "score": thumbnail, "value": f"{len(with_thumbnail)}/{len(tasks)} tarefas com thumbnail" if tasks else "Sem tarefas analisadas", "source": "Artefactos locais"},
|
|
1479
|
+
{"label": "Qualidade do título", "score": title, "value": f"{len(with_title)}/{len(tasks)} tarefas com título" if tasks else "Sem tarefas analisadas", "source": "Artefactos locais"},
|
|
1480
|
+
{"label": "Hook — retenção inicial", "score": 50, "value": "Sem dados de retenção", "source": "Requer YouTube Studio"},
|
|
1481
|
+
{"label": "Ritmo e edição", "score": 50, "value": "Sem gráfico de retenção", "source": "Requer YouTube Studio"},
|
|
1482
|
+
{"label": "Origem do tráfego", "score": 50, "value": "Sem distribuição de tráfego", "source": "Requer YouTube Studio"},
|
|
1483
|
+
{"label": "Conversão em inscritos", "score": conversion, "value": f"{subscribers:,.0f} inscritos / {views:,.0f} visualizações" if subscribers is not None and views is not None else "Sem dados de inscritos e visualizações", "source": "Canal YouTube" if subscribers is not None and views is not None else "Requer dados do canal"},
|
|
1484
|
+
{"label": "Cadência de publicação", "score": cadence, "value": f"{len(tasks)} tarefas locais" if tasks else "Sem tarefas locais", "source": "Pipeline local"},
|
|
1485
|
+
]
|
|
1486
|
+
available = [metric["score"] for metric in metrics if metric["source"] not in {"Requer YouTube Studio", "Requer dados do canal"}]
|
|
1487
|
+
overall = round(sum(available) / len(available)) if available else 50
|
|
1488
|
+
return metrics, overall
|
|
1489
|
+
|
|
1490
|
+
|
|
1491
|
+
def render_growth_youtube():
|
|
1492
|
+
st.title("Analista Growth Youtube")
|
|
1493
|
+
st.caption("Auditoria de crescimento baseada nos 3 pilares críticos e nas métricas secundárias do agente Growth.")
|
|
1494
|
+
channels = [item for item in read_json("channels.json", []) if isinstance(item, dict) and item.get("active", True) and is_youtube_channel_record(item)]
|
|
1495
|
+
if not channels:
|
|
1496
|
+
st.info("Cadastre pelo menos um canal YouTube em Canais YouTube para iniciar a análise.")
|
|
1497
|
+
return
|
|
1498
|
+
selected = st.selectbox("Canal a analisar", channels, format_func=lambda item: str(item.get("name") or "Canal sem nome"), key="growth_youtube_channel")
|
|
1499
|
+
metrics, overall = _growth_channel_metrics(selected)
|
|
1500
|
+
color = _growth_score_color(overall)
|
|
1501
|
+
st.markdown(
|
|
1502
|
+
f'<div style="border:1px solid #263447;border-radius:12px;padding:18px;margin:12px 0 20px;background:#101722;display:flex;align-items:center;justify-content:space-between;">'
|
|
1503
|
+
f'<div><div style="font-size:13px;color:#94a3b8;text-transform:uppercase;letter-spacing:.08em;">Nota geral do canal</div><div style="font-size:18px;font-weight:700;margin-top:5px;">{str(selected.get("name") or "Canal")}</div></div>'
|
|
1504
|
+
f'<div style="font-size:42px;font-weight:800;color:{color};line-height:1;">{overall}<span style="font-size:16px;color:#94a3b8;">/100</span></div></div>',
|
|
1505
|
+
unsafe_allow_html=True,
|
|
1506
|
+
)
|
|
1507
|
+
st.subheader("Dashboard de Growth")
|
|
1508
|
+
st.caption("Vermelho: 0–30 · Amarelo: 31–69 · Verde: 70–100. Métricas sem fonte local aparecem explicitamente como sem dados.")
|
|
1509
|
+
for row_start in range(0, len(metrics), 4):
|
|
1510
|
+
cols = st.columns(4, gap="small")
|
|
1511
|
+
for col, metric in zip(cols, metrics[row_start:row_start + 4]):
|
|
1512
|
+
with col:
|
|
1513
|
+
score = metric["score"]
|
|
1514
|
+
st.markdown(
|
|
1515
|
+
f'<div style="min-height:142px;border:1px solid #263447;border-radius:10px;padding:14px;background:#0d141e;">'
|
|
1516
|
+
f'<div style="font-size:13px;color:#cbd5e1;min-height:34px;">{metric["label"]}</div>'
|
|
1517
|
+
f'<div style="font-size:30px;font-weight:800;color:{_growth_score_color(score)};">{score}<span style="font-size:13px;color:#94a3b8;">/100</span></div>'
|
|
1518
|
+
f'<div style="font-size:12px;color:#e2e8f0;margin-top:5px;">{metric["value"]}</div>'
|
|
1519
|
+
f'<div style="font-size:11px;color:#64748b;margin-top:5px;">Fonte: {metric["source"]}</div></div>',
|
|
1520
|
+
unsafe_allow_html=True,
|
|
1521
|
+
)
|
|
1522
|
+
st.divider()
|
|
1523
|
+
st.subheader("Leitura estratégica")
|
|
1524
|
+
st.write("Priorize primeiro as métricas abaixo de 70. A referência do agente indica que Thumbnail + Título devem ser optimizados antes de alterações de conteúdo; Hook, Ritmo e Tráfego ficam pendentes de dados do YouTube Studio.")
|
|
1525
|
+
|
|
1526
|
+
|
|
1445
1527
|
def render_dashboard():
|
|
1446
1528
|
ui_language = current_ui_language()
|
|
1447
1529
|
st.title("Thunderbolt")
|
|
@@ -8668,7 +8750,7 @@ def main():
|
|
|
8668
8750
|
"Motion Control": lambda: render_motion_control(read_json("settings.json", {})),
|
|
8669
8751
|
"UGC Products": lambda: render_ugc_products(read_json("settings.json", {})),
|
|
8670
8752
|
"Redes Sociais": lambda: render_edit_placeholder("Redes Sociais", "Área reservada para a futura funcionalidade de redes sociais."),
|
|
8671
|
-
"Analista Growth Youtube":
|
|
8753
|
+
"Analista Growth Youtube": render_growth_youtube,
|
|
8672
8754
|
"Analista Growth Tiktok": lambda: render_edit_placeholder("Analista Growth Tiktok", ""),
|
|
8673
8755
|
"Analista Growth Instagram": lambda: render_edit_placeholder("Analista Growth Instagram", ""),
|
|
8674
8756
|
"Analista Facebook Pages": lambda: render_edit_placeholder("Analista Facebook Pages", ""),
|
package/package.json
CHANGED