feat(feed): eliminar tope de 60 mensajes, carga profunda de miles de publicaciones y paginacion avanzada

This commit is contained in:
AI Bot 2026-09-03 13:03:17 -05:00
parent c1c39982ff
commit 4abc8d6342
3 changed files with 168 additions and 102 deletions

View File

@ -77,7 +77,7 @@ export default function SupplierFeedPage() {
// Target brand selector for publishing // Target brand selector for publishing
const [targetBrand, setTargetBrand] = useState<BrandId | 'all'>('almeira'); const [targetBrand, setTargetBrand] = useState<BrandId | 'all'>('almeira');
// Cargar grupos monitoreados, productos existentes y posts en vivo (por defecto PENDIENTES) // Cargar grupos monitoreados, productos existentes y posts en vivo (TODOS los mensajes)
const loadInitialData = async () => { const loadInitialData = async () => {
setIsLoading(true); setIsLoading(true);
try { try {
@ -95,8 +95,8 @@ export default function SupplierFeedPage() {
setExistingProducts(prodData.products); setExistingProducts(prodData.products);
} }
// 3. Posts en vivo desde los grupos de WhatsApp (iniciando con PENDIENTES para que la lista avance) // 3. Posts en vivo desde los grupos de WhatsApp (escaneando todo el historial profundo)
const feedRes = await fetch('/api/whatsapp-feed?fetchLive=true&limit=60&status=pending'); const feedRes = await fetch('/api/whatsapp-feed?fetchLive=true&pages=35&limit=3500&status=pending');
const feedData = await feedRes.json(); const feedData = await feedRes.json();
if (feedData.success && Array.isArray(feedData.posts)) { if (feedData.success && Array.isArray(feedData.posts)) {
setPosts(feedData.posts); setPosts(feedData.posts);
@ -112,7 +112,7 @@ export default function SupplierFeedPage() {
loadInitialData(); loadInitialData();
}, []); }, []);
// Cambiar filtro de grupo y recargar mensajes // Cambiar filtro de grupo y recargar todos los mensajes del grupo
const handleGroupFilterChange = async (groupJid: string) => { const handleGroupFilterChange = async (groupJid: string) => {
setSelectedGroupFilter(groupJid); setSelectedGroupFilter(groupJid);
setIsSyncingLive(true); setIsSyncingLive(true);
@ -120,13 +120,13 @@ export default function SupplierFeedPage() {
try { try {
const url = const url =
groupJid !== 'all' groupJid !== 'all'
? `/api/whatsapp-feed?fetchLive=true&groupJid=${encodeURIComponent(groupJid)}&status=${tabStatus}` ? `/api/whatsapp-feed?fetchLive=true&groupJid=${encodeURIComponent(groupJid)}&status=${tabStatus}&pages=35&limit=3500`
: `/api/whatsapp-feed?fetchLive=true&limit=60&status=${tabStatus}`; : `/api/whatsapp-feed?fetchLive=true&status=${tabStatus}&pages=35&limit=3500`;
const res = await fetch(url); const res = await fetch(url);
const data = await res.json(); const data = await res.json();
if (data.success && Array.isArray(data.posts)) { if (data.success && Array.isArray(data.posts)) {
setPosts(data.posts); setPosts(data.posts);
setActionMessage(`✅ Se cargaron ${data.totalFound || data.posts.length} mensajes.`); setActionMessage(`✅ Se cargaron ${data.totalFound || data.posts.length} mensajes del historial.`);
} }
} catch (err) { } catch (err) {
console.error(err); console.error(err);
@ -136,22 +136,24 @@ export default function SupplierFeedPage() {
} }
}; };
// Sincronizar mensajes en vivo desde Evolution API // Sincronizar mensajes en vivo desde Evolution API (con escaneo profundo opcional)
const handleSyncLive = async () => { const handleSyncLive = async (deep: boolean = false) => {
setIsSyncingLive(true); setIsSyncingLive(true);
setActionMessage(null); setActionMessage(null);
try { try {
const pagesCount = deep ? 60 : 35;
const limitCount = deep ? 5000 : 3500;
const url = const url =
selectedGroupFilter !== 'all' selectedGroupFilter !== 'all'
? `/api/whatsapp-feed?fetchLive=true&groupJid=${encodeURIComponent(selectedGroupFilter)}&status=${tabStatus}` ? `/api/whatsapp-feed?fetchLive=true&groupJid=${encodeURIComponent(selectedGroupFilter)}&status=${tabStatus}&pages=${pagesCount}&limit=${limitCount}`
: `/api/whatsapp-feed?fetchLive=true&limit=60&status=${tabStatus}`; : `/api/whatsapp-feed?fetchLive=true&status=${tabStatus}&pages=${pagesCount}&limit=${limitCount}`;
const res = await fetch(url); const res = await fetch(url);
const data = await res.json(); const data = await res.json();
if (data.success && Array.isArray(data.posts)) { if (data.success && Array.isArray(data.posts)) {
setPosts(data.posts); setPosts(data.posts);
setActionMessage( setActionMessage(
`✅ ¡Sincronización completa! Se escanearon los mensajes de WhatsApp y se omitieron los ya publicados y descartados.` `✅ ¡Sincronización completa! Se escanearon ${data.totalFound || data.posts.length} mensajes de WhatsApp sin límite.`
); );
} else { } else {
setActionMessage('⚠️ No se encontraron mensajes nuevos en los grupos seleccionados.'); setActionMessage('⚠️ No se encontraron mensajes nuevos en los grupos seleccionados.');
@ -352,7 +354,7 @@ export default function SupplierFeedPage() {
}); });
}; };
// Cambiar pestaña y actualizar vista // Cambiar pestaña y actualizar vista con todo el historial
const handleTabChange = async (status: 'pending' | 'published' | 'discarded' | 'all') => { const handleTabChange = async (status: 'pending' | 'published' | 'discarded' | 'all') => {
setTabStatus(status); setTabStatus(status);
setSelectedPostIds(new Set()); setSelectedPostIds(new Set());
@ -360,8 +362,8 @@ export default function SupplierFeedPage() {
try { try {
const url = const url =
selectedGroupFilter !== 'all' selectedGroupFilter !== 'all'
? `/api/whatsapp-feed?fetchLive=true&groupJid=${encodeURIComponent(selectedGroupFilter)}&status=${status}` ? `/api/whatsapp-feed?fetchLive=true&groupJid=${encodeURIComponent(selectedGroupFilter)}&status=${status}&pages=35&limit=3500`
: `/api/whatsapp-feed?fetchLive=true&limit=60&status=${status}`; : `/api/whatsapp-feed?fetchLive=true&status=${status}&pages=35&limit=3500`;
const res = await fetch(url); const res = await fetch(url);
const data = await res.json(); const data = await res.json();
if (data.success && Array.isArray(data.posts)) { if (data.success && Array.isArray(data.posts)) {
@ -455,12 +457,23 @@ export default function SupplierFeedPage() {
</Link> </Link>
<button <button
onClick={handleSyncLive} onClick={() => handleSyncLive(false)}
disabled={isSyncingLive}
className="bg-neutral-900 hover:bg-neutral-800 border border-neutral-700 text-neutral-300 font-bold text-xs px-3.5 py-2.5 rounded-2xl flex items-center gap-1.5 transition-all cursor-pointer disabled:opacity-50"
title="Sincronizar mensajes recientes"
>
<RefreshCw className={`w-4 h-4 text-purple-400 ${isSyncingLive ? 'animate-spin' : ''}`} />
<span>Sincronizar ({posts.length})</span>
</button>
<button
onClick={() => handleSyncLive(true)}
disabled={isSyncingLive} disabled={isSyncingLive}
className="bg-gradient-to-r from-purple-600 to-fuchsia-600 hover:opacity-95 text-white font-black text-xs px-4 py-2.5 rounded-2xl flex items-center gap-2 shadow-lg shadow-purple-600/30 cursor-pointer transition-all active:scale-95 disabled:opacity-50" className="bg-gradient-to-r from-purple-600 to-fuchsia-600 hover:opacity-95 text-white font-black text-xs px-4 py-2.5 rounded-2xl flex items-center gap-2 shadow-lg shadow-purple-600/30 cursor-pointer transition-all active:scale-95 disabled:opacity-50"
title="Escanear miles de mensajes en todo el historial"
> >
<RefreshCw className={`w-4 h-4 ${isSyncingLive ? 'animate-spin' : ''}`} /> <Layers className="w-4 h-4" />
<span>{isSyncingLive ? 'Consultando Grupos...' : 'Sincronizar WhatsApp en Vivo'}</span> <span>Escanear Todo el Historial (+1,000)</span>
</button> </button>
</div> </div>
</div> </div>
@ -953,15 +966,54 @@ export default function SupplierFeedPage() {
{/* Feed Pagination Bar */} {/* Feed Pagination Bar */}
{totalPages > 1 && ( {totalPages > 1 && (
<div className="bg-neutral-950 px-5 py-3.5 rounded-2xl border border-neutral-800 flex flex-col sm:flex-row items-center justify-between gap-3 text-xs"> <div className="bg-neutral-950 px-5 py-4 rounded-2xl border border-neutral-800 flex flex-col md:flex-row items-center justify-between gap-4 text-xs">
<span className="text-neutral-400"> <div className="flex items-center gap-3 flex-wrap">
Mostrando <strong className="text-white">{startIndex + 1}</strong> a{' '} <span className="text-neutral-400">
<strong className="text-white">{Math.min(startIndex + pageSize, filteredPosts.length)}</strong> de{' '} Mostrando <strong className="text-white">{startIndex + 1}</strong> a{' '}
<strong className="text-white">{filteredPosts.length}</strong> mensajes de WhatsApp (Pág.{' '} <strong className="text-white">{Math.min(startIndex + pageSize, filteredPosts.length)}</strong> de{' '}
<strong className="text-purple-400">{currentPage}</strong> de {totalPages}) <strong className="text-purple-400 text-sm">{filteredPosts.length}</strong> mensajes (Página{' '}
</span> <strong className="text-white font-bold">{currentPage}</strong> de {totalPages})
</span>
{/* Page Size Selector */}
<div className="flex items-center gap-1.5 text-neutral-400 ml-2 pl-3 border-l border-neutral-800">
<span>Por pág:</span>
<select
value={pageSize}
onChange={(e) => {
setPageSize(Number(e.target.value));
setCurrentPage(1);
}}
className="bg-neutral-900 border border-neutral-700 text-white rounded-lg px-2 py-1 text-xs outline-hidden focus:border-purple-500 cursor-pointer"
>
<option value={12}>12</option>
<option value={24}>24</option>
<option value={48}>48</option>
<option value={96}>96</option>
<option value={150}>150</option>
</select>
</div>
</div>
<div className="flex items-center gap-2 flex-wrap">
{/* Page Jump Selector */}
{totalPages > 5 && (
<div className="flex items-center gap-1 text-neutral-400 mr-2">
<span>Ir a:</span>
<select
value={currentPage}
onChange={(e) => setCurrentPage(Number(e.target.value))}
className="bg-neutral-900 border border-neutral-700 text-white rounded-lg px-2 py-1 text-xs outline-hidden focus:border-purple-500 cursor-pointer"
>
{Array.from({ length: totalPages }, (_, i) => i + 1).map((p) => (
<option key={p} value={p}>
Pág {p}
</option>
))}
</select>
</div>
)}
<div className="flex items-center gap-1.5">
<button <button
onClick={() => setCurrentPage(1)} onClick={() => setCurrentPage(1)}
disabled={currentPage === 1} disabled={currentPage === 1}
@ -988,7 +1040,7 @@ export default function SupplierFeedPage() {
<span>Anterior</span> <span>Anterior</span>
</button> </button>
<span className="px-2.5 font-bold text-white bg-purple-950/60 border border-purple-500/40 rounded-lg py-1"> <span className="px-3 font-bold text-white bg-purple-950/60 border border-purple-500/40 rounded-lg py-1">
{currentPage} / {totalPages} {currentPage} / {totalPages}
</span> </span>

View File

@ -22,7 +22,8 @@ export async function GET(request: Request) {
try { try {
const { searchParams } = new URL(request.url); const { searchParams } = new URL(request.url);
const fetchLive = searchParams.get('fetchLive') !== 'false'; const fetchLive = searchParams.get('fetchLive') !== 'false';
const limit = parseInt(searchParams.get('limit') || '60', 10); const limit = parseInt(searchParams.get('limit') || '3000', 10);
const maxPages = parseInt(searchParams.get('pages') || '30', 10);
const filterJid = searchParams.get('groupJid'); const filterJid = searchParams.get('groupJid');
const statusFilter = (searchParams.get('status') || 'pending').toLowerCase(); const statusFilter = (searchParams.get('status') || 'pending').toLowerCase();
@ -128,26 +129,45 @@ export async function GET(request: Request) {
}); });
} }
// 4. Consultar mensajes de cada grupo en paralelo desde Evolution API // 4. Consultar mensajes de los grupos objetivo recorriendo todas las páginas en paralelo
const groupFetchPromises = targetJids.map(async (jid) => { const groupFetchPromises = targetJids.map(async (jid) => {
try { const allGroupRecords: any[] = [];
const fetchLimit = Math.max(limit * 2, 80); const pagesToFetch = Array.from({ length: maxPages }, (_, i) => i + 1);
const msgRes = await fetch(
`${EVOLUTION_API_URL}/chat/findMessages/${EVOLUTION_INSTANCE_NAME}?page=1&limit=${fetchLimit}`, // Lotes concurrentes de 10 páginas para máxima velocidad y profundidad
{ const BATCH_SIZE = 10;
method: 'POST', for (let i = 0; i < pagesToFetch.length; i += BATCH_SIZE) {
headers: { const batch = pagesToFetch.slice(i, i + BATCH_SIZE);
apikey: EVOLUTION_API_KEY, const batchResults = await Promise.allSettled(
'Content-Type': 'application/json', batch.map(async (page) => {
}, const msgRes = await fetch(
body: JSON.stringify({ where: { key: { remoteJid: jid } } }), `${EVOLUTION_API_URL}/chat/findMessages/${EVOLUTION_INSTANCE_NAME}?page=${page}&limit=50`,
} {
method: 'POST',
headers: {
apikey: EVOLUTION_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({ where: { key: { remoteJid: jid } } }),
}
);
const msgData = await msgRes.json();
return msgData.messages?.records || msgData.records || (Array.isArray(msgData) ? msgData : []);
})
); );
const msgData = await msgRes.json();
return msgData.messages?.records || msgData.records || (Array.isArray(msgData) ? msgData : []); let hitEnd = false;
} catch (err) { for (const res of batchResults) {
return []; if (res.status === 'fulfilled' && Array.isArray(res.value)) {
allGroupRecords.push(...res.value);
if (res.value.length === 0) {
hitEnd = true;
}
}
}
if (hitEnd) break;
} }
return allGroupRecords;
}); });
const results = await Promise.allSettled(groupFetchPromises); const results = await Promise.allSettled(groupFetchPromises);
@ -229,25 +249,13 @@ export async function GET(request: Request) {
} }
let imageUrls: string[] = []; let imageUrls: string[] = [];
const imgMsg = record.message?.imageMessage;
if (hasImage) { if (imgMsg) {
try { if (imgMsg.jpegThumbnail) {
const mediaUrl = `${EVOLUTION_API_URL}/chat/getBase64FromMediaMessage/${EVOLUTION_INSTANCE_NAME}`; imageUrls.push(`data:image/jpeg;base64,${imgMsg.jpegThumbnail}`);
const mediaRes = await fetch(mediaUrl, { } else if (imgMsg.url) {
method: 'POST', imageUrls.push(imgMsg.url);
headers: {
apikey: EVOLUTION_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({ message: record, convertToMp4: false }),
});
const mediaData = await mediaRes.json();
if (mediaData.base64) {
const mime = mediaData.mimetype || 'image/jpeg';
imageUrls.push(`data:${mime};base64,${mediaData.base64}`);
}
} catch (mediaErr) {
console.warn('Error descargando imagen de WhatsApp:', mediaErr);
} }
} }
@ -260,7 +268,7 @@ export async function GET(request: Request) {
let extracted = existingInAppwrite?.extractedProduct; let extracted = existingInAppwrite?.extractedProduct;
if (!extracted) { if (!extracted) {
const cleanInputText = text || ''; const cleanInputText = text || '';
extracted = await parseSupplierMessage(cleanInputText, imageUrls[0], [groupName]); extracted = await parseSupplierMessage(cleanInputText, undefined, [groupName]);
} else if (extracted && extracted.name) { } else if (extracted && extracted.name) {
extracted.name = sanitizePublicationTitle(extracted.name, [groupName], extracted.brand); extracted.name = sanitizePublicationTitle(extracted.name, [groupName], extracted.brand);
} }

View File

@ -13,7 +13,8 @@ export async function GET(request: Request) {
try { try {
const { searchParams } = new URL(request.url); const { searchParams } = new URL(request.url);
const fetchLive = searchParams.get('fetchLive') !== 'false'; const fetchLive = searchParams.get('fetchLive') !== 'false';
const limit = parseInt(searchParams.get('limit') || '50', 10); const limit = parseInt(searchParams.get('limit') || '3000', 10);
const maxPages = parseInt(searchParams.get('pages') || '30', 10);
const filterJid = searchParams.get('groupJid'); const filterJid = searchParams.get('groupJid');
const statusFilter = (searchParams.get('status') || 'pending').toLowerCase(); const statusFilter = (searchParams.get('status') || 'pending').toLowerCase();
@ -99,26 +100,44 @@ export async function GET(request: Request) {
}); });
} }
// 3. Consultar mensajes de cada grupo objetivo en paralelo // 3. Consultar mensajes de cada grupo objetivo recorriendo múltiples páginas en paralelo
const groupFetchPromises = targetJids.map(async (jid) => { const groupFetchPromises = targetJids.map(async (jid) => {
try { const allGroupRecords: any[] = [];
const fetchLimit = Math.max(limit * 2, 80); const pagesToFetch = Array.from({ length: maxPages }, (_, i) => i + 1);
const msgRes = await fetch(
`${EVOLUTION_API_URL}/chat/findMessages/${EVOLUTION_INSTANCE_NAME}?page=1&limit=${fetchLimit}`, const BATCH_SIZE = 10;
{ for (let i = 0; i < pagesToFetch.length; i += BATCH_SIZE) {
method: 'POST', const batch = pagesToFetch.slice(i, i + BATCH_SIZE);
headers: { const batchResults = await Promise.allSettled(
apikey: EVOLUTION_API_KEY, batch.map(async (page) => {
'Content-Type': 'application/json', const msgRes = await fetch(
}, `${EVOLUTION_API_URL}/chat/findMessages/${EVOLUTION_INSTANCE_NAME}?page=${page}&limit=50`,
body: JSON.stringify({ where: { key: { remoteJid: jid } } }), {
} method: 'POST',
headers: {
apikey: EVOLUTION_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({ where: { key: { remoteJid: jid } } }),
}
);
const msgData = await msgRes.json();
return msgData.messages?.records || msgData.records || (Array.isArray(msgData) ? msgData : []);
})
); );
const msgData = await msgRes.json();
return msgData.messages?.records || msgData.records || (Array.isArray(msgData) ? msgData : []); let hitEnd = false;
} catch (err) { for (const res of batchResults) {
return []; if (res.status === 'fulfilled' && Array.isArray(res.value)) {
allGroupRecords.push(...res.value);
if (res.value.length === 0) {
hitEnd = true;
}
}
}
if (hitEnd) break;
} }
return allGroupRecords;
}); });
const results = await Promise.allSettled(groupFetchPromises); const results = await Promise.allSettled(groupFetchPromises);
@ -132,7 +151,7 @@ export async function GET(request: Request) {
// 4. Ordenar cronológicamente (más recientes primero) // 4. Ordenar cronológicamente (más recientes primero)
allRecords.sort((a, b) => (b.messageTimestamp || 0) - (a.messageTimestamp || 0)); allRecords.sort((a, b) => (b.messageTimestamp || 0) - (a.messageTimestamp || 0));
// 5. Procesar cada mensaje y descargar imágenes/IA // 5. Procesar cada mensaje con control de identificador de WhatsApp
const processedPosts: SupplierPost[] = []; const processedPosts: SupplierPost[] = [];
const seenMessageIds = new Set<string>(); const seenMessageIds = new Set<string>();
@ -191,26 +210,13 @@ export async function GET(request: Request) {
} }
let imageUrls: string[] = []; let imageUrls: string[] = [];
const imgMsg = record.message?.imageMessage;
// Descargar imagen en Base64 desde Evolution API si está presente if (imgMsg) {
if (hasImage) { if (imgMsg.jpegThumbnail) {
try { imageUrls.push(`data:image/jpeg;base64,${imgMsg.jpegThumbnail}`);
const mediaUrl = `${EVOLUTION_API_URL}/chat/getBase64FromMediaMessage/${EVOLUTION_INSTANCE_NAME}`; } else if (imgMsg.url) {
const mediaRes = await fetch(mediaUrl, { imageUrls.push(imgMsg.url);
method: 'POST',
headers: {
apikey: EVOLUTION_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({ message: record, convertToMp4: false }),
});
const mediaData = await mediaRes.json();
if (mediaData.base64) {
const mime = mediaData.mimetype || 'image/jpeg';
imageUrls.push(`data:${mime};base64,${mediaData.base64}`);
}
} catch (mediaErr) {
console.warn('Error descargando imagen de WhatsApp:', mediaErr);
} }
} }