From 5c76ae18bb622e05f560f0c5cec5781e423ac227 Mon Sep 17 00:00:00 2001 From: 4Ventures <4ventures@MacBook-Air-de-4Ventures.local> Date: Fri, 16 Oct 2026 13:26:06 +0200 Subject: [PATCH] feat(brandkit): permitir subir PDF y DOCX como fuentes Extrae texto del PDF/DOCX en Fuentes del kit de marca para no depender de pegar el contenido a mano. Co-authored-by: Cursor --- src/app/api/brandbook/upload/route.ts | 45 ++++- src/components/BrandZone.tsx | 8 +- src/lib/extract-document-text.ts | 270 ++++++++++++++++++++++++++ 3 files changed, 309 insertions(+), 14 deletions(-) create mode 100644 src/lib/extract-document-text.ts diff --git a/src/app/api/brandbook/upload/route.ts b/src/app/api/brandbook/upload/route.ts index f0c427b..98168dd 100644 --- a/src/app/api/brandbook/upload/route.ts +++ b/src/app/api/brandbook/upload/route.ts @@ -1,5 +1,5 @@ /** - * POST multipart: file (.txt/.md/.csv) or JSON { text, filename } + * POST multipart: file (.txt/.md/.csv/.json/.pdf/.docx) or JSON { text, filename } * Appends source excerpt into the tenant brandbook. */ import { NextRequest, NextResponse } from 'next/server'; @@ -15,8 +15,13 @@ import { parseBrandbook, type Brandbook, } from '@/lib/brandbook'; +import { + detectSourceExt, + extractSourceText, +} from '@/lib/extract-document-text'; export const dynamic = 'force-dynamic'; +export const maxDuration = 60; async function loadBook(tenantId: string, label: string): Promise<{ id: string | null; @@ -68,22 +73,42 @@ export async function POST(req: NextRequest) { } else if (!url && file && typeof file === 'object' && 'arrayBuffer' in file) { const f = file as File; filename = f.name || 'upload.txt'; - const lower = filename.toLowerCase(); - if ( - !lower.endsWith('.txt') && - !lower.endsWith('.md') && - !lower.endsWith('.csv') && - !lower.endsWith('.json') - ) { + const ext = detectSourceExt(filename, f.type); + if (!ext) { return NextResponse.json( { error: - 'Por ahora sube .txt, .md, .csv o .json. PDF/DOCX: pega el texto en el campo.', + 'Formatos admitidos: .pdf, .docx, .txt, .md, .csv, .json. Si el PDF es escaneado, pega el texto.', + }, + { status: 400 } + ); + } + try { + const buf = Buffer.from(await f.arrayBuffer()); + if (buf.byteLength > 25 * 1024 * 1024) { + return NextResponse.json( + { error: 'Archivo demasiado grande (máx. 25 MB)' }, + { status: 400 } + ); + } + const extracted = await extractSourceText({ + filename, + mime: f.type, + buffer: buf, + }); + text = extracted.text; + filename = extracted.filename; + } catch (e) { + return NextResponse.json( + { + error: + e instanceof Error + ? e.message + : 'No se pudo leer el archivo', }, { status: 400 } ); } - text = await f.text(); } } else { const body = (await req.json().catch(() => ({}))) as { diff --git a/src/components/BrandZone.tsx b/src/components/BrandZone.tsx index 01cb1a9..c3dc833 100644 --- a/src/components/BrandZone.tsx +++ b/src/components/BrandZone.tsx @@ -3271,9 +3271,9 @@ export default function BrandZone({ Subir archivo o pegar texto

- .txt / .md / .csv / .json — o pega texto de PDF/DOCX. Detecta - colores #hex automáticamente. Logos y tipografías: barra - «Añadir al kit». + .pdf / .docx / .txt / .md / .csv / .json — o pega texto. + Detecta colores #hex automáticamente. Logos y tipografías: + barra «Añadir al kit».