File: /home/storage/5/78/dd/wicomm2/public_html/blackfriday/api/evaluate.php
<?php
// ===== evaluate.php =====
// Classifica maturidade, monta roadmap (determinístico + IA) e
// BUSCA Lighthouse no Master Data (LH) por scan_id; fallback: storage local > cookie.
require_once __DIR__ . '/../config/config.php';
@date_default_timezone_set(defined('APP_TIMEZONE') ? APP_TIMEZONE : 'America/Sao_Paulo');
if (!is_dir(STORAGE_DIR)) @mkdir(STORAGE_DIR, 0775, true);
// -------------------- HELPERS --------------------
function json_out($data, $code = 200) {
if (!headers_sent()) {
http_response_code($code);
header('Content-Type: application/json; charset=utf-8');
}
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
function require_post_json() {
$raw = file_get_contents('php://input');
$j = json_decode($raw, true);
if (!is_array($j)) json_out(['ok'=>false,'error'=>'Invalid JSON body'], 400);
return $j;
}
function clamp100($v) {
$n = is_numeric($v) ? floatval($v) : 0;
return max(0, min(100, $n));
}
function summarize_answers_pairs($list, $maxItems = 10) {
$out = [];
foreach ((array)$list as $item) {
$q = $item['pergunta'] ?? $item['title'] ?? $item['text'] ?? '';
$r = $item['resposta'] ?? $item['answer'] ?? '';
$q = trim((string)$q);
$r = trim((string)$r);
if ($q === '' && $r === '') continue;
$out[] = "• ".$q." → ".$r;
if (count($out) >= $maxItems) break;
}
return implode("\n", $out);
}
// -------------------- VTEX MD (LH): GET por scanId --------------------
function vtex_md_get_LH_by_scanId($scanId) {
if (!$scanId) return [null, 'missing scanId'];
$host = 'https://' . VTEX_ACCOUNT . '.' . VTEX_ENVIRONMENT . '.com.br';
$url = $host . '/api/dataentities/LH/search?_fields=scanId,payload&_where=scanId=' . rawurlencode($scanId) . '&_schema=1';
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Accept: application/json',
'X-VTEX-API-AppKey: ' . VTEX_APPKEY,
'X-VTEX-API-AppToken: ' . VTEX_APPTOKEN,
],
CURLOPT_TIMEOUT => 15,
CURLOPT_CONNECTTIMEOUT => 8,
]);
$body = curl_exec($ch);
$err = curl_error($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($code >= 400 || !$body) return [null, $body ?: $err ?: ("HTTP ".$code)];
$arr = json_decode($body, true);
if (!is_array($arr) || !count($arr)) return [null, 'not_found'];
return [$arr[0], null];
}
// -------------------- Lighthouse summary a partir de fontes múltiplas --------------------
function lighthouse_summary_from_sources($scanId, $storageDir) {
// 1) Tenta Master Data LH
if ($scanId) {
[$doc, $err] = vtex_md_get_LH_by_scanId($scanId);
if ($doc && isset($doc['payload'])) {
$raw = json_decode($doc['payload'], true);
if (is_array($raw) && isset($raw['lighthouseResult'])) {
return build_lh_summary($raw);
}
}
}
// 2) Storage local
if ($scanId) {
$path = rtrim($storageDir,'/').'/scan_'.$scanId.'.json';
if (is_file($path)) {
$raw = json_decode(file_get_contents($path), true);
if (is_array($raw) && isset($raw['lighthouseResult'])) {
return build_lh_summary($raw);
}
}
}
// 3) Cookie (resumo)
if (!empty($_COOKIE['fridayup_lighthouse'])) {
$sum = json_decode($_COOKIE['fridayup_lighthouse'], true);
if (is_array($sum)) {
return [
'score' => $sum['score'] ?? null,
'metrics' => $sum['metrics'] ?? [],
'oportunidades' => $sum['opportunidades'] ?? []
];
}
}
// Falhou → vazio
return ['score'=>null, 'metrics'=>[], 'oportunidades'=>[]];
}
function build_lh_summary($raw) {
$lh = $raw['lighthouseResult'] ?? [];
$aud = $lh['audits'] ?? [];
$cats = $lh['categories'] ?? [];
$score = (int) round((($cats['performance']['score'] ?? 0) * 100));
$disp = function($id) use ($aud) { return $aud[$id]['displayValue'] ?? ''; };
$opps = [];
foreach ($aud as $id => $a) {
if (isset($a['details']['type']) && $a['details']['type'] === 'opportunity') {
$opps[] = [
'id' => $id,
'title' => $a['title'] ?? $id,
'savingsMs' => $a['details']['overallSavingsMs'] ?? 0
];
}
}
usort($opps, fn($A,$B) => ($B['savingsMs'] ?? 0) <=> ($A['savingsMs'] ?? 0));
$opps = array_slice($opps, 0, 10);
return [
'score' => clamp100($score),
'metrics' => [
'lcp' => $disp('largest-contentful-paint'),
'tbt' => $disp('total-blocking-time'),
'cls' => $disp('cumulative-layout-shift'),
'interactive' => $disp('interactive'),
],
'oportunidades' => $opps
];
}
// -------------------- OPENAI: CLASSIFY MATURITY (1 palavra) --------------------
function openai_classify_maturity($answers, $apiKey, $model) {
$pairs = summarize_answers_pairs($answers, 8);
$payload = [
'model' => $model ?: 'gpt-4o-mini',
'temperature' => 0.0,
'messages' => [
['role'=>'system','content'=>'Você é um avaliador de maturidade para Black Friday. Regra: responda apenas uma palavra: basico, intermediario ou avancado.'],
['role'=>'user','content'=>"Responda APENAS uma palavra (sem pontuação): basico, intermediario ou avancado.\n\nQuiz:\n".$pairs],
]
];
$ch = curl_init('https://api.openai.com/v1/chat/completions');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer '.OPENAI_API_KEY
],
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_CONNECTTIMEOUT => 8,
CURLOPT_TIMEOUT => 20,
]);
$body = curl_exec($ch);
$err = curl_error($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($code >= 400 || !$body) throw new Exception("OpenAI error $code: ".($body ?: $err ?: ''));
$j = json_decode($body, true);
$txt = trim($j['choices'][0]['message']['content'] ?? '');
$txt = mb_strtolower($txt, 'UTF-8');
if (strpos($txt, 'inter') === 0) return 'intermediario';
if (strpos($txt, 'av') === 0) return 'avancado';
return 'basico';
}
// -------------------- OPENAI: EXPLICAÇÃO CURTA DA MATURIDADE --------------------
function openai_explain_maturity($maturidade, $answersCore, $answersExtra, $lhSummary) {
// Resumo compacto das respostas
$core = summarize_answers_pairs($answersCore, 8);
$extra = summarize_answers_pairs($answersExtra, 12);
// Compacta Lighthouse (se houver)
$lh_str = '';
if (is_array($lhSummary) && !empty($lhSummary)) {
$score = $lhSummary['score'] ?? null;
$m = $lhSummary['metrics'] ?? [];
$lh_str =
"Score: ".($score === null ? 'n/a' : $score)."; ".
"LCP: ".($m['lcp'] ?? 'n/a')."; ".
"TBT: ".($m['tbt'] ?? 'n/a')."; ".
"CLS: ".($m['cls'] ?? 'n/a')."; ".
"Interactive: ".($m['interactive'] ?? 'n/a');
}
$system = <<<SYS
Você é um consultor sênior de e-commerce. Explique em 3 a 5 linhas, em português do Brasil, por que a operação foi classificada no nível de maturidade informado.
Regras:
- Seja específico, cite 2–3 motivos objetivos (ex.: existência/ausência de processos, governança por canal/coorte, consistência de metas, preparação para Black Friday, sinais de performance do Lighthouse).
- Não cite tecnologias ou plataformas.
- Não faça lista; escreva apenas um parágrafo corrido, claro e direto.
SYS;
$user = "NÍVEL: {$maturidade}\nRESPOSTAS-CORE:\n{$core}\nRESPOSTAS-EXTRA:\n{$extra}\nLIGHTHOUSE:\n{$lh_str}\n\nEscreva a explicação (2–3 linhas).";
$payload = [
'model' => OPENAI_MODEL,
'temperature' => 0.7,
'messages' => [
['role'=>'system','content'=>$system],
['role'=>'user','content'=>$user],
]
];
$ch = curl_init('https://api.openai.com/v1/chat/completions');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer '.OPENAI_API_KEY
],
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_CONNECTTIMEOUT => 8,
CURLOPT_TIMEOUT => 20,
]);
$body = curl_exec($ch);
$err = curl_error($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($code >= 400 || !$body) {
// Em caso de falha, devolve uma frase curta padrão
return "Classificação como ".ucfirst($maturidade).": explicação automatizada indisponível no momento.";
}
$j = json_decode($body, true);
$txt = trim($j['choices'][0]['message']['content'] ?? '');
// Remove quebras excessivas
$txt = preg_replace('/\s+/', ' ', $txt);
return $txt !== '' ? $txt : ("Classificação como ".ucfirst($maturidade).".");
}
// -------------------- OPENAI: ROADMAP (estratégico, sem plataforma) --------------------
function openai_tailored_roadmap($maturidade, $answersCore, $answersExtra, $lhSummary, $maxItems = 18) {
$core = summarize_answers_pairs($answersCore, 8);
$extra = summarize_answers_pairs($answersExtra, 24);
$lh_str = '';
if (!empty($lhSummary)) {
$lh_str =
"Score: ".($lhSummary['score'] ?? 'n/a')."\n".
"LCP: ".($lhSummary['metrics']['lcp'] ?? 'n/a')." | ".
"TBT: ".($lhSummary['metrics']['tbt'] ?? 'n/a')." | ".
"CLS: ".($lhSummary['metrics']['cls'] ?? 'n/a')." | ".
"Interactive: ".($lhSummary['metrics']['interactive'] ?? 'n/a')."\n".
"Oportunidades (top):\n".
implode("\n", array_map(function($o){
return "- ".$o['title']." (~".$o['savingsMs']."ms)";
}, $lhSummary['oportunidades'] ?? []));
}
$system = <<<SYS
Você é um estrategista sênior de e-commerce para Black Friday. Gere um roadmap extremamente objetivo, sem genéricos e sem dependência de plataforma (não citar VTEX, Shopify, etc). Foque em decisões estratégicas e governança, você deve retornar exatamente 3 itens por area.
Regras:
- Sem detalhes técnicos de implementação.
- Cada item com título, por_que, acao_estrategica, kpi, area (CRM|UI/UX|SEO|Performance|Dados).
- O por_que precisa ser um hook para fazer o gestor que fez o quiz continuar lendo
- A acao_estrategica precisa ser uma estrategia de ponta a ponta, sem ser generico ou gerar uma analise basica que ele pode encontrar em qualquer lugar, você precisa realmente desenvolver uma estrategia de acordo com a {$maturidade} adaptando sua linguagem com essa maturidade a e estrategia sugerida. Não sugira nenhum, repito, nenhuma estratégica básica, tudo precisa ser detalhado e no final de cada acao_estrategica, precisa de mais duas linhas fazendo a explicação técnica avançada do que precisa ser feito.
- Retorne SOMENTE um JSON (array) com até {$maxItems} itens.
SYS;
$user = "MATURIDADE: {$maturidade}\n\nRESPOSTAS-CORE:\n{$core}\n\nRESPOSTAS-EXTRA:\n{$extra}\n\nLIGHTHOUSE:\n{$lh_str}\n\nSomente o JSON.";
$payload = [
'model' => OPENAI_MODEL,
'temperature' => 0.7,
'messages' => [
['role'=>'system','content'=>$system],
['role'=>'user','content'=>$user],
]
];
$ch = curl_init('https://api.openai.com/v1/chat/completions');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer '.OPENAI_API_KEY
],
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 45,
]);
$body = curl_exec($ch);
$err = curl_error($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($code >= 400 || !$body) throw new Exception("OpenAI error $code: ".($body ?: $err ?: ''));
$j = json_decode($body, true);
$txt = trim($j['choices'][0]['message']['content'] ?? '');
$items = json_decode($txt, true);
if (!is_array($items) && preg_match('/\[[\s\S]*\]/', $txt, $m)) $items = json_decode($m[0], true);
if (!is_array($items)) throw new Exception('Resposta da IA inválida (JSON não parseável)');
// saneia
$validAreas = ['CRM','UI/UX','SEO','Performance','Dados','Operacao'];
foreach ($items as &$it) {
$it['titulo'] = trim((string)($it['titulo'] ?? ''));
$it['por_que'] = trim((string)($it['por_que'] ?? ''));
$it['acao_estrategica'] = trim((string)($it['acao_estrategica'] ?? ''));
$it['kpi'] = trim((string)($it['kpi'] ?? ''));
$it['impacto'] = in_array(($it['impacto'] ?? ''), ['alto','medio','baixo'], true) ? $it['impacto'] : 'medio';
$it['prazo'] = in_array(($it['prazo'] ?? ''), ['imediato','antes da BF','H1','H2'], true) ? $it['prazo'] : 'antes da BF';
$it['area'] = in_array(($it['area'] ?? ''), $validAreas, true) ? $it['area'] : 'Operacao';
}
unset($it);
if (count($items) > $maxItems) $items = array_slice($items, 0, $maxItems);
return $items;
}
// -------------------- ROADMAP determinístico (backup) --------------------
function roadmap_by_maturity($maturidade) {
$areas = [
'Performance' => [
'basico' => [
'Ativar compressão gzip/brotli e cache estático com cabeçalhos de longo prazo.',
'Otimizar imagens (WebP/AVIF) e dimensionamento responsivo (srcset/sizes).',
'Remover scripts não usados e adiar JS não crítico (defer/async).'
],
'intermediario' => [
'Implementar Critical CSS e pré-carregar fontes/recursos essenciais.',
'Split de bundles JS por rota e lazy-load de componentes pesados.',
'Configurar CDN com HTTP/2/3 e otimização de TLS.'
],
'avancado' => [
'Server-Side Rendering/Edge Rendering com hidratação seletiva.',
'RUM (Real User Monitoring) para LCP/CLS/TBT e regressões por release.',
'Imagem on-the-fly (thumbor/imgproxy) com cache por variantes.'
],
],
'SEO' => [
'basico' => [
'Garantir títulos (15–65) e meta descriptions (50–165) únicas por página.',
'Definir canonical absoluto e sitemap.xml referenciado no robots.txt.',
'Corrigir status HTTP (evitar 4xx/5xx) e links quebrados.'
],
'intermediario' => [
'Estruturar dados (JSON-LD) para PDP (Product), PLP (ItemList) e breadcrumbs.',
'Hreflang completo (incluindo x-default) para variações de idioma/região.',
'Otimizar facetas/indexação (noindex/nofollow) e parâmetros de URL.'
],
'avancado' => [
'Arquitetura de conteúdo suportada por pesquisa de intenção e clusters.',
'Automação de metadados e internal linking com regras por template.',
'Monitoramento técnico contínuo (log analysis, crawl budget).'
],
],
'CRO/UX' => [
'basico' => [
'Checkout simplificado, eliminar campos desnecessários e fricções.',
'PDP com hierarquia clara (título, preço, frete, call-to-action visível).',
'PLP com filtros ordenados e indicador de estoque/preço atualizado.'
],
'intermediario' => [
'Testes A/B contínuos (CTA, layout, prova social) com meta de uplift.',
'Personalização básica (recentes, recomendados) por comportamento.',
'Recuperação de abandono (email/SMS/web push) com ofertas dinâmicas.'
],
'avancado' => [
'Segmentação preditiva e recomendações por ML (cross/up-sell em tempo real).',
'Orquestração omnichannel (loja física/marketplace) e preços dinâmicos.',
'Experimentação avançada (multi-armed bandit, guardrails métricos).'
],
],
'Dados/Analytics' => [
'basico' => [
'GA4 corretamente implementado (page_view, view_item, add_to_cart, purchase).',
'Mapear eventos essenciais do funil e validar via DebugView.',
'UTMs padronizadas e integração com Search Console.'
],
'intermediario' => [
'Server-side tagging e BigQuery export para análises de coorte.',
'Métricas de produto (Margem, LTV, Churn) no painel de e-commerce.',
'Alertas automáticos de anomalia (queda de receita, taxa de erro).'
],
'avancado' => [
'Modelos de atribuição próprios e MMM simplificado.',
'Feature store para personalização e campanhas em tempo real.',
'Ciclos de feedback entre BI e CRO (insight → experimento → rollout).'
],
],
'Infra/Operacao' => [
'basico' => [
'Monitoramento de uptime e erros (Sentry/Datadog) e plano de contingência.',
'Política de cache/CDN definida e invalidação por deploy.',
'Checklist de Black Friday (carga, estoque, prazos de entrega).'
],
'intermediario' => [
'Testes de carga e chaos testing em endpoints críticos.',
'Blue/Green deploy e rollback automático.',
'SLOs por rota (p95 LCP/CLS/TBT) e alertas por canal.'
],
'avancado' => [
'Edge functions para personalização de baixa latência.',
'Escalonamento elástico e prewarming para picos de tráfego.',
'DRP (Disaster Recovery Plan) com testes trimestrais.'
],
],
];
$result = [];
foreach ($areas as $area => $levels) {
$melhorias = [];
if ($maturidade === 'basico') {
$melhorias = array_merge($levels['basico'], $levels['intermediario'], $levels['avancado']);
} elseif ($maturidade === 'intermediario') {
$melhorias = array_merge($levels['intermediario'], $levels['avancado']);
} else {
$melhorias = $levels['avancado'];
}
$result[] = ['area'=>$area, 'melhorias'=>$melhorias];
}
return $result;
}
// -------------------- MAIN --------------------
try {
$data = require_post_json();
$answersCore = $data['answers'] ?? [];
$answersExtra = $data['answers_extra'] ?? [];
$scanId = isset($data['scan_id']) ? trim((string)$data['scan_id']) : '';
if (!is_array($answersCore) || count($answersCore) < 5) {
json_out(['ok'=>false,'error'=>'5 respostas são necessárias em "answers"'], 400);
}
// 1) Obter Lighthouse de LH (fallbacks)
$lighthouse = lighthouse_summary_from_sources($scanId, STORAGE_DIR);
// 2) Classificação de maturidade + explicação breve
$maturidade = 'basico';
$explanation = '';
if (!defined('OPENAI_API_KEY') || !OPENAI_API_KEY) {
// Sem OpenAI: usa heurística pelo score do Lighthouse + pistas das respostas
$score = (int)($lighthouse['score'] ?? 0);
$maturidade = ($score >= 80) ? 'avancado' : (($score >= 50) ? 'intermediario' : 'basico');
// Constrói uma explicação heurística (2–3 linhas)
$motivos = [];
if ($score) { $motivos[] = "score de performance {$score}"; }
$negativos = 0;
foreach ($answersCore as $a) {
$r = mb_strtolower(trim((string)($a['resposta'] ?? '')), 'UTF-8');
if ($r === 'não' || $r === 'nao' || $r === 'em discussão' || $r === 'em discussao') $negativos++;
}
if ($negativos >= 3) $motivos[] = "processos ainda pouco padronizados nas respostas do quiz";
$opps = $lighthouse['oportunidades'] ?? [];
if (!empty($opps)) {
$titulos = array_map(fn($o) => $o['title'] ?? $o['id'] ?? '', array_slice($opps, 0, 2));
$titulos = array_filter($titulos);
if ($titulos) $motivos[] = "oportunidades como ".implode(' e ', $titulos);
}
$explanation = "Classificação como ".ucfirst($maturidade).": ".implode('; ', $motivos).".";
} else {
// Com OpenAI: além de classificar, gera explicação curta e específica
$maturidade = openai_classify_maturity($answersCore, OPENAI_API_KEY, OPENAI_MODEL);
if (!in_array($maturidade, ['basico','intermediario','avancado'], true)) $maturidade = 'basico';
$explanation = openai_explain_maturity($maturidade, $answersCore, $answersExtra, $lighthouse);
}
// 3) Roadmap determinístico (backup)
$roadmapDet = roadmap_by_maturity($maturidade);
// 4) Roadmap otimizado por IA
$roadmapAI = [];
if (defined('OPENAI_API_KEY') && OPENAI_API_KEY) {
try {
$roadmapAI = openai_tailored_roadmap(
$maturidade,
$answersCore,
$answersExtra,
$lighthouse,
12
);
} catch (Throwable $e) {
$roadmapAI = [];
}
}
json_out([
'ok' => true,
'maturidade' => $maturidade,
'explanation' => $explanation,
'lighthouse' => $lighthouse,
'roadmap' => $roadmapDet,
'roadmap_ai' => $roadmapAI
]);
} catch (Throwable $e) {
json_out(['ok'=>false,'error'=>'evaluate: '.$e->getMessage()], 500);
}