metaads-pro/src/app/api/analysis/route.ts

217 lines
8.5 KiB
TypeScript
Raw Normal View History

import { NextRequest, NextResponse } from 'next/server';
import { createMetaMCPClient } from '@/lib/mcp/client';
import { getCampaignDetails } from '@/lib/mcp/tools';
import { getAccessToken } from '@/lib/auth/session';
import { getAIClient } from '@/lib/ai/client';
const ANALYSIS_MODEL = 'gpt-5.4-mini';
// Extrai o valor de uma action pelo tipo
function findAction(actions: any[], type: string): number {
return parseInt(actions.find((a: any) => a.action_type === type)?.value || '0', 10);
}
// Rótulo legível do objetivo da campanha
function objectiveLabel(objective: string): string {
const map: Record<string, string> = {
OUTCOME_LEADS: 'Geração de Leads',
OUTCOME_SALES: 'Vendas (Conversões)',
OUTCOME_TRAFFIC: 'Tráfego',
OUTCOME_AWARENESS: 'Reconhecimento de Marca',
OUTCOME_ENGAGEMENT: 'Engajamento',
OUTCOME_APP_PROMOTION: 'Promoção de App',
};
return map[objective] || objective;
}
export async function POST(request: NextRequest) {
try {
const token = await getAccessToken();
if (!token) {
return NextResponse.json({ success: false, error: 'Not authenticated' }, { status: 401 });
}
const { campaignId } = await request.json();
if (!campaignId) {
return NextResponse.json({ success: false, error: 'Campaign ID required' }, { status: 400 });
}
const client = await createMetaMCPClient(token);
let campaignData: any;
try {
const result = await getCampaignDetails(client, campaignId);
campaignData = Array.isArray(result) ? result[0] : result;
} finally {
await client.close();
}
if (!campaignData || !campaignData.id) {
return NextResponse.json({ success: false, error: 'Campaign not found on Meta' }, { status: 404 });
}
const insights = campaignData.insights?.data?.[0] || {};
const adsets = campaignData.adsets?.data || [];
const actions = insights.actions || [];
const objective = campaignData.objective || '';
const spend = parseFloat(insights.spend || '0');
const ctr = parseFloat(insights.ctr || '0');
const cpc = parseFloat(insights.cpc || '0');
const cpm = parseFloat(insights.cpm || '0');
const reach = parseInt(insights.reach || '0', 10);
const impressions = parseInt(insights.impressions || '0', 10);
const leads = findAction(actions, 'lead') || findAction(actions, 'onsite_conversion.lead_grouped');
const purchases = findAction(actions, 'omni_purchase');
const roasRaw = parseFloat(insights.purchase_roas?.[0]?.value || '0');
const cpl = leads > 0 ? spend / leads : 0;
const cpa = purchases > 0 ? spend / purchases : 0;
// Extract Ads & Creatives
const adsToAnalyze: any[] = [];
for (const adset of adsets) {
for (const ad of adset.ads?.data || []) {
const adIns = ad.insights?.data?.[0] || {};
const adActions = adIns.actions || [];
const creative = ad.creative || {};
let body = creative.body || '';
let title = creative.title || '';
let imageUrl = creative.image_url || creative.thumbnail_url || '';
if (creative.object_story_spec?.link_data) {
const ld = creative.object_story_spec.link_data;
body = body || ld.message || '';
title = title || ld.name || '';
}
if (creative.asset_feed_spec) {
const feed = creative.asset_feed_spec;
body = body || feed.bodies?.map((b: any) => b.text).join(' | ') || '';
title = title || feed.titles?.map((t: any) => t.text).join(' | ') || '';
if (!imageUrl && feed.images?.length > 0) {
imageUrl = feed.images[0].url || feed.images[0].thumbnail_url || '';
}
}
const adLeads = findAction(adActions, 'lead') || findAction(adActions, 'onsite_conversion.lead_grouped');
const adPurchases = findAction(adActions, 'omni_purchase');
const adSpend = parseFloat(adIns.spend || '0');
adsToAnalyze.push({
ad_name: ad.name,
spend: adSpend.toFixed(2),
ctr: parseFloat(adIns.ctr || '0').toFixed(2) + '%',
cpc: 'R$ ' + parseFloat(adIns.cpc || '0').toFixed(2),
leads: adLeads,
purchases: adPurchases,
cpl: adLeads > 0 ? 'R$ ' + (adSpend / adLeads).toFixed(2) : null,
cpa: adPurchases > 0 ? 'R$ ' + (adSpend / adPurchases).toFixed(2) : null,
roas: adIns.purchase_roas?.[0]?.value ? parseFloat(adIns.purchase_roas[0].value).toFixed(2) + 'x' : null,
body: body || '(sem texto)',
title: title || '(sem título)',
image_url: imageUrl,
});
}
}
// Monta o contexto de métricas de forma objetiva para o tipo de campanha
const isLeadGen = objective === 'OUTCOME_LEADS' || (leads > 0 && purchases === 0);
const isSales = objective === 'OUTCOME_SALES' || purchases > 0;
const metricsBlock = [
`Objetivo: ${objectiveLabel(objective)}`,
`Gasto (30d): R$ ${spend.toFixed(2)}`,
`Alcance: ${reach.toLocaleString('pt-BR')} pessoas`,
`Impressões: ${impressions.toLocaleString('pt-BR')}`,
`CTR: ${ctr.toFixed(2)}%`,
`CPC: R$ ${cpc.toFixed(2)}`,
`CPM: R$ ${cpm.toFixed(2)}`,
leads > 0 ? `Leads gerados: ${leads} | CPL: R$ ${cpl.toFixed(2)}` : null,
purchases > 0 ? `Compras: ${purchases} | CPA: R$ ${cpa.toFixed(2)} | ROAS: ${roasRaw.toFixed(2)}x` : null,
].filter(Boolean).join('\n');
const focusInstructions = isLeadGen
? 'Esta é uma campanha de LEADS. Priorize: CPL (custo por lead), volume de leads, qualidade do copy para conversão em formulário.'
: isSales
? 'Esta é uma campanha de VENDAS. Priorize: ROAS, CPA (custo por compra), volume de compras e lucratividade.'
: 'Esta é uma campanha de TRÁFEGO. Priorize: CTR, CPC, qualidade dos cliques e relevância do público.';
const openai = getAIClient();
const prompt = `Você é um Especialista Sênior em Tráfego Pago e Copywriting com foco no mercado brasileiro.
Faça uma auditoria completa e honesta desta campanha no Meta Ads.
${focusInstructions}
DADOS DA CAMPANHA:
Nome: ${campaignData.name}
${metricsBlock}
ANÚNCIOS (CRIATIVOS E MÉTRICAS INDIVIDUAIS):
${JSON.stringify(adsToAnalyze, null, 2)}
Retorne SOMENTE um JSON válido, sem markdown, com esta estrutura exata:
{
"diagnostico_geral": "Parágrafo forte resumindo saúde da campanha, mencionando a métrica-chave do objetivo (CPL para leads, ROAS/CPA para vendas, CTR para tráfego).",
"analise_copy": ["Ponto sobre texto do anúncio X...", "Sugestão específica de melhoria..."],
"analise_imagem": ["Avaliação do visual...", "Sugestão de formato ou abordagem..."],
"analise_metricas": ["Métrica X está Y — o que significa...", "Comparativo com benchmark..."],
"acao_recomendada": ["Ação imediata 1...", "Ação 2..."],
"nova_estrategia_sugerida": {
"nicho_sugerido": "ecommerce / info_produto / negocios_locais / etc",
"produto_foco": "Produto ou oferta que deve ser o foco",
"publico_alvo": "Público ideal baseado nos dados",
"angulos_de_venda": ["Ângulo 1", "Ângulo 2"]
}
}`;
const completion = await openai.chat.completions.create({
model: ANALYSIS_MODEL,
messages: [{ role: 'user', content: prompt }],
response_format: { type: 'json_object' },
});
const textRes = completion.choices[0]?.message?.content || '';
let jsonRes: any;
try {
const cleaned = textRes.replace(/^```json/m, '').replace(/```$/m, '').trim();
jsonRes = JSON.parse(cleaned);
} catch {
console.error('[Analysis] Failed to parse AI response:', textRes);
return NextResponse.json({ success: false, error: 'Failed to parse AI response' }, { status: 500 });
}
return NextResponse.json({
success: true,
data: {
campaign_metrics: {
spend: spend.toFixed(2),
ctr: ctr.toFixed(2),
cpc: cpc.toFixed(2),
cpm: cpm.toFixed(2),
reach,
impressions,
leads,
purchases,
cpl: cpl > 0 ? cpl.toFixed(2) : null,
cpa: cpa > 0 ? cpa.toFixed(2) : null,
roas: roasRaw > 0 ? roasRaw.toFixed(2) : null,
objective,
objective_label: objectiveLabel(objective),
},
ads_count: adsToAnalyze.length,
ads: adsToAnalyze,
analysis: jsonRes,
},
});
} catch (error) {
console.error('[Analysis] API Error:', error);
return NextResponse.json(
{ success: false, error: error instanceof Error ? error.message : 'Unknown error' },
{ status: 500 }
);
}
}