diff --git a/src/app/api/webhooks/whatsapp/route.ts b/src/app/api/webhooks/whatsapp/route.ts index b425fbb..0547b1b 100644 --- a/src/app/api/webhooks/whatsapp/route.ts +++ b/src/app/api/webhooks/whatsapp/route.ts @@ -207,11 +207,41 @@ function isDuplicateMessage(chatId: string, text: string): boolean { return false; } +function detectExplicitPublicationType(inputText: string): 'noticia' | 'evento' | 'aviso' | null { + if (!inputText) return null; + const lower = inputText.toLowerCase(); + + // Detección de palabras clave explícitas enviadas por el usuario: + // Ejemplos: "Noticia: ...", "Evento: ...", "Publicidad: ...", "#noticia", "publicar como evento", etc. + if ( + /^\s*(?:#|tipo:\s*|categoria:\s*|publicar\s+como:\s*|\[)?\s*noticia\b/i.test(lower) || + /\b(?:publicar\s+como\s+noticia|es\s+una\s+noticia)\b/i.test(lower) + ) { + return 'noticia'; + } + + if ( + /^\s*(?:#|tipo:\s*|categoria:\s*|publicar\s+como:\s*|\[)?\s*evento\b/i.test(lower) || + /\b(?:publicar\s+como\s+evento|es\s+un\s+evento|agenda\s+cultural|agenda\s+de\s+eventos)\b/i.test(lower) + ) { + return 'evento'; + } + + if ( + /^\s*(?:#|tipo:\s*|categoria:\s*|publicar\s+como:\s*|\[)?\s*(?:publicidad|aviso|clasificado|anuncio)\b/i.test(lower) || + /\b(?:publicar\s+como\s+publicidad|publicar\s+como\s+aviso|publicar\s+como\s+clasificado|es\s+publicidad|es\s+un\s+aviso)\b/i.test(lower) + ) { + return 'aviso'; + } + + return null; +} + // LOGIC 2: PROCESAR PUBLICACIÓN ÚNICA // En WhatsApp, los álbumes de fotos (ej. 25 fotos + 1 texto) llegan como 26 eventos separados. // Las fotos individuales sin texto sustancial NO deben generar publicaciones independientes; // se recolectan como recursos para la publicación principal que contiene el texto de la noticia. - const hasSubstantialText = Boolean(text && text.trim().length >= 15); + const hasSubstantialText = Boolean(text && text.trim().length >= 10); if (!hasSubstantialText) { console.log(`[WhatsApp Webhook] Mensaje multimedia sin texto editorial (${text?.length || 0} chars). Registrado para lote de fotos.`); @@ -224,17 +254,24 @@ function isDuplicateMessage(chatId: string, text: string): boolean { return NextResponse.json({ success: true, message: 'Ignored duplicate text in burst' }, { status: 200 }); } + // Detectar si el usuario definió explícitamente el tipo en el texto (noticia, evento o publicidad/aviso) + const explicitType = detectExplicitPublicationType(text); + if (explicitType) { + console.log(`[WhatsApp Webhook] Tipo de publicación explícito detectado: "${explicitType.toUpperCase()}"`); + } + // Descubrir el grupo central de Notificaciones y Aprobaciones (Notificaciones Almeira) const adminNotificationJid = await getNotificationGroupJid() || '120363428012905103@g.us'; const destinationJid = adminNotificationJid; // Notificación de Progreso 1: Enviada EXCLUSIVAMENTE a Notificaciones Almeira + const typeInfoText = explicitType ? ` (${explicitType.toUpperCase()})` : ''; sendWhatsAppMessage( destinationJid, - `📥 *Nueva Información Recibida*\n` + + `📥 *Nueva Información Recibida${typeInfoText}*\n` + `📍 *Fuente:* ${filterResult.resolvedName}\n` + `_Almeira Web • Asistente Editorial con IA_\n\n` + - `🤖 Recolectando lote de imágenes y analizando contenido...` + `🤖 Recolectando lote de imágenes y estructurando contenido...` ).catch(e => console.warn('[WhatsApp Webhook] No se pudo enviar notificación de inicio:', e.message)); // Recolectamos todas las imágenes/multimedia candidatas (mensaje actual + álbum de fotos reciente del chat) @@ -260,10 +297,10 @@ function isDuplicateMessage(chatId: string, text: string): boolean { console.warn('[WhatsApp Webhook] No se pudo consultar lote multimedia reciente:', e); } - console.log(`[WhatsApp Webhook] Procesando ${candidateMediaList.length} imágenes del álbum con Gemini...`); + console.log(`[WhatsApp Webhook] Procesando ${candidateMediaList.length} imágenes del álbum con Gemini (Tipo forzado: ${explicitType || 'Auto'})...`); - // Llamar a Gemini para estructurar la publicación y seleccionar la mejor imagen - const parsed = await processWhatsAppMessage(text, candidateMediaList); + // Llamar a Gemini para estructurar la publicación y seleccionar la mejor imagen respetando el tipo explícito si existe + const parsed = await processWhatsAppMessage(text, candidateMediaList, undefined, explicitType); // Determinar dimensiones según el tipo de publicación // Noticias: 640x360 (16:9) | Clasificados y Eventos: 640x640 (1:1) diff --git a/src/lib/gemini.ts b/src/lib/gemini.ts index c336d2b..8aa86d8 100644 --- a/src/lib/gemini.ts +++ b/src/lib/gemini.ts @@ -23,7 +23,11 @@ export interface ProcessedPublication { /** * Fallback parser in case Gemini API is rate-limited (429) or unavailable */ -function fallbackRuleBasedParser(text: string, mediaCount: number): ProcessedPublication { +function fallbackRuleBasedParser( + text: string, + mediaCount: number, + explicitType?: 'noticia' | 'evento' | 'aviso' | null +): ProcessedPublication { const clean = (text || '').trim(); const lines = clean.split('\n').map(l => l.trim()).filter(Boolean); @@ -34,11 +38,13 @@ function fallbackRuleBasedParser(text: string, mediaCount: number): ProcessedPub } const lower = clean.toLowerCase(); - let tipo: 'noticia' | 'evento' | 'aviso' = 'noticia'; - if (lower.includes('evento') || lower.includes('invitación') || lower.includes('invitacion') || lower.includes('jornada') || lower.includes('taller') || lower.includes('festival') || lower.includes('feria') || lower.includes('convocatoria')) { - tipo = 'evento'; - } else if (lower.includes('se vende') || lower.includes('se arrienda') || lower.includes('clasificado') || lower.includes('empleo') || lower.includes('vacante') || lower.includes('servicio')) { - tipo = 'aviso'; + let tipo: 'noticia' | 'evento' | 'aviso' = explicitType || 'noticia'; + if (!explicitType) { + if (lower.includes('evento') || lower.includes('invitación') || lower.includes('invitacion') || lower.includes('jornada') || lower.includes('taller') || lower.includes('festival') || lower.includes('feria') || lower.includes('convocatoria')) { + tipo = 'evento'; + } else if (lower.includes('se vende') || lower.includes('se arrienda') || lower.includes('clasificado') || lower.includes('empleo') || lower.includes('vacante') || lower.includes('servicio') || lower.includes('publicidad')) { + tipo = 'aviso'; + } } const seo = clean.replace(/\n+/g, ' ').substring(0, 150) || rawTitle; @@ -63,7 +69,8 @@ function fallbackRuleBasedParser(text: string, mediaCount: number): ProcessedPub export async function processWhatsAppMessage( text: string, mediaParam?: string | MediaInput[] | MediaInput, - mimeTypeParam?: string + mimeTypeParam?: string, + explicitType?: 'noticia' | 'evento' | 'aviso' | null ): Promise { // Normalize mediaParam into an array of MediaInput let mediaList: MediaInput[] = []; @@ -77,18 +84,23 @@ export async function processWhatsAppMessage( if (!process.env.GEMINI_API_KEY) { console.warn('[Gemini] Sin GEMINI_API_KEY configurada, usando parser de respaldo'); - return fallbackRuleBasedParser(text, mediaList.length); + return fallbackRuleBasedParser(text, mediaList.length, explicitType); } + const explicitTypeDirective = explicitType + ? `\n IMPORTANTE: El usuario ha definido explícitamente que la categoría DEBE SER "${explicitType}". Debes asignar tipo: "${explicitType}" obligatoriamente.\n` + : ''; + const systemInstruction = ` Eres un asistente editorial experto y editor gráfico de Almeira Web. Analiza los mensajes y recursos multimedia recibidos desde WhatsApp para estructurarlos como publicaciones del portal. + ${explicitTypeDirective} Debes estructurar el mensaje en base a lo siguiente: 1. titulo: Asigna un título claro, profesional y conciso (máx. 70 caracteres). - 2. tipo: Determina la categoría exacta: + 2. tipo: Determina la categoría exacta ${explicitType ? `(Usa obligatoriamente "${explicitType}")` : ''}: - "noticia": Noticias generales, política, actualidad, comunicados y reportajes (dimensiones recomendadas de imagen: 640x360 px, relación 16:9). - "evento": Invitaciones a actividades, jornadas, ferias, foros, congresos, festivales o convocatorias con fecha (dimensiones de imagen: 640x640 px, relación 1:1). - - "aviso": Clasificados, ofertas de empleo, compra/venta, servicios profesionales, comunicados comunitarios (dimensiones de imagen: 640x640 px, relación 1:1). + - "aviso": Publicidad comercial, clasificados, ofertas de empleo, compra/venta, servicios profesionales, comunicados comunitarios (dimensiones de imagen: 640x640 px, relación 1:1). 3. seo_descripcion: Un resumen conciso de máximo 150 caracteres optimizado para redes y motores de búsqueda. 4. contenido: El texto detallado de la publicación en HTML válido y limpio (usa

, ,