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
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 () => {
setIsLoading(true);
try {
@ -95,8 +95,8 @@ export default function SupplierFeedPage() {
setExistingProducts(prodData.products);
}
// 3. Posts en vivo desde los grupos de WhatsApp (iniciando con PENDIENTES para que la lista avance)
const feedRes = await fetch('/api/whatsapp-feed?fetchLive=true&limit=60&status=pending');
// 3. Posts en vivo desde los grupos de WhatsApp (escaneando todo el historial profundo)
const feedRes = await fetch('/api/whatsapp-feed?fetchLive=true&pages=35&limit=3500&status=pending');
const feedData = await feedRes.json();
if (feedData.success && Array.isArray(feedData.posts)) {
setPosts(feedData.posts);
@ -112,7 +112,7 @@ export default function SupplierFeedPage() {
loadInitialData();
}, []);
// Cambiar filtro de grupo y recargar mensajes
// Cambiar filtro de grupo y recargar todos los mensajes del grupo
const handleGroupFilterChange = async (groupJid: string) => {
setSelectedGroupFilter(groupJid);
setIsSyncingLive(true);
@ -120,13 +120,13 @@ export default function SupplierFeedPage() {
try {
const url =
groupJid !== 'all'
? `/api/whatsapp-feed?fetchLive=true&groupJid=${encodeURIComponent(groupJid)}&status=${tabStatus}`
: `/api/whatsapp-feed?fetchLive=true&limit=60&status=${tabStatus}`;
? `/api/whatsapp-feed?fetchLive=true&groupJid=${encodeURIComponent(groupJid)}&status=${tabStatus}&pages=35&limit=3500`
: `/api/whatsapp-feed?fetchLive=true&status=${tabStatus}&pages=35&limit=3500`;
const res = await fetch(url);
const data = await res.json();
if (data.success && Array.isArray(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) {
console.error(err);
@ -136,22 +136,24 @@ export default function SupplierFeedPage() {
}
};
// Sincronizar mensajes en vivo desde Evolution API
const handleSyncLive = async () => {
// Sincronizar mensajes en vivo desde Evolution API (con escaneo profundo opcional)
const handleSyncLive = async (deep: boolean = false) => {
setIsSyncingLive(true);
setActionMessage(null);
try {
const pagesCount = deep ? 60 : 35;
const limitCount = deep ? 5000 : 3500;
const url =
selectedGroupFilter !== 'all'
? `/api/whatsapp-feed?fetchLive=true&groupJid=${encodeURIComponent(selectedGroupFilter)}&status=${tabStatus}`
: `/api/whatsapp-feed?fetchLive=true&limit=60&status=${tabStatus}`;
? `/api/whatsapp-feed?fetchLive=true&groupJid=${encodeURIComponent(selectedGroupFilter)}&status=${tabStatus}&pages=${pagesCount}&limit=${limitCount}`
: `/api/whatsapp-feed?fetchLive=true&status=${tabStatus}&pages=${pagesCount}&limit=${limitCount}`;
const res = await fetch(url);
const data = await res.json();
if (data.success && Array.isArray(data.posts)) {
setPosts(data.posts);
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 {
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') => {
setTabStatus(status);
setSelectedPostIds(new Set());
@ -360,8 +362,8 @@ export default function SupplierFeedPage() {
try {
const url =
selectedGroupFilter !== 'all'
? `/api/whatsapp-feed?fetchLive=true&groupJid=${encodeURIComponent(selectedGroupFilter)}&status=${status}`
: `/api/whatsapp-feed?fetchLive=true&limit=60&status=${status}`;
? `/api/whatsapp-feed?fetchLive=true&groupJid=${encodeURIComponent(selectedGroupFilter)}&status=${status}&pages=35&limit=3500`
: `/api/whatsapp-feed?fetchLive=true&status=${status}&pages=35&limit=3500`;
const res = await fetch(url);
const data = await res.json();
if (data.success && Array.isArray(data.posts)) {
@ -455,12 +457,23 @@ export default function SupplierFeedPage() {
</Link>
<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}
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' : ''}`} />
<span>{isSyncingLive ? 'Consultando Grupos...' : 'Sincronizar WhatsApp en Vivo'}</span>
<Layers className="w-4 h-4" />
<span>Escanear Todo el Historial (+1,000)</span>
</button>
</div>
</div>
@ -953,15 +966,54 @@ export default function SupplierFeedPage() {
{/* Feed Pagination Bar */}
{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">
<span className="text-neutral-400">
Mostrando <strong className="text-white">{startIndex + 1}</strong> a{' '}
<strong className="text-white">{Math.min(startIndex + pageSize, filteredPosts.length)}</strong> de{' '}
<strong className="text-white">{filteredPosts.length}</strong> mensajes de WhatsApp (Pág.{' '}
<strong className="text-purple-400">{currentPage}</strong> de {totalPages})
</span>
<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">
<div className="flex items-center gap-3 flex-wrap">
<span className="text-neutral-400">
Mostrando <strong className="text-white">{startIndex + 1}</strong> a{' '}
<strong className="text-white">{Math.min(startIndex + pageSize, filteredPosts.length)}</strong> de{' '}
<strong className="text-purple-400 text-sm">{filteredPosts.length}</strong> mensajes (Página{' '}
<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
onClick={() => setCurrentPage(1)}
disabled={currentPage === 1}
@ -988,7 +1040,7 @@ export default function SupplierFeedPage() {
<span>Anterior</span>
</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}
</span>

View File

@ -22,7 +22,8 @@ export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url);
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 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) => {
try {
const fetchLimit = Math.max(limit * 2, 80);
const msgRes = await fetch(
`${EVOLUTION_API_URL}/chat/findMessages/${EVOLUTION_INSTANCE_NAME}?page=1&limit=${fetchLimit}`,
{
method: 'POST',
headers: {
apikey: EVOLUTION_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({ where: { key: { remoteJid: jid } } }),
}
const allGroupRecords: any[] = [];
const pagesToFetch = Array.from({ length: maxPages }, (_, i) => i + 1);
// Lotes concurrentes de 10 páginas para máxima velocidad y profundidad
const BATCH_SIZE = 10;
for (let i = 0; i < pagesToFetch.length; i += BATCH_SIZE) {
const batch = pagesToFetch.slice(i, i + BATCH_SIZE);
const batchResults = await Promise.allSettled(
batch.map(async (page) => {
const msgRes = await fetch(
`${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 : []);
} catch (err) {
return [];
let hitEnd = false;
for (const res of batchResults) {
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);
@ -229,25 +249,13 @@ export async function GET(request: Request) {
}
let imageUrls: string[] = [];
const imgMsg = record.message?.imageMessage;
if (hasImage) {
try {
const mediaUrl = `${EVOLUTION_API_URL}/chat/getBase64FromMediaMessage/${EVOLUTION_INSTANCE_NAME}`;
const mediaRes = await fetch(mediaUrl, {
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);
if (imgMsg) {
if (imgMsg.jpegThumbnail) {
imageUrls.push(`data:image/jpeg;base64,${imgMsg.jpegThumbnail}`);
} else if (imgMsg.url) {
imageUrls.push(imgMsg.url);
}
}
@ -260,7 +268,7 @@ export async function GET(request: Request) {
let extracted = existingInAppwrite?.extractedProduct;
if (!extracted) {
const cleanInputText = text || '';
extracted = await parseSupplierMessage(cleanInputText, imageUrls[0], [groupName]);
extracted = await parseSupplierMessage(cleanInputText, undefined, [groupName]);
} else if (extracted && extracted.name) {
extracted.name = sanitizePublicationTitle(extracted.name, [groupName], extracted.brand);
}

View File

@ -13,7 +13,8 @@ export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url);
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 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) => {
try {
const fetchLimit = Math.max(limit * 2, 80);
const msgRes = await fetch(
`${EVOLUTION_API_URL}/chat/findMessages/${EVOLUTION_INSTANCE_NAME}?page=1&limit=${fetchLimit}`,
{
method: 'POST',
headers: {
apikey: EVOLUTION_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({ where: { key: { remoteJid: jid } } }),
}
const allGroupRecords: any[] = [];
const pagesToFetch = Array.from({ length: maxPages }, (_, i) => i + 1);
const BATCH_SIZE = 10;
for (let i = 0; i < pagesToFetch.length; i += BATCH_SIZE) {
const batch = pagesToFetch.slice(i, i + BATCH_SIZE);
const batchResults = await Promise.allSettled(
batch.map(async (page) => {
const msgRes = await fetch(
`${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 : []);
} catch (err) {
return [];
let hitEnd = false;
for (const res of batchResults) {
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);
@ -132,7 +151,7 @@ export async function GET(request: Request) {
// 4. Ordenar cronológicamente (más recientes primero)
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 seenMessageIds = new Set<string>();
@ -191,26 +210,13 @@ export async function GET(request: Request) {
}
let imageUrls: string[] = [];
const imgMsg = record.message?.imageMessage;
// Descargar imagen en Base64 desde Evolution API si está presente
if (hasImage) {
try {
const mediaUrl = `${EVOLUTION_API_URL}/chat/getBase64FromMediaMessage/${EVOLUTION_INSTANCE_NAME}`;
const mediaRes = await fetch(mediaUrl, {
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);
if (imgMsg) {
if (imgMsg.jpegThumbnail) {
imageUrls.push(`data:image/jpeg;base64,${imgMsg.jpegThumbnail}`);
} else if (imgMsg.url) {
imageUrls.push(imgMsg.url);
}
}