From e4a605bdbd8f4256fb5a8db5481055a88043a878 Mon Sep 17 00:00:00 2001 From: Marcos Fargas Date: Thu, 15 Oct 2026 01:14:38 +0200 Subject: [PATCH] feat(brandbook): anadir ingesta automatica de fuentes desde URL web en seccion Fuentes --- src/app/api/brandbook/upload/route.ts | 75 ++++++++++++++++++++++++++- src/components/BrandZone.tsx | 67 +++++++++++++++++++++++- 2 files changed, 139 insertions(+), 3 deletions(-) diff --git a/src/app/api/brandbook/upload/route.ts b/src/app/api/brandbook/upload/route.ts index ac699e4..f0c427b 100644 --- a/src/app/api/brandbook/upload/route.ts +++ b/src/app/api/brandbook/upload/route.ts @@ -55,15 +55,17 @@ export async function POST(req: NextRequest) { let filename = 'pegado.txt'; let text = ''; + let url = ''; const ctype = req.headers.get('content-type') || ''; if (ctype.includes('multipart/form-data')) { const form = await req.formData(); const file = form.get('file'); const pasted = form.get('text'); - if (typeof pasted === 'string' && pasted.trim()) { + url = String(form.get('url') || '').trim(); + if (!url && typeof pasted === 'string' && pasted.trim()) { text = pasted; filename = String(form.get('filename') || 'pegado.txt'); - } else if (file && typeof file === 'object' && 'arrayBuffer' in file) { + } else if (!url && file && typeof file === 'object' && 'arrayBuffer' in file) { const f = file as File; filename = f.name || 'upload.txt'; const lower = filename.toLowerCase(); @@ -87,11 +89,80 @@ export async function POST(req: NextRequest) { const body = (await req.json().catch(() => ({}))) as { text?: string; filename?: string; + url?: string; }; + url = String(body.url || '').trim(); text = String(body.text || ''); filename = String(body.filename || 'pegado.txt'); } + if (url) { + if (!url.startsWith('http://') && !url.startsWith('https://')) { + return NextResponse.json( + { error: 'La URL debe empezar por http:// o https://' }, + { status: 400 } + ); + } + try { + const resp = await fetch(url, { + headers: { + 'User-Agent': + 'Mozilla/5.0 (compatible; 4VenturesBrandbot/1.0; +https://4ventures.es)', + Accept: 'text/html,application/xhtml+xml,text/plain;q=0.9,*/*;q=0.8', + }, + signal: AbortSignal.timeout(15000), + }); + if (!resp.ok) { + return NextResponse.json( + { error: `Error al acceder a la URL: HTTP ${resp.status}` }, + { status: 400 } + ); + } + const raw = await resp.text(); + const clean = raw + .replace(//gi, '') + .replace(//gi, '') + .replace(//gi, '') + .replace(//gi, '') + .replace(/<\/(p|div|h[1-6]|li|tr)>/gi, '\n\n') + .replace(//gi, '\n'); + + text = clean + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/ /g, ' ') + .replace(/<[^>]+>/g, ' ') + .replace(/[ \t]+/g, ' ') + .replace(/\n\s*\n\s*\n+/g, '\n\n') + .trim(); + + try { + const parsed = new URL(url); + const pathSegments = parsed.pathname.split('/').filter(Boolean); + const lastSeg = pathSegments[pathSegments.length - 1]; + filename = lastSeg + ? lastSeg.endsWith('.html') + ? lastSeg + : `${lastSeg}.txt` + : `${parsed.hostname}.txt`; + } catch { + filename = 'fuente-url.txt'; + } + } catch (err) { + return NextResponse.json( + { + error: `No se pudo conectar a la URL: ${ + err instanceof Error ? err.message : String(err) + }`, + }, + { status: 400 } + ); + } + } + if (!text.trim()) { return NextResponse.json({ error: 'Sin texto' }, { status: 400 }); } diff --git a/src/components/BrandZone.tsx b/src/components/BrandZone.tsx index c23ed01..5038ab4 100644 --- a/src/components/BrandZone.tsx +++ b/src/components/BrandZone.tsx @@ -133,6 +133,8 @@ export default function BrandZone({ const [blogAnalysisResult, setBlogAnalysisResult] = useState(null); const [showBlogAnalysisModal, setShowBlogAnalysisModal] = useState(false); const [blogAnalyzeError, setBlogAnalyzeError] = useState(null); + const [sourceUrl, setSourceUrl] = useState(''); + const [ingestingUrl, setIngestingUrl] = useState(false); const load = useCallback(async () => { setLoading(true); @@ -297,6 +299,30 @@ export default function BrandZone({ } } + async function ingestSourceUrl() { + if (!sourceUrl.trim()) return; + setIngestingUrl(true); + setErr(null); + try { + const fd = new FormData(); + fd.append('url', sourceUrl.trim()); + const r = await fetch('/api/brandbook/upload', { method: 'POST', body: fd }); + const j = await r.json(); + if (!r.ok) throw new Error(j.error || 'Ingesta de URL falló'); + setBook(j.brandbook); + setScore(j.completeness?.score ?? 0); + setMissing(j.completeness?.missing || []); + setExists(true); + setSourceUrl(''); + setOkMsg(`Fuente web añadida con éxito: ${j.ingested}`); + setSection('fuentes'); + } catch (e) { + setErr(e instanceof Error ? e.message : 'Error al ingestar URL'); + } finally { + setIngestingUrl(false); + } + } + async function uploadKit(file: File) { setKitUploading(true); setErr(null); @@ -2560,10 +2586,49 @@ export default function BrandZone({ {section === 'fuentes' && (
+ {/* Ingesta desde URL */} +
+
+ + Ingestar fuente desde URL web (Estrategias, Artículos, Notions públicos o Dossiers) +
+

+ Pega cualquier enlace web. El sistema descargará el texto, limpiará el HTML y lo añadirá automáticamente a la base de conocimiento de la marca (RAG). +

+
+ setSourceUrl(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); + void ingestSourceUrl(); + } + }} + /> + +
+
+
- Subir o pegar brandbook / brief + Subir archivo o pegar texto

.txt / .md / .csv / .json — o pega texto de PDF/DOCX. Detecta