From 85e9f4c18493ba485758d9cbb372d1e6c9cba0f9 Mon Sep 17 00:00:00 2001 From: Marcos Fargas Date: Thu, 15 Oct 2026 00:18:22 +0200 Subject: [PATCH] feat(brandbook): anadir ingesta y analisis automatico de blog con IA en Zona de Marca --- src/app/api/brandbook/analyze-blog/route.ts | 124 +++++ src/components/BrandZone.tsx | 402 ++++++++++++++-- src/lib/blog-analyzer.ts | 501 ++++++++++++++++++++ 3 files changed, 982 insertions(+), 45 deletions(-) create mode 100644 src/app/api/brandbook/analyze-blog/route.ts create mode 100644 src/lib/blog-analyzer.ts diff --git a/src/app/api/brandbook/analyze-blog/route.ts b/src/app/api/brandbook/analyze-blog/route.ts new file mode 100644 index 0000000..80ace36 --- /dev/null +++ b/src/app/api/brandbook/analyze-blog/route.ts @@ -0,0 +1,124 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { createClient } from '@/lib/supabase/server'; +import { isAgencyRole, userRole } from '@/lib/agency'; +import { resolveActiveTenant } from '@/lib/tenant-context'; +import { createClient as createAdminClient } from '@supabase/supabase-js'; +import { fetchBlogPosts, analyzeBlogWithClaude } from '@/lib/blog-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 del blog corporativo' }, + { 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, slug') + .eq('id', tenantId) + .maybeSingle(); + + if (tenant?.name) { + brandName = tenant.name; + } + + // Buscar si hay brandbook previo para dar contexto a Claude + const { data: doc } = await admin + .from('documents') + .select('content') + .eq('tenant_id', tenantId) + .eq('type', 'brandbook') + .maybeSingle(); + + if (doc?.content) { + try { + const parsed = + typeof doc.content === 'string' + ? JSON.parse(doc.content) + : doc.content; + const b = parsed.content || parsed; + if (b.brand_name) brandName = b.brand_name; + if (b.elevator_pitch || b.value_proposition) { + extraContext = `Propuesta de valor: ${b.value_proposition || b.elevator_pitch || ''}\nVertical: ${b.vertical || ''}`; + } + } catch {} + } + } + + try { + // 1. Extraer artículos de la URL del blog + const posts = await fetchBlogPosts(url); + + if (!posts || posts.length === 0) { + return NextResponse.json( + { + error: + 'No se encontraron artículos en la URL proporcionada. Revisa que sea accesible y contenga posts.', + }, + { status: 404 } + ); + } + + // 2. Analizar con Claude + const analysis = await analyzeBlogWithClaude(posts, brandName, extraContext); + + return NextResponse.json({ + ok: true, + url, + brand_name: brandName, + posts_count: posts.length, + result: analysis, + }); + } catch (err: any) { + console.error('[analyze-blog] Error:', err); + return NextResponse.json( + { error: err.message || 'Error analizando el blog con IA' }, + { status: 500 } + ); + } +} diff --git a/src/components/BrandZone.tsx b/src/components/BrandZone.tsx index 2d2e95d..1834f32 100644 --- a/src/components/BrandZone.tsx +++ b/src/components/BrandZone.tsx @@ -13,7 +13,13 @@ import { ImageIcon, Type, Palette, + Globe, + ExternalLink, + X, + Check, + Rss, } from 'lucide-react'; +import type { BlogAnalysisResult } from '@/lib/blog-analyzer'; import { PageHeader, Surface, SectionTitle, SegmentedControl } from '@/components/ui'; import { ASSET_KIND_LABELS, @@ -123,6 +129,10 @@ export default function BrandZone({ const [exists, setExists] = useState(false); const [kitKind, setKitKind] = useState('logo'); const [kitRole, setKitRole] = useState('primary'); + const [analyzingBlog, setAnalyzingBlog] = useState(false); + const [blogAnalysisResult, setBlogAnalysisResult] = useState(null); + const [showBlogAnalysisModal, setShowBlogAnalysisModal] = useState(false); + const [blogAnalyzeError, setBlogAnalyzeError] = useState(null); const load = useCallback(async () => { setLoading(true); @@ -202,6 +212,63 @@ export default function BrandZone({ } } + async function handleAnalyzeBlog() { + const blogUrl = book.channel_handles?.blog?.trim(); + if (!blogUrl) { + setBlogAnalyzeError('Introduce primero la URL del blog corporativo para poder analizar sus posts.'); + return; + } + setAnalyzingBlog(true); + setBlogAnalyzeError(null); + try { + const res = await fetch('/api/brandbook/analyze-blog', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + url: blogUrl, + tenant_id: tenantId, + }), + }); + const data = await res.json(); + if (!res.ok) { + throw new Error(data.error || 'No se pudo analizar el blog'); + } + setBlogAnalysisResult(data.result); + setShowBlogAnalysisModal(true); + } catch (e: any) { + setBlogAnalyzeError(e.message || 'Error al conectar con el blog'); + } finally { + setAnalyzingBlog(false); + } + } + + function applyBlogAnalysis(mode: 'merge' | 'overwrite') { + if (!blogAnalysisResult) return; + if (mode === 'overwrite') { + patch({ + blog_themes: blogAnalysisResult.blog_themes, + blog_seo_keywords: blogAnalysisResult.blog_seo_keywords, + content_clusters: blogAnalysisResult.content_clusters, + blog_cta: blogAnalysisResult.blog_cta, + blog_cadence: blogAnalysisResult.blog_cadence || book.blog_cadence, + }); + } else { + const mergeUnique = (curr: string[], incoming: string[]) => { + const set = new Set([...(curr || []), ...(incoming || [])]); + return Array.from(set).filter(Boolean); + }; + patch({ + blog_themes: mergeUnique(book.blog_themes, blogAnalysisResult.blog_themes), + blog_seo_keywords: mergeUnique(book.blog_seo_keywords, blogAnalysisResult.blog_seo_keywords), + content_clusters: mergeUnique(book.content_clusters, blogAnalysisResult.content_clusters), + blog_cta: mergeUnique(book.blog_cta, blogAnalysisResult.blog_cta), + blog_cadence: book.blog_cadence || blogAnalysisResult.blog_cadence, + }); + } + setShowBlogAnalysisModal(false); + setOkMsg('Estrategia de blog actualizada con el análisis de IA. Recuerda pulsar «Guardar cambios».'); + } + async function uploadSource(file: File | null) { if (!file && !paste.trim()) return; setUploading(true); @@ -2073,51 +2140,296 @@ export default function BrandZone({ )} {section === 'blog' && ( -
- -