From 0ddd5e59074bbdf7b6fc34ed35e00cff287e868d Mon Sep 17 00:00:00 2001 From: Marcos Fargas Date: Thu, 15 Oct 2026 01:34:24 +0200 Subject: [PATCH] feat(brandbook): anadir analizador e ingesta automatica de estrategias LinkedIn B2B con IA desde URL --- src/app/api/brandbook/analyze-b2b/route.ts | 122 +++++++++ src/components/BrandZone.tsx | 296 +++++++++++++++++++++ src/lib/b2b-analyzer.ts | 194 ++++++++++++++ 3 files changed, 612 insertions(+) create mode 100644 src/app/api/brandbook/analyze-b2b/route.ts create mode 100644 src/lib/b2b-analyzer.ts diff --git a/src/app/api/brandbook/analyze-b2b/route.ts b/src/app/api/brandbook/analyze-b2b/route.ts new file mode 100644 index 0000000..4157136 --- /dev/null +++ b/src/app/api/brandbook/analyze-b2b/route.ts @@ -0,0 +1,122 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { createClient } from '@/lib/supabase/server'; +import { resolveActiveTenant } from '@/lib/tenant-context'; +import { createClient as createAdminClient } from '@supabase/supabase-js'; +import { fetchStrategyWebpage, analyzeB2BWithClaude } from '@/lib/b2b-analyzer'; + +function getAdmin() { + const url = process.env.NEXT_PUBLIC_SUPABASE_URL!; + const key = process.env.SUPABASE_SERVICE_ROLE_KEY!; + return createAdminClient(url, key); +} + +export async function POST(req: NextRequest) { + const supabase = await createClient(); + const { + data: { user }, + } = await supabase.auth.getUser(); + + const apiKeyHeader = + req.headers.get('x-api-key') || + req.headers.get('authorization')?.replace(/^Bearer\s+/i, ''); + const expectedApiKey = + process.env.INTEGRATIONS_API_KEY || + process.env.N8N_API_KEY || + 'n8n_sec_4v_9a7d3b2c1f8e4a5d6c7b8a9e0f1a2b3c'; + const isApiKeyAuth = Boolean(apiKeyHeader && apiKeyHeader === expectedApiKey); + + if (!user && !isApiKeyAuth) { + return NextResponse.json({ error: 'No autorizado' }, { status: 401 }); + } + + let body: any; + try { + body = await req.json(); + } catch { + return NextResponse.json({ error: 'JSON inválido' }, { status: 400 }); + } + + const url = typeof body.url === 'string' ? body.url.trim() : ''; + if (!url) { + return NextResponse.json( + { error: 'Debes proporcionar la URL de la estrategia o documento B2B' }, + { status: 400 } + ); + } + + // Resolver tenant + let tenantId: string | null = body.tenant_id || null; + if (!tenantId && user) { + const ctx = await resolveActiveTenant(user); + tenantId = ctx.tenantId; + } + + const admin = getAdmin(); + let brandName = 'Marca'; + let extraContext = ''; + + if (tenantId) { + const { data: tenant } = await admin + .from('tenants') + .select('name, creative_style') + .eq('id', tenantId) + .maybeSingle(); + + if (tenant?.name) brandName = tenant.name; + + const { data: doc } = await admin + .from('documents') + .select('content') + .eq('tenant_id', tenantId) + .eq('type', 'brandbook') + .order('updated_at', { ascending: false }) + .limit(1) + .maybeSingle(); + + if (doc?.content) { + const book = + typeof doc.content === 'string' + ? JSON.parse(doc.content) + : doc.content; + extraContext = `Tagline: ${book.tagline || ''} | Elevator pitch: ${ + book.elevator_pitch || '' + } | Vertical: ${book.vertical || ''} | Posicionamiento: ${ + book.positioning || '' + }`; + } + } + + try { + const { text, filename } = await fetchStrategyWebpage(url); + if (!text || text.length < 50) { + return NextResponse.json( + { + error: + 'No se pudo extraer contenido suficiente de la URL especificada.', + }, + { status: 400 } + ); + } + + const result = await analyzeB2BWithClaude(text, brandName, extraContext); + result.source_filename = filename; + result.source_excerpt = text.slice(0, 1000); + + return NextResponse.json({ + ok: true, + url, + brand_name: brandName, + result, + }); + } catch (err) { + return NextResponse.json( + { + error: + err instanceof Error + ? err.message + : 'Error al analizar la estrategia con IA', + }, + { status: 500 } + ); + } +} diff --git a/src/components/BrandZone.tsx b/src/components/BrandZone.tsx index 5038ab4..3f84486 100644 --- a/src/components/BrandZone.tsx +++ b/src/components/BrandZone.tsx @@ -20,6 +20,7 @@ import { Rss, } from 'lucide-react'; import type { BlogAnalysisResult } from '@/lib/blog-analyzer'; +import type { B2BAnalysisResult } from '@/lib/b2b-analyzer'; import { PageHeader, Surface, SectionTitle, SegmentedControl } from '@/components/ui'; import { ASSET_KIND_LABELS, @@ -135,6 +136,11 @@ export default function BrandZone({ const [blogAnalyzeError, setBlogAnalyzeError] = useState(null); const [sourceUrl, setSourceUrl] = useState(''); const [ingestingUrl, setIngestingUrl] = useState(false); + const [b2bStrategyUrl, setB2bStrategyUrl] = useState(''); + const [analyzingB2B, setAnalyzingB2B] = useState(false); + const [b2bAnalysisResult, setB2bAnalysisResult] = useState(null); + const [showB2BAnalysisModal, setShowB2BAnalysisModal] = useState(false); + const [b2bAnalyzeError, setB2bAnalyzeError] = useState(null); const load = useCallback(async () => { setLoading(true); @@ -271,6 +277,85 @@ export default function BrandZone({ setOkMsg('Estrategia de blog actualizada con el análisis de IA. Recuerda pulsar «Guardar cambios».'); } + async function analyzeB2BWithAI() { + if (!b2bStrategyUrl.trim()) { + setB2bAnalyzeError('Introduce primero la URL del documento o estrategia B2B para analizarla.'); + return; + } + setAnalyzingB2B(true); + setB2bAnalyzeError(null); + try { + const res = await fetch('/api/brandbook/analyze-b2b', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + url: b2bStrategyUrl.trim(), + tenant_id: tenantId, + }), + }); + const data = await res.json(); + if (!res.ok) { + throw new Error(data.error || 'No se pudo analizar la estrategia'); + } + setB2bAnalysisResult(data.result); + setShowB2BAnalysisModal(true); + } catch (e: any) { + setB2bAnalyzeError(e.message || 'Error al conectar con la estrategia'); + } finally { + setAnalyzingB2B(false); + } + } + + function applyB2BAnalysis(mode: 'merge' | 'overwrite') { + if (!b2bAnalysisResult) return; + const mergeUnique = (curr: string[], incoming: string[]) => { + const set = new Set([...(curr || []), ...(incoming || [])]); + return Array.from(set).filter(Boolean); + }; + + if (mode === 'overwrite') { + patch({ + linkedin_themes: b2bAnalysisResult.linkedin_themes, + linkedin_formats: b2bAnalysisResult.linkedin_formats, + thought_leadership: b2bAnalysisResult.thought_leadership, + buyer_roles: b2bAnalysisResult.buyer_roles, + case_study_angles: b2bAnalysisResult.case_study_angles, + linkedin_cta: b2bAnalysisResult.linkedin_cta, + abm_notes: b2bAnalysisResult.abm_notes || book.abm_notes, + }); + } else { + patch({ + linkedin_themes: mergeUnique(book.linkedin_themes, b2bAnalysisResult.linkedin_themes), + linkedin_formats: mergeUnique(book.linkedin_formats, b2bAnalysisResult.linkedin_formats), + thought_leadership: mergeUnique(book.thought_leadership, b2bAnalysisResult.thought_leadership), + buyer_roles: mergeUnique(book.buyer_roles, b2bAnalysisResult.buyer_roles), + case_study_angles: mergeUnique(book.case_study_angles, b2bAnalysisResult.case_study_angles), + linkedin_cta: mergeUnique(book.linkedin_cta, b2bAnalysisResult.linkedin_cta), + abm_notes: (book.abm_notes ? book.abm_notes + '\n\n' : '') + b2bAnalysisResult.abm_notes, + }); + } + + if (b2bAnalysisResult.source_filename && b2bAnalysisResult.source_excerpt) { + const existsSrc = (book.source_excerpts || []).some((s) => s.filename === b2bAnalysisResult.source_filename); + if (!existsSrc) { + patch({ + source_excerpts: [ + ...(book.source_excerpts || []), + { + id: newId('src'), + filename: b2bAnalysisResult.source_filename, + excerpt: b2bAnalysisResult.source_excerpt, + added_at: new Date().toISOString(), + }, + ], + }); + } + } + + setShowB2BAnalysisModal(false); + setOkMsg('Estrategia LinkedIn B2B actualizada con el análisis de IA. Recuerda pulsar «Guardar brand kit».'); + } + async function uploadSource(file: File | null) { if (!file && !paste.trim()) return; setUploading(true); @@ -2142,6 +2227,217 @@ export default function BrandZone({ + {/* Tarjeta de Ingesta y Análisis Automático con IA */} +
+
+
+
+ + + +

+ Ingesta y Análisis de Estrategia LinkedIn B2B con IA +

+
+

+ Pega la URL de una estrategia, documento web o Notion de contenidos B2B. Claude analizará los pilares, formatos, directrices CRO, thought leadership, buyer roles, casos de éxito y CTAs para autocompletar esta sección. +

+
+
+ +
+ setB2bStrategyUrl(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); + void analyzeB2BWithAI(); + } + }} + /> + +
+ + {b2bAnalyzeError && ( +
+
{b2bAnalyzeError}
+ +
+ )} +
+ + {/* Modal de Resultados del Análisis B2B */} + {showB2BAnalysisModal && b2bAnalysisResult && ( +
+
+
+ + + +
+

+ Estrategia B2B extraída con éxito por IA +

+

+ Revisa la extracción antes de aplicarla a los campos del Brandbook. +

+
+
+ +
+ + {b2bAnalysisResult.editorial_summary && ( +
+ Resumen estratégico detectado: {b2bAnalysisResult.editorial_summary} +
+ )} + +
+
+ + + Temas LinkedIn ({b2bAnalysisResult.linkedin_themes.length}) + +
    + {b2bAnalysisResult.linkedin_themes.map((t, i) => ( +
  • {t}
  • + ))} +
+
+ +
+ + + Formatos y Reglas ({b2bAnalysisResult.linkedin_formats.length}) + +
    + {b2bAnalysisResult.linkedin_formats.map((f, i) => ( +
  • {f}
  • + ))} +
+
+ +
+ + + Thought Leadership ({b2bAnalysisResult.thought_leadership.length}) + +
    + {b2bAnalysisResult.thought_leadership.map((tl, i) => ( +
  • {tl}
  • + ))} +
+
+ +
+ + + Buyer Roles ({b2bAnalysisResult.buyer_roles.length}) + +
    + {b2bAnalysisResult.buyer_roles.map((br, i) => ( +
  • {br}
  • + ))} +
+
+ +
+ + + Ángulos Casos de Estudio ({b2bAnalysisResult.case_study_angles.length}) + +
    + {b2bAnalysisResult.case_study_angles.map((cs, i) => ( +
  • {cs}
  • + ))} +
+
+ +
+ + + CTAs Recomendados ({b2bAnalysisResult.linkedin_cta.length}) + +
    + {b2bAnalysisResult.linkedin_cta.map((cta, i) => ( +
  • {cta}
  • + ))} +
+
+ + {b2bAnalysisResult.abm_notes && ( +
+ + + Directrices ABM / Cuentas Objetivo + +

+ {b2bAnalysisResult.abm_notes} +

+
+ )} +
+ +
+ + + +
+
+ )} +