feat: categorizacion inteligente de zapatillas, chanclas, bolsos, maletines, carrieles, gorras y ropa
This commit is contained in:
parent
68f6954001
commit
eb4f1affd4
@ -29,6 +29,7 @@ import {
|
||||
Store,
|
||||
DollarSign,
|
||||
HelpCircle,
|
||||
Tag,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
ChevronsLeft,
|
||||
@ -39,11 +40,36 @@ import { formatCOP, calculateMarketPricingStrategies } from '@/lib/pricing-engin
|
||||
import { BRANDS, BrandId } from '@/config/brands';
|
||||
import { OptimizedImage } from '@/components/ui/OptimizedImage';
|
||||
|
||||
const CATEGORY_TABS = [
|
||||
{ id: 'all', label: 'Todas las Categorías', icon: '🔥' },
|
||||
{ id: 'zapatillas', label: 'Zapatillas', icon: '👟' },
|
||||
{ id: 'chanclas', label: 'Chanclas / Sandalias', icon: '🩴' },
|
||||
{ id: 'bolsos', label: 'Bolsos / Carteras', icon: '👜' },
|
||||
{ id: 'maletines', label: 'Maletines / Morrales', icon: '🎒' },
|
||||
{ id: 'carrieles', label: 'Carrieles / Bandoleras', icon: '👝' },
|
||||
{ id: 'gorras', label: 'Gorras / Caps', icon: '🧢' },
|
||||
{ id: 'ropa', label: 'Ropa / Streetwear', icon: '👕' },
|
||||
{ id: 'accesorios', label: 'Accesorios', icon: '🕶️' },
|
||||
];
|
||||
|
||||
function getCategoryIcon(category?: string): string {
|
||||
const cat = (category || '').toLowerCase();
|
||||
if (cat.includes('chancla') || cat.includes('slide') || cat.includes('sandalia') || cat.includes('croc')) return '🩴';
|
||||
if (cat.includes('carriel') || cat.includes('bandolera') || cat.includes('riñonera') || cat.includes('crossbody') || cat.includes('pechera')) return '👝';
|
||||
if (cat.includes('malet') || cat.includes('morral') || cat.includes('mochila') || cat.includes('tula')) return '🎒';
|
||||
if (cat.includes('bolso') || cat.includes('cartera') || cat.includes('tote')) return '👜';
|
||||
if (cat.includes('gorra') || cat.includes('cap')) return '🧢';
|
||||
if (cat.includes('ropa') || cat.includes('camiseta') || cat.includes('hoodie')) return '👕';
|
||||
if (cat.includes('accesorio') || cat.includes('billetera')) return '🕶️';
|
||||
return '👟';
|
||||
}
|
||||
|
||||
export default function SupplierFeedPage() {
|
||||
const [posts, setPosts] = useState<SupplierPost[]>([]);
|
||||
const [monitoredGroups, setMonitoredGroups] = useState<MonitoredWhatsAppGroup[]>([]);
|
||||
const [existingProducts, setExistingProducts] = useState<Product[]>([]);
|
||||
const [selectedGroupFilter, setSelectedGroupFilter] = useState<string>('all');
|
||||
const [selectedCategoryFilter, setSelectedCategoryFilter] = useState<string>('all');
|
||||
const [tabStatus, setTabStatus] = useState<'pending' | 'published' | 'discarded' | 'all'>('pending');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isSyncingLive, setIsSyncingLive] = useState(false);
|
||||
@ -65,6 +91,7 @@ export default function SupplierFeedPage() {
|
||||
const [editBrand, setEditBrand] = useState('');
|
||||
const [editModelName, setEditModelName] = useState('');
|
||||
const [editColorName, setEditColorName] = useState('');
|
||||
const [editCategory, setEditCategory] = useState('Zapatillas');
|
||||
const [editStyleCode, setEditStyleCode] = useState('');
|
||||
const [editPrice, setEditPrice] = useState(125000);
|
||||
const [editSupplierPrice, setEditSupplierPrice] = useState(85000);
|
||||
@ -216,6 +243,7 @@ export default function SupplierFeedPage() {
|
||||
brand?: string;
|
||||
modelName?: string;
|
||||
colorName?: string;
|
||||
category?: string;
|
||||
retailPrice?: number;
|
||||
sizes?: string[];
|
||||
marketIntelligence?: any;
|
||||
@ -231,6 +259,7 @@ export default function SupplierFeedPage() {
|
||||
customName: customData?.name,
|
||||
customBrand: customData?.brand,
|
||||
customColorName: customData?.colorName,
|
||||
customCategory: customData?.category,
|
||||
customRetailPrice: customData?.retailPrice,
|
||||
customSizes: customData?.sizes,
|
||||
marketIntelligence: customData?.marketIntelligence,
|
||||
@ -354,7 +383,19 @@ export default function SupplierFeedPage() {
|
||||
});
|
||||
};
|
||||
|
||||
// Cambiar pestaña y actualizar vista con todo el historial
|
||||
const handleSelectAllCurrentPage = () => {
|
||||
const next = new Set(selectedPostIds);
|
||||
paginatedPosts.forEach((p) => {
|
||||
if (p.status !== 'published') next.add(p.id);
|
||||
});
|
||||
setSelectedPostIds(next);
|
||||
};
|
||||
|
||||
const handleDeselectAll = () => {
|
||||
setSelectedPostIds(new Set());
|
||||
};
|
||||
|
||||
// Cambiar pestaña y actualizar vista
|
||||
const handleTabChange = async (status: 'pending' | 'published' | 'discarded' | 'all') => {
|
||||
setTabStatus(status);
|
||||
setSelectedPostIds(new Set());
|
||||
@ -372,8 +413,8 @@ export default function SupplierFeedPage() {
|
||||
} catch (_) {}
|
||||
};
|
||||
|
||||
// Filtrado de posts por estado y grupo
|
||||
const filteredPosts = useMemo(() => {
|
||||
// Pre-filtrado por estado y grupo
|
||||
const postsByStatusAndGroup = useMemo(() => {
|
||||
return posts.filter((p) => {
|
||||
// Estado
|
||||
if (tabStatus === 'pending' && (p.status === 'published' || p.status === 'discarded')) return false;
|
||||
@ -388,10 +429,115 @@ export default function SupplierFeedPage() {
|
||||
});
|
||||
}, [posts, tabStatus, selectedGroupFilter]);
|
||||
|
||||
// Contadores dinámicos por categoría
|
||||
const categoryCounts = useMemo(() => {
|
||||
const counts: Record<string, number> = { all: postsByStatusAndGroup.length };
|
||||
for (const p of postsByStatusAndGroup) {
|
||||
const cat = (p.extractedProduct?.category || '').toLowerCase();
|
||||
const combined = `${cat} ${(p.extractedProduct?.name || '').toLowerCase()} ${(p.rawMessage || '').toLowerCase()}`;
|
||||
if (combined.includes('chancla') || combined.includes('slide') || combined.includes('sandalia') || combined.includes('croc')) {
|
||||
counts.chanclas = (counts.chanclas || 0) + 1;
|
||||
} else if (
|
||||
combined.includes('carriel') ||
|
||||
combined.includes('bandolera') ||
|
||||
combined.includes('riñonera') ||
|
||||
combined.includes('rinonera') ||
|
||||
combined.includes('crossbody') ||
|
||||
combined.includes('pechera')
|
||||
) {
|
||||
counts.carrieles = (counts.carrieles || 0) + 1;
|
||||
} else if (
|
||||
combined.includes('malet') ||
|
||||
combined.includes('morral') ||
|
||||
combined.includes('mochila') ||
|
||||
combined.includes('backpack') ||
|
||||
combined.includes('tula')
|
||||
) {
|
||||
counts.maletines = (counts.maletines || 0) + 1;
|
||||
} else if (combined.includes('bolso') || combined.includes('cartera') || combined.includes('tote')) {
|
||||
counts.bolsos = (counts.bolsos || 0) + 1;
|
||||
} else if (combined.includes('gorra') || combined.includes('cap')) {
|
||||
counts.gorras = (counts.gorras || 0) + 1;
|
||||
} else if (combined.includes('ropa') || combined.includes('camiseta') || combined.includes('hoodie')) {
|
||||
counts.ropa = (counts.ropa || 0) + 1;
|
||||
} else if (combined.includes('billetera') || combined.includes('reloj') || combined.includes('gafas')) {
|
||||
counts.accesorios = (counts.accesorios || 0) + 1;
|
||||
} else {
|
||||
counts.zapatillas = (counts.zapatillas || 0) + 1;
|
||||
}
|
||||
}
|
||||
return counts;
|
||||
}, [postsByStatusAndGroup]);
|
||||
|
||||
// Filtrado final incluyendo categoría seleccionada
|
||||
const filteredPosts = useMemo(() => {
|
||||
if (selectedCategoryFilter === 'all') return postsByStatusAndGroup;
|
||||
|
||||
return postsByStatusAndGroup.filter((p) => {
|
||||
const cat = (p.extractedProduct?.category || '').toLowerCase();
|
||||
const combined = `${cat} ${(p.extractedProduct?.name || '').toLowerCase()} ${(p.rawMessage || '').toLowerCase()}`;
|
||||
|
||||
if (selectedCategoryFilter === 'chanclas') {
|
||||
return combined.includes('chancla') || combined.includes('slide') || combined.includes('sandalia') || combined.includes('croc');
|
||||
}
|
||||
if (selectedCategoryFilter === 'carrieles') {
|
||||
return (
|
||||
combined.includes('carriel') ||
|
||||
combined.includes('bandolera') ||
|
||||
combined.includes('riñonera') ||
|
||||
combined.includes('rinonera') ||
|
||||
combined.includes('crossbody') ||
|
||||
combined.includes('pechera')
|
||||
);
|
||||
}
|
||||
if (selectedCategoryFilter === 'maletines') {
|
||||
return (
|
||||
combined.includes('malet') ||
|
||||
combined.includes('morral') ||
|
||||
combined.includes('mochila') ||
|
||||
combined.includes('backpack') ||
|
||||
combined.includes('tula')
|
||||
);
|
||||
}
|
||||
if (selectedCategoryFilter === 'bolsos') {
|
||||
return combined.includes('bolso') || combined.includes('cartera') || combined.includes('tote');
|
||||
}
|
||||
if (selectedCategoryFilter === 'gorras') {
|
||||
return combined.includes('gorra') || combined.includes('cap');
|
||||
}
|
||||
if (selectedCategoryFilter === 'ropa') {
|
||||
return combined.includes('ropa') || combined.includes('camiseta') || combined.includes('hoodie');
|
||||
}
|
||||
if (selectedCategoryFilter === 'accesorios') {
|
||||
return combined.includes('billetera') || combined.includes('reloj') || combined.includes('gafas');
|
||||
}
|
||||
if (selectedCategoryFilter === 'zapatillas') {
|
||||
const isOther =
|
||||
combined.includes('chancla') ||
|
||||
combined.includes('slide') ||
|
||||
combined.includes('sandalia') ||
|
||||
combined.includes('croc') ||
|
||||
combined.includes('carriel') ||
|
||||
combined.includes('bandolera') ||
|
||||
combined.includes('riñonera') ||
|
||||
combined.includes('malet') ||
|
||||
combined.includes('morral') ||
|
||||
combined.includes('mochila') ||
|
||||
combined.includes('bolso') ||
|
||||
combined.includes('cartera') ||
|
||||
combined.includes('gorra') ||
|
||||
combined.includes('ropa') ||
|
||||
combined.includes('billetera');
|
||||
return !isOther || cat.includes('zapatilla');
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}, [postsByStatusAndGroup, selectedCategoryFilter]);
|
||||
|
||||
// Reset pagination on filter change
|
||||
useEffect(() => {
|
||||
setCurrentPage(1);
|
||||
}, [tabStatus, selectedGroupFilter, pageSize]);
|
||||
}, [tabStatus, selectedGroupFilter, selectedCategoryFilter, pageSize]);
|
||||
|
||||
const totalPages = Math.max(1, Math.ceil(filteredPosts.length / pageSize));
|
||||
const startIndex = (currentPage - 1) * pageSize;
|
||||
@ -406,10 +552,11 @@ export default function SupplierFeedPage() {
|
||||
|
||||
const handleOpenEdit = (post: SupplierPost) => {
|
||||
setEditingPost(post);
|
||||
const prodName = post.extractedProduct?.name || 'Zapatilla Urbana';
|
||||
const prodBrand = post.extractedProduct?.brand || 'Nike';
|
||||
const prodName = post.extractedProduct?.name || 'Artículo Urbano';
|
||||
const prodBrand = post.extractedProduct?.brand || 'Urbano / Variado';
|
||||
setEditName(prodName);
|
||||
setEditBrand(prodBrand);
|
||||
setEditCategory(post.extractedProduct?.category || 'Zapatillas');
|
||||
setEditModelName(post.extractedProduct?.modelName || prodName);
|
||||
setEditColorName(post.extractedProduct?.colorName || '');
|
||||
setEditStyleCode(post.extractedProduct?.styleCode || '');
|
||||
@ -419,11 +566,11 @@ export default function SupplierFeedPage() {
|
||||
(post.extractedProduct?.supplierPrice ? post.extractedProduct.supplierPrice + 35000 : 125000)
|
||||
);
|
||||
setEditMarketPriceCOP(post.extractedProduct?.marketPriceCOP || 450000);
|
||||
setEditMarketPriceSource(post.extractedProduct?.marketPriceSource || 'Nike Store Oficial Colombia');
|
||||
setEditMarketPriceSourceUrl(post.extractedProduct?.marketPriceSourceUrl || 'https://www.nike.com/co/');
|
||||
setEditMarketPriceSource(post.extractedProduct?.marketPriceSource || 'Tienda Oficial Colombia');
|
||||
setEditMarketPriceSourceUrl(post.extractedProduct?.marketPriceSourceUrl || 'https://www.google.com.co/');
|
||||
setEditMarketSummary(
|
||||
post.extractedProduct?.marketComparisonSummary ||
|
||||
`En tiendas oficiales se vende a ${formatCOP(post.extractedProduct?.marketPriceCOP || 450000)}. Gran margen y ahorro para el cliente.`
|
||||
`En tiendas oficiales se comercializa en ${formatCOP(post.extractedProduct?.marketPriceCOP || 450000)}. Gran margen y ahorro para el cliente.`
|
||||
);
|
||||
setEditSizes(post.extractedProduct?.sizes?.join(', ') || '37, 38, 39, 40, 41, 42, 43');
|
||||
};
|
||||
@ -573,6 +720,38 @@ export default function SupplierFeedPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Category Filter Pills Bar */}
|
||||
<div className="bg-neutral-900/70 border border-neutral-800/80 rounded-2xl p-3 flex items-center gap-2 overflow-x-auto scrollbar-none backdrop-blur-md">
|
||||
<span className="text-xs text-neutral-400 font-bold shrink-0 flex items-center gap-1.5 pl-1 pr-2 border-r border-neutral-800">
|
||||
<Tag className="w-3.5 h-3.5 text-purple-400" /> Categorías:
|
||||
</span>
|
||||
{CATEGORY_TABS.map((cat) => {
|
||||
const isActive = selectedCategoryFilter === cat.id;
|
||||
const count = categoryCounts[cat.id] || 0;
|
||||
return (
|
||||
<button
|
||||
key={cat.id}
|
||||
onClick={() => setSelectedCategoryFilter(cat.id)}
|
||||
className={`shrink-0 text-xs font-bold px-3 py-1.5 rounded-xl transition-all duration-200 cursor-pointer flex items-center gap-1.5 border ${
|
||||
isActive
|
||||
? 'bg-purple-600 text-white border-purple-400 shadow-md shadow-purple-900/30 scale-105'
|
||||
: 'bg-neutral-950/80 text-neutral-400 border-neutral-800 hover:text-white hover:border-neutral-700'
|
||||
}`}
|
||||
>
|
||||
<span>{cat.icon}</span>
|
||||
<span>{cat.label}</span>
|
||||
<span
|
||||
className={`text-[10px] px-1.5 py-0.2 rounded-full font-mono ${
|
||||
isActive ? 'bg-purple-950 text-purple-200' : 'bg-neutral-800 text-neutral-400'
|
||||
}`}
|
||||
>
|
||||
{count}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Bulk Action Bar */}
|
||||
{selectedPostIds.size > 0 && (
|
||||
<div className="bg-purple-900/40 border border-purple-500/50 rounded-2xl p-4 flex items-center justify-between gap-4 animate-in slide-in-from-top-2">
|
||||
@ -716,9 +895,14 @@ export default function SupplierFeedPage() {
|
||||
<div className="p-4 space-y-3">
|
||||
<div>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-[10px] font-bold text-purple-400 uppercase tracking-wider">
|
||||
{ext?.brand || 'Sneakers'}
|
||||
</span>
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
<span className="text-[10px] font-bold text-purple-400 uppercase tracking-wider">
|
||||
{ext?.brand || 'Urbano'}
|
||||
</span>
|
||||
<span className="text-[10px] font-bold px-2 py-0.5 rounded-full bg-neutral-950 text-neutral-300 border border-neutral-800 flex items-center gap-1 shadow-xs">
|
||||
{getCategoryIcon(ext?.category)} {ext?.category || 'Zapatillas'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span
|
||||
className="text-[9px] font-mono bg-neutral-950 text-neutral-400 px-2 py-0.5 rounded-md border border-neutral-800/80"
|
||||
@ -1139,6 +1323,24 @@ export default function SupplierFeedPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-[11px] font-bold text-purple-400 mb-1">Categoría del Producto *</label>
|
||||
<select
|
||||
value={editCategory}
|
||||
onChange={(e) => setEditCategory(e.target.value)}
|
||||
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-xs text-white outline-hidden focus:border-purple-500"
|
||||
>
|
||||
<option value="Zapatillas">👟 Zapatillas / Sneakers</option>
|
||||
<option value="Chanclas">🩴 Chanclas / Sandalias / Slides</option>
|
||||
<option value="Bolsos">👜 Bolsos / Carteras</option>
|
||||
<option value="Maletines">🎒 Maletines / Morrales</option>
|
||||
<option value="Carrieles">👝 Carrieles / Bandoleras</option>
|
||||
<option value="Gorras">🧢 Gorras / Caps</option>
|
||||
<option value="Ropa">👕 Ropa / Streetwear</option>
|
||||
<option value="Accesorios">🕶️ Accesorios Varios</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-[11px] font-bold text-neutral-400 mb-1">Color / Colorway</label>
|
||||
<input
|
||||
@ -1331,6 +1533,7 @@ export default function SupplierFeedPage() {
|
||||
brand: editBrand,
|
||||
modelName: editModelName,
|
||||
colorName: editColorName,
|
||||
category: editCategory,
|
||||
retailPrice: editPrice,
|
||||
sizes: sizesArr,
|
||||
marketIntelligence: {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { dataService } from '@/lib/data-service';
|
||||
import { parseSupplierMessage } from '@/lib/gemini-parser';
|
||||
import { parseSupplierMessage, detectCategory } from '@/lib/gemini-parser';
|
||||
import { performMarketIntelligenceSearch } from '@/lib/market-intelligence';
|
||||
import { getProductSocialProof } from '@/lib/social-proof';
|
||||
import { generateAttractiveProductCopy } from '@/lib/attractive-copy';
|
||||
@ -269,8 +269,13 @@ export async function GET(request: Request) {
|
||||
if (!extracted) {
|
||||
const cleanInputText = text || '';
|
||||
extracted = await parseSupplierMessage(cleanInputText, undefined, [groupName]);
|
||||
} else if (extracted && extracted.name) {
|
||||
extracted.name = sanitizePublicationTitle(extracted.name, [groupName], extracted.brand);
|
||||
} else if (extracted) {
|
||||
if (extracted.name) {
|
||||
extracted.name = sanitizePublicationTitle(extracted.name, [groupName], extracted.brand);
|
||||
}
|
||||
if (!extracted.category || extracted.category === 'Calzado / Sneakers' || extracted.category === 'Sneakers / Colección') {
|
||||
extracted.category = detectCategory(`${extracted.name || ''} ${text}`).category;
|
||||
}
|
||||
}
|
||||
|
||||
const timestamp = record.messageTimestamp
|
||||
@ -335,6 +340,7 @@ export async function POST(request: Request) {
|
||||
customBrand,
|
||||
customColorName,
|
||||
customSizes,
|
||||
customCategory,
|
||||
marketIntelligence,
|
||||
} = body;
|
||||
|
||||
@ -344,15 +350,17 @@ export async function POST(request: Request) {
|
||||
post.extractedProduct?.suggestedRetailPrice ||
|
||||
(post.extractedProduct?.supplierPrice ? post.extractedProduct.supplierPrice + 35000 : 125000);
|
||||
|
||||
const rawProdName = customName || post.extractedProduct?.name || 'Zapatillas Urbanas';
|
||||
const rawProdName = customName || post.extractedProduct?.name || 'Artículo Urbano';
|
||||
const prodBrand = customBrand || post.extractedProduct?.brand || 'Urbano / Variado';
|
||||
const forbiddenContext = [post.channelName, post.supplierName];
|
||||
const prodName = sanitizePublicationTitle(rawProdName, forbiddenContext, prodBrand);
|
||||
const prodCategory = customCategory || post.extractedProduct?.category || 'Zapatillas';
|
||||
const proof = getProductSocialProof({ name: prodName, brand: prodBrand });
|
||||
|
||||
const prodDescription = generateAttractiveProductCopy({
|
||||
name: prodName,
|
||||
brand: prodBrand,
|
||||
category: prodCategory,
|
||||
retailPrice,
|
||||
supplierPrice: post.extractedProduct?.supplierPrice || 75000,
|
||||
forbiddenTerms: forbiddenContext,
|
||||
@ -376,7 +384,7 @@ export async function POST(request: Request) {
|
||||
sizes: customSizes || post.extractedProduct?.sizes || ['37', '38', '39', '40', '41', '42', '43'],
|
||||
images: Array.isArray(post.rawImages) && post.rawImages.length > 0 ? post.rawImages : ['https://images.unsplash.com/photo-1595950653106-6c9ebd614d3a?auto=format&fit=crop&w=800&q=80'],
|
||||
description: prodDescription,
|
||||
category: post.extractedProduct?.category || 'Calzado / Sneakers',
|
||||
category: prodCategory,
|
||||
inStock: true,
|
||||
isActive: true,
|
||||
rating: proof.rating,
|
||||
@ -407,15 +415,17 @@ export async function POST(request: Request) {
|
||||
p.extractedProduct?.suggestedRetailPrice ||
|
||||
(p.extractedProduct?.supplierPrice ? p.extractedProduct.supplierPrice + 35000 : 125000);
|
||||
|
||||
const rawProdName = p.extractedProduct?.name || 'Zapatillas Urbanas';
|
||||
const rawProdName = p.extractedProduct?.name || 'Artículo Urbano';
|
||||
const prodBrand = p.extractedProduct?.brand || 'Urbano / Variado';
|
||||
const forbiddenContext = [p.channelName, p.supplierName];
|
||||
const prodName = sanitizePublicationTitle(rawProdName, forbiddenContext, prodBrand);
|
||||
const prodCategory = p.extractedProduct?.category || 'Zapatillas';
|
||||
const proof = getProductSocialProof({ name: prodName, brand: prodBrand });
|
||||
|
||||
const prodDescription = generateAttractiveProductCopy({
|
||||
name: prodName,
|
||||
brand: prodBrand,
|
||||
category: prodCategory,
|
||||
retailPrice,
|
||||
supplierPrice: p.extractedProduct?.supplierPrice || 75000,
|
||||
forbiddenTerms: forbiddenContext,
|
||||
@ -438,7 +448,7 @@ export async function POST(request: Request) {
|
||||
sizes: p.extractedProduct?.sizes || ['37', '38', '39', '40', '41', '42', '43'],
|
||||
images: p.rawImages || ['https://images.unsplash.com/photo-1595950653106-6c9ebd614d3a?auto=format&fit=crop&w=800&q=80'],
|
||||
description: prodDescription,
|
||||
category: p.extractedProduct?.category || 'Calzado / Sneakers',
|
||||
category: prodCategory,
|
||||
inStock: true,
|
||||
isActive: true,
|
||||
rating: proof.rating,
|
||||
|
||||
@ -79,7 +79,73 @@ export default function StoreHomePage() {
|
||||
if (activeCategory === 'flash') return p.isFlashDeal;
|
||||
if (activeCategory === 'under120') return p.retailPrice <= 120000;
|
||||
if (activeCategory !== 'all') {
|
||||
return p.brand.toLowerCase() === activeCategory.toLowerCase();
|
||||
const catLow = activeCategory.toLowerCase();
|
||||
const prodCat = (p.category || '').toLowerCase();
|
||||
const prodBrand = (p.brand || '').toLowerCase();
|
||||
const prodName = (p.name || '').toLowerCase();
|
||||
|
||||
if (catLow === 'zapatillas') {
|
||||
return (
|
||||
prodCat.includes('zapatilla') ||
|
||||
(!prodCat.includes('chancla') &&
|
||||
!prodCat.includes('slide') &&
|
||||
!prodCat.includes('bolso') &&
|
||||
!prodCat.includes('malet') &&
|
||||
!prodCat.includes('morral') &&
|
||||
!prodCat.includes('carriel') &&
|
||||
!prodCat.includes('gorra') &&
|
||||
!prodName.includes('chancla') &&
|
||||
!prodName.includes('morral') &&
|
||||
!prodName.includes('carriel'))
|
||||
);
|
||||
}
|
||||
if (catLow === 'chanclas') {
|
||||
return (
|
||||
prodCat.includes('chancla') ||
|
||||
prodCat.includes('slide') ||
|
||||
prodCat.includes('sandalia') ||
|
||||
prodCat.includes('croc') ||
|
||||
prodName.includes('chancla') ||
|
||||
prodName.includes('slide') ||
|
||||
prodName.includes('croc')
|
||||
);
|
||||
}
|
||||
if (catLow === 'bolsos') {
|
||||
return (
|
||||
prodCat.includes('bolso') ||
|
||||
prodCat.includes('cartera') ||
|
||||
prodName.includes('bolso') ||
|
||||
prodName.includes('tote')
|
||||
);
|
||||
}
|
||||
if (catLow === 'maletines') {
|
||||
return (
|
||||
prodCat.includes('malet') ||
|
||||
prodCat.includes('morral') ||
|
||||
prodCat.includes('mochila') ||
|
||||
prodName.includes('morral') ||
|
||||
prodName.includes('mochila') ||
|
||||
prodName.includes('malet')
|
||||
);
|
||||
}
|
||||
if (catLow === 'carrieles') {
|
||||
return (
|
||||
prodCat.includes('carriel') ||
|
||||
prodCat.includes('bandolera') ||
|
||||
prodCat.includes('riñonera') ||
|
||||
prodCat.includes('rinonera') ||
|
||||
prodCat.includes('crossbody') ||
|
||||
prodName.includes('carriel') ||
|
||||
prodName.includes('crossbody') ||
|
||||
prodName.includes('bandolera')
|
||||
);
|
||||
}
|
||||
if (catLow === 'gorras') {
|
||||
return prodCat.includes('gorra') || prodCat.includes('cap') || prodName.includes('gorra');
|
||||
}
|
||||
|
||||
// Por marca directa (Nike, Adidas, etc.) o coincidencia exacta
|
||||
return prodBrand === catLow || prodCat === catLow;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
@ -9,12 +9,16 @@ interface CategoryPillsProps {
|
||||
}
|
||||
|
||||
const CATEGORIES = [
|
||||
{ id: 'all', label: '🔥 Todas las Zapatillas' },
|
||||
{ id: 'Nike', label: '👟 Nike' },
|
||||
{ id: 'Jordan', label: '🏀 Air Jordan' },
|
||||
{ id: 'all', label: '🔥 Todo el Catálogo' },
|
||||
{ id: 'Zapatillas', label: '👟 Zapatillas' },
|
||||
{ id: 'Chanclas', label: '🩴 Chanclas & Slides' },
|
||||
{ id: 'Bolsos', label: '👜 Bolsos' },
|
||||
{ id: 'Maletines', label: '🎒 Maletines & Morrales' },
|
||||
{ id: 'Carrieles', label: '👝 Carrieles & Bandoleras' },
|
||||
{ id: 'Gorras', label: '🧢 Gorras' },
|
||||
{ id: 'Nike', label: '⚡ Nike' },
|
||||
{ id: 'Adidas', label: '🛹 Adidas' },
|
||||
{ id: 'New Balance', label: '🌿 New Balance' },
|
||||
{ id: 'Puma', label: '🐆 Puma' },
|
||||
{ id: 'Jordan', label: '🏀 Jordan' },
|
||||
{ id: 'flash', label: '⚡ Ofertas Flash' },
|
||||
{ id: 'under120', label: '💰 Menos de $120k' },
|
||||
];
|
||||
|
||||
@ -17,33 +17,67 @@ export function generateAttractiveProductCopy(product: {
|
||||
retailPrice?: number;
|
||||
forbiddenTerms?: (string | undefined | null)[];
|
||||
}): string {
|
||||
const brand = product.brand || 'Sneakers';
|
||||
const brand = product.brand || 'Colección Urbana';
|
||||
const cleanName = sanitizePublicationTitle(
|
||||
product.name || 'Zapatillas Exclusivas',
|
||||
product.name || 'Artículo Exclusivo',
|
||||
product.forbiddenTerms,
|
||||
brand
|
||||
);
|
||||
const cat = (product.category || '').toLowerCase();
|
||||
|
||||
const hooks = [
|
||||
`🔥 ¡Lleva tu outfit al siguiente nivel con las icónicas ${cleanName}! Diseñadas para quienes no negocian entre estilo de alto impacto y máxima comodidad para el día a día.`,
|
||||
`⚡ Las ${cleanName} son el drop definitivo que tu colección estaba esperando. Silueta imbatible, acabados prémium y una estética urbana que no pasa desapercibida.`,
|
||||
`💎 Edición Prémium ${brand}: ${cleanName}. Fabricadas con materiales de máxima durabilidad, textura prémium y una combinación de colores pensada para resaltar en cualquier ocasión.`,
|
||||
`🚀 Dale un upgrade a tus pasos con las exclusivas ${cleanName}. Comodidad extrema desde el primer uso, amortiguación reactiva y estilo 100% garantizado.`,
|
||||
];
|
||||
let hook = '';
|
||||
let bullet1 = '• 💎 *Calidad Prémium Garantizada:* Materiales de alto rendimiento, costuras reforzadas y acabados de lujo.';
|
||||
let bullet2 = '• ☁️ *Máximo Confort y Ergonomía:* Diseñado para acompañarte todo el día con total comodidad.';
|
||||
let cta = '⚠️ *Disponibilidad Limitada:* Pocas unidades disponibles en inventario nacional. ¡Asegura el tuyo ahora antes de que se agoten!';
|
||||
|
||||
const seed = cleanName.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0);
|
||||
const selectedHook = hooks[Math.abs(seed) % hooks.length];
|
||||
if (cat.includes('chancla') || cat.includes('slide') || cat.includes('sandalia')) {
|
||||
hook = `🔥 ¡Comodidad insuperable con las nuevas ${cleanName}! Diseñadas para descanso, piscina o estilo urbano relajado con materiales ultra livianos y amortiguación suave.`;
|
||||
bullet1 = '• 💎 *Material EVA / Suave de Alta Densidad:* Espuma ergonómica que no se deforma y absorbe el impacto al caminar.';
|
||||
bullet2 = '• 🌊 *Resistentes al Agua y Antideslizantes:* Suela con agarre óptimo para máxima seguridad y secado rápido.';
|
||||
cta = '⚠️ *Disponibilidad Limitada:* Pocas unidades disponibles por talla. ¡Pide tus chanclas hoy mismo!';
|
||||
} else if (cat.includes('carriel') || cat.includes('bandolera') || cat.includes('riñonera') || cat.includes('crossbody')) {
|
||||
hook = `🔥 ¡Lleva tus esenciales con el auténtico estilo y practicidad del ${cleanName}! Múltiples compartimientos, cierres de alta durabilidad y correa ajustable ergonómica.`;
|
||||
bullet1 = '• 💎 *Materiales de Alta Resistencia:* Acabados prémium, costuras de seguridad y cierres metálicos fluidos.';
|
||||
bullet2 = '• 👝 *Distribución Inteligente:* Espacio seguro para celular, billetera, llaves y documentos con fácil acceso.';
|
||||
cta = '⚠️ *Edición Exclusiva:* Pocas unidades en bodega. ¡Haz tu pedido y paga al recibir en tu casa!';
|
||||
} else if (cat.includes('malet') || cat.includes('morral') || cat.includes('mochila') || cat.includes('tula')) {
|
||||
hook = `🚀 ¡Organiza tu día con la máxima capacidad y resistencia del ${cleanName}! Ideal para estudio, trabajo, gimnasio o viajes de fin de semana.`;
|
||||
bullet1 = '• 💎 *Compartimiento Acolchado para Laptop:* Protección antichoque para tus dispositivos y pertenencias de valor.';
|
||||
bullet2 = '• 🎒 *Cargaderas Acolchadas Antitranspirantes:* Distribución ergonómica del peso para proteger tu espalda.';
|
||||
cta = '⚠️ *Alta Demanda:* Pocas unidades disponibles para despacho inmediato nacional.';
|
||||
} else if (cat.includes('bolso') || cat.includes('cartera')) {
|
||||
hook = `✨ ¡Elegancia, distinción y versatilidad con el ${cleanName}! El toque definitivo para complementar tu outfit en cualquier momento del día.`;
|
||||
bullet1 = '• 💎 *Textura Prémium y Detalles Finos:* Herrajes dorados/plateados de alta gama y forro interior reforzado.';
|
||||
bullet2 = '• 👜 *Espacio Ideal y Versátil:* Capacidad perfecta para tu día a día manteniendo una silueta estilizada.';
|
||||
cta = '⚠️ *Colección Limitada:* Stock muy reducido en Colombia. ¡Asegura tu bolso favorito antes de que se agote!';
|
||||
} else if (cat.includes('gorra') || cat.includes('cap')) {
|
||||
hook = `🧢 ¡Completa tu look con la exclusiva ${cleanName}! Silueta estructurada, visera impecable y bordados en alta definición.`;
|
||||
bullet1 = '• 💎 *Bordado 3D de Alta Densidad:* Acabados milimétricos y corona estructurada que no pierde su forma.';
|
||||
bullet2 = '• 🔒 *Ajuste Cómodo y Seguro:* Correa regulable para adaptarse perfectamente a cualquier contorno.';
|
||||
cta = '⚠️ *Drop Exclusivo:* Pocas piezas en inventario. ¡Pídela ahora con envío a toda Colombia!';
|
||||
} else if (cat.includes('ropa') || cat.includes('textil') || cat.includes('camiseta') || cat.includes('hoodie')) {
|
||||
hook = `🔥 ¡Estilo urbano auténtico con la prenda ${cleanName}! Silueta moderna, textura suave al tacto y máxima frescura.`;
|
||||
bullet1 = '• 💎 *Algodón Prémium / Tela Pesada:* No destiñe ni encoge tras las lavadas, manteniendo caída y color.';
|
||||
bullet2 = '• 👕 *Corte Streetwear Confort:* Ajuste ideal para lucir impecable en cualquier ocasión.';
|
||||
cta = '⚠️ *Tallas Limitadas:* Disponible para entrega inmediata a nivel nacional.';
|
||||
} else {
|
||||
// Por defecto: Zapatillas / Sneakers
|
||||
hook = `🔥 ¡Lleva tu outfit al siguiente nivel con las icónicas ${cleanName}! Diseñadas para quienes no negocian entre estilo de alto impacto y máxima comodidad para el día a día.`;
|
||||
bullet1 = '• 💎 *Calidad G5 / 1.1 Prémium:* Materiales de alto rendimiento, costuras reforzadas y terminaciones impecables.';
|
||||
bullet2 = '• ☁️ *Confort Inigualable:* Plantilla acolchada ergonómica para máxima suavidad al caminar o entrenar.';
|
||||
cta = '⚠️ *Disponibilidad Limitada:* Pocas unidades disponibles por talla en inventario nacional. ¡Asegura tu par ahora antes de que se agoten!';
|
||||
}
|
||||
|
||||
const rawCopy = `${selectedHook}
|
||||
const rawCopy = `${hook}
|
||||
|
||||
✨ *¿Por qué todos las eligen?*
|
||||
• 💎 *Calidad G5 / 1.1 Prémium:* Materiales de alto rendimiento, costuras reforzadas y terminaciones impecables.
|
||||
• ☁️ *Confort Inigualable:* Plantilla acolchada ergonómica para máxima suavidad al caminar o entrenar.
|
||||
✨ *¿Por qué todos lo eligen?*
|
||||
${bullet1}
|
||||
${bullet2}
|
||||
• 🚚 *Despacho Prioritario Nacional:* Despachamos en 24 horas hábiles a cualquier ciudad de Colombia.
|
||||
• 💵 *Pago 100% Contraentrega:* Realizas tu pedido con total tranquilidad y pagas al recibir en tu puerta.
|
||||
• 🛡️ *Garantía Directa:* Revisas tu producto y verificas tu talla con total respaldo.
|
||||
• 🛡️ *Garantía Directa:* Revisas tu producto al llegar con total respaldo.
|
||||
|
||||
⚠️ *Disponibilidad Limitada:* Pocas unidades disponibles por talla en inventario nacional. ¡Asegura tu par ahora antes de que se agoten!`;
|
||||
${cta}`;
|
||||
|
||||
return sanitizePublicationText(rawCopy, product.forbiddenTerms, product.supplierPrice);
|
||||
}
|
||||
|
||||
@ -428,9 +428,124 @@ export function extractSupplierPrice(text: string): number {
|
||||
}
|
||||
|
||||
/**
|
||||
* Extractor de tallas con normalización Unicode y rangos
|
||||
* Detección y Categorización Inteligente Multi-categoría
|
||||
* Identifica con precisión: Zapatillas, Chanclas, Bolsos, Maletines, Carrieles, Gorras, Ropa, Accesorios
|
||||
*/
|
||||
export function extractSizes(rawText: string): string[] {
|
||||
export interface CategoryDetection {
|
||||
category: string;
|
||||
categoryKey: 'zapatillas' | 'chanclas' | 'bolsos' | 'maletines' | 'carrieles' | 'gorras' | 'ropa' | 'accesorios';
|
||||
icon: string;
|
||||
confidence: number;
|
||||
}
|
||||
|
||||
export const OFFICIAL_CATEGORIES: { id: CategoryDetection['categoryKey']; label: string; icon: string }[] = [
|
||||
{ id: 'zapatillas', label: 'Zapatillas', icon: '👟' },
|
||||
{ id: 'chanclas', label: 'Chanclas / Sandalias', icon: '🩴' },
|
||||
{ id: 'bolsos', label: 'Bolsos / Carteras', icon: '👜' },
|
||||
{ id: 'maletines', label: 'Maletines / Morrales', icon: '🎒' },
|
||||
{ id: 'carrieles', label: 'Carrieles / Bandoleras', icon: '👝' },
|
||||
{ id: 'gorras', label: 'Gorras / Caps', icon: '🧢' },
|
||||
{ id: 'ropa', label: 'Ropa / Streetwear', icon: '👕' },
|
||||
{ id: 'accesorios', label: 'Accesorios', icon: '🕶️' },
|
||||
];
|
||||
|
||||
export function detectCategory(text: string, title?: string): CategoryDetection {
|
||||
const full = `${title || ''} ${text || ''}`.toLowerCase();
|
||||
|
||||
// 1. Chanclas / Sandalias / Slides / Crocs
|
||||
if (
|
||||
/\b(chancl[as]|sandali[as]|slide[s]?|croc[s]?|pantufl[as]|ojot[as]|adilette|chola[s]|chinela[s]|huarache[s]|alparagatas?|flip[\s-]*flops?|chancleta[s]?|chancletero)\b/i.test(
|
||||
full
|
||||
)
|
||||
) {
|
||||
return { category: 'Chanclas', categoryKey: 'chanclas', icon: '🩴', confidence: 0.98 };
|
||||
}
|
||||
|
||||
// 2. Carrieles / Bandoleras / Riñoneras / Crossbody / Pecheras / Canguros
|
||||
if (
|
||||
/\b(carriel(es)?|bandolera[s]?|ri[ñn]onera[s]?|pechera[s]?|crossbody|waist[\s-]*bag|fanny[\s-]*pack|chest[\s-]*bag|canguro[s]?|mariconera[s]?|shoulder[\s-]*bag|koala[s]?|manbag)\b/i.test(
|
||||
full
|
||||
)
|
||||
) {
|
||||
return { category: 'Carrieles', categoryKey: 'carrieles', icon: '👝', confidence: 0.98 };
|
||||
}
|
||||
|
||||
// 3. Maletines / Morrales / Mochilas / Backpacks / Tulas
|
||||
if (
|
||||
/\b(malet[ií]n(es)?|morral(es)?|mochila[s]?|backpack[s]?|tula[s]?|maleta[s]?|bolso\s*deportivo|duffle|gym[\s-]*bag|equipaje)\b/i.test(
|
||||
full
|
||||
)
|
||||
) {
|
||||
return { category: 'Maletines', categoryKey: 'maletines', icon: '🎒', confidence: 0.98 };
|
||||
}
|
||||
|
||||
// 4. Bolsos / Carteras / Tote
|
||||
if (
|
||||
/\b(bolso[s]?|cartera[s]?|tote([\s-]*bag)?|clutch|satchel|baguette|monedero[s]?|shopping[\s-]*bag|handbag[s]?|bolsa[s]?\s*de\s*mano)\b/i.test(
|
||||
full
|
||||
)
|
||||
) {
|
||||
return { category: 'Bolsos', categoryKey: 'bolsos', icon: '👜', confidence: 0.97 };
|
||||
}
|
||||
|
||||
// 5. Gorras / Sombreros / Accesorios para la cabeza
|
||||
if (
|
||||
/\b(gorra[s]?|gorro[s]?|cap[s]?|snapback|bucket[\s-]*hat|beanie|sombrero[s]?|visera[s]?)\b/i.test(
|
||||
full
|
||||
)
|
||||
) {
|
||||
return { category: 'Gorras', categoryKey: 'gorras', icon: '🧢', confidence: 0.98 };
|
||||
}
|
||||
|
||||
// 6. Ropa / Textil
|
||||
if (
|
||||
/\b(camiset[as]|camibuso[s]?|buzo[s]?|hoodie[s]?|chaqueta[s]?|sudadera[s]?|pantalon(es)?|short[s]?|jogger[s]?|polo[s]?|conjunto[s]?|bermuda[s]?|overol[es]?)\b/i.test(
|
||||
full
|
||||
)
|
||||
) {
|
||||
return { category: 'Ropa', categoryKey: 'ropa', icon: '👕', confidence: 0.97 };
|
||||
}
|
||||
|
||||
// 7. Accesorios varios
|
||||
if (
|
||||
/\b(billetera[s]?|cintur[oó]n(es)?|correa[s]?|reloj(es)?|gafa[s]?|medias?|calcet[ií]n(es)?)\b/i.test(
|
||||
full
|
||||
)
|
||||
) {
|
||||
return { category: 'Accesorios', categoryKey: 'accesorios', icon: '🕶️', confidence: 0.95 };
|
||||
}
|
||||
|
||||
// 8. Por defecto: Zapatillas
|
||||
return { category: 'Zapatillas', categoryKey: 'zapatillas', icon: '👟', confidence: 0.92 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Extractor de tallas con normalización Unicode y distinción por categoría
|
||||
*/
|
||||
export function extractSizes(rawText: string, category?: string): string[] {
|
||||
const normCat = (category || '').toLowerCase();
|
||||
const isAccessory =
|
||||
normCat.includes('bolso') ||
|
||||
normCat.includes('malet') ||
|
||||
normCat.includes('carriel') ||
|
||||
normCat.includes('gorra') ||
|
||||
normCat.includes('accesorio');
|
||||
|
||||
// Si es un carriel, bolso, maletín o gorra, buscar si dice Talla Única o estándar
|
||||
if (isAccessory) {
|
||||
if (/\b(talla\s*[\u00fa]nica|talla\s*unica|unitalla|ajustable|est[aá]ndar|standard)\b/i.test(rawText)) {
|
||||
return ['Talla Única'];
|
||||
}
|
||||
}
|
||||
|
||||
// Si es Ropa, buscar tallas S, M, L, XL, XXL
|
||||
if (normCat.includes('ropa')) {
|
||||
const textSizes = rawText.match(/\b(xs|s|m|l|xl|xxl|2xl|3xl)\b/gi);
|
||||
if (textSizes && textSizes.length > 0) {
|
||||
return Array.from(new Set(textSizes.map((s) => s.toUpperCase())));
|
||||
}
|
||||
}
|
||||
|
||||
const normalized = rawText
|
||||
.normalize('NFKD')
|
||||
.replace(/[ᴛ]/g, 't')
|
||||
@ -473,6 +588,12 @@ export function extractSizes(rawText: string): string[] {
|
||||
}
|
||||
|
||||
if (sizes.length === 0) {
|
||||
if (isAccessory) {
|
||||
return ['Talla Única'];
|
||||
}
|
||||
if (normCat.includes('ropa')) {
|
||||
return ['S', 'M', 'L', 'XL'];
|
||||
}
|
||||
return ['37', '38', '39', '40', '41', '42', '43'];
|
||||
}
|
||||
|
||||
@ -491,7 +612,6 @@ export function cleanProductName(
|
||||
const lines = text.split('\n').map((l) => l.trim()).filter((l) => l.length > 0);
|
||||
for (const line of lines) {
|
||||
const lower = line.toLowerCase();
|
||||
// Omitir líneas completas que son encabezados o nombres de proveedores/grupos
|
||||
if (
|
||||
lower.includes('difusion jovic') ||
|
||||
lower.includes('jovic') ||
|
||||
@ -529,11 +649,28 @@ export function cleanProductName(
|
||||
}
|
||||
}
|
||||
|
||||
return sanitizePublicationTitle(
|
||||
detectedBrand !== 'Urbano / Variado' ? `${detectedBrand} Urban Edition` : 'Zapatilla Urbana Nacional',
|
||||
extraForbidden,
|
||||
detectedBrand
|
||||
);
|
||||
const catDetect = detectCategory(text);
|
||||
let defaultGeneric = `${detectedBrand} Urban Edition`;
|
||||
if (detectedBrand === 'Urbano / Variado') {
|
||||
if (catDetect.category === 'Chanclas') defaultGeneric = 'Sandalia / Slide Confort';
|
||||
else if (catDetect.category === 'Carrieles') defaultGeneric = 'Carriel / Bandolera Urbana';
|
||||
else if (catDetect.category === 'Maletines') defaultGeneric = 'Morral / Maletín Deportivo';
|
||||
else if (catDetect.category === 'Bolsos') defaultGeneric = 'Bolso Elegante Casual';
|
||||
else if (catDetect.category === 'Gorras') defaultGeneric = 'Gorra Urbana Clásica';
|
||||
else if (catDetect.category === 'Ropa') defaultGeneric = 'Prenda Streetwear Urbana';
|
||||
else if (catDetect.category === 'Accesorios') defaultGeneric = 'Accesorio Urbano Prémium';
|
||||
else defaultGeneric = 'Zapatilla Urbana Nacional';
|
||||
} else {
|
||||
if (catDetect.category === 'Chanclas') defaultGeneric = `${detectedBrand} Slide Confort`;
|
||||
else if (catDetect.category === 'Carrieles') defaultGeneric = `${detectedBrand} Carriel / Bandolera`;
|
||||
else if (catDetect.category === 'Maletines') defaultGeneric = `${detectedBrand} Morral Deportivo`;
|
||||
else if (catDetect.category === 'Bolsos') defaultGeneric = `${detectedBrand} Bolso Casual`;
|
||||
else if (catDetect.category === 'Gorras') defaultGeneric = `${detectedBrand} Gorra Clásica`;
|
||||
else if (catDetect.category === 'Ropa') defaultGeneric = `${detectedBrand} Colección Streetwear`;
|
||||
else defaultGeneric = `${detectedBrand} Urban Edition`;
|
||||
}
|
||||
|
||||
return sanitizePublicationTitle(defaultGeneric, extraForbidden, detectedBrand);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -542,7 +679,6 @@ export function cleanProductName(
|
||||
function extractBase64Payload(imageInput?: string): { mimeType: string; data: string } | null {
|
||||
if (!imageInput || typeof imageInput !== 'string') return null;
|
||||
|
||||
// Formato data:image/jpeg;base64,...
|
||||
const dataUriMatch = imageInput.match(/^data:([^;]+);base64,(.+)$/);
|
||||
if (dataUriMatch) {
|
||||
return {
|
||||
@ -551,7 +687,6 @@ function extractBase64Payload(imageInput?: string): { mimeType: string; data: st
|
||||
};
|
||||
}
|
||||
|
||||
// Base64 puro de más de 100 caracteres sin espacios ni URLs
|
||||
if (imageInput.length > 100 && !imageInput.startsWith('http') && !imageInput.includes(' ')) {
|
||||
return {
|
||||
mimeType: 'image/jpeg',
|
||||
@ -563,12 +698,11 @@ function extractBase64Payload(imageInput?: string): { mimeType: string; data: st
|
||||
}
|
||||
|
||||
/**
|
||||
* Parser Inteligente de Mensajes y Visión Multimodal con Gemini
|
||||
* Reconoce logos en la foto (Swoosh, 3 Rayas, Trefoil, N de New Balance, On Clouds, Formstrip de Puma, etc.)
|
||||
* y extrae nombre comercial, marca exacta, tallas y precio mayorista.
|
||||
* Parser Inteligente de Mensajes y Visión Multimodal con Gemini y Categorización Automática
|
||||
* Reconoce zapatillas, chanclas, bolsos, maletines, carrieles, gorras, ropa y accesorios.
|
||||
*
|
||||
* REGLA ESTRICTA: NI en el título NI en el texto de la publicación se debe incluir el nombre
|
||||
* del grupo de WhatsApp ni el proveedor del producto.
|
||||
* del grupo de WhatsApp ni el proveedor del producto ni el costo.
|
||||
*/
|
||||
export async function parseSupplierMessage(
|
||||
text: string,
|
||||
@ -576,11 +710,12 @@ export async function parseSupplierMessage(
|
||||
extraForbidden: (string | undefined | null)[] = []
|
||||
): Promise<ExtractedProductData> {
|
||||
const apiKey = process.env.GEMINI_API_KEY;
|
||||
const categoryDetection = detectCategory(text);
|
||||
|
||||
// Fallback determinístico con catálogo maestro de marcas
|
||||
// Fallback determinístico con catálogo maestro de marcas y categorías
|
||||
const fallbackExtract = (): ExtractedProductData => {
|
||||
const price = extractSupplierPrice(text);
|
||||
const sizes = extractSizes(text);
|
||||
const sizes = extractSizes(text, categoryDetection.category);
|
||||
const brandDetection = detectBrandFromText(text);
|
||||
const brand = brandDetection.brand;
|
||||
const rawClean = brandDetection.modelName || cleanProductName(text, brand, extraForbidden);
|
||||
@ -593,7 +728,7 @@ export async function parseSupplierMessage(
|
||||
sizes,
|
||||
supplierPrice: price,
|
||||
suggestedRetailPrice: calculateRetailPrice(price, 35, 30000),
|
||||
category: 'Calzado / Sneakers',
|
||||
category: categoryDetection.category,
|
||||
confidence: brandDetection.confidence,
|
||||
rating: proof.rating,
|
||||
reviewsCount: proof.reviewsCount,
|
||||
@ -615,47 +750,47 @@ export async function parseSupplierMessage(
|
||||
? `Términos específicos PROHIBIDOS a omitir absolutamente: "${forbiddenTermsStr}".`
|
||||
: '';
|
||||
|
||||
const systemPrompt = `Eres un experto mundial en reconocimiento de calzado deportivo y zapatillas urbanas para e-commerce en Colombia.
|
||||
Tu misión es identificar con MÁXIMA PRECISIÓN la marca y el modelo exacto del calzado.
|
||||
const systemPrompt = `Eres un experto mundial en reconocimiento y categorización de calzado, marroquinería y artículos urbanos para e-commerce en Colombia.
|
||||
Tu misión es identificar con MÁXIMA PRECISIÓN la marca, el modelo exacto y la CATEGORÍA del producto.
|
||||
|
||||
CATEGORÍAS OFICIALES OBLIGATORIAS:
|
||||
Debes clasificar el producto en EXACTAMENTE una de las siguientes categorías en el campo "category":
|
||||
- "Zapatillas": Zapatillas deportivas, tenis urbanos, sneakers, calzado deportivo o casual.
|
||||
- "Chanclas": Chanclas, sandalias, slides, crocs, zuecos, pantuflas, ojotas.
|
||||
- "Bolsos": Bolsos para dama, carteras, tote bags, carteras de mano o de hombro.
|
||||
- "Maletines": Maletines ejecutivos, mochilas, morrales, backpacks, tulas deportivas.
|
||||
- "Carrieles": Carrieles tradicionales o modernos, bandoleras, riñoneras, pecheras, canguros, crossbody.
|
||||
- "Gorras": Gorras, gorros, caps, sombreros.
|
||||
- "Ropa": Camisetas, buzos, hoodies, chaquetas, sudaderas, shorts, pantalones.
|
||||
- "Accesorios": Billeteras, cinturones, gafas, relojes.
|
||||
|
||||
⚠️ REGLA CRÍTICA DE CERO PROVEEDORES / CERO GRUPOS / CERO COSTOS (ESTRICTO):
|
||||
- NI EN EL TÍTULO ("name") NI EN NINGÚN TEXTO COMERCIAL DE LA PUBLICACIÓN SE DEBE INCLUIR EL NOMBRE DEL GRUPO DE WHATSAPP, CANAL, PROVEEDOR, BODEGA MAYORISTA, DISTRIBUIDOR, NI EL COSTO DEL PRODUCTO (PRECIO DE COMPRA / MAYORISTA).
|
||||
- El costo del producto SOLO debe extraerse en el campo numérico interno "supplierPrice" para el cálculo de margen, pero JAMÁS debe figurar en el título ("name") ni en ninguna descripción de la publicación.
|
||||
- El campo "name" debe ser ÚNICAMENTE el modelo limpio y comercial del calzado (ej: "Adidas Samba OG Blanco Negro", "Nike Air Force 1 07 All White", "New Balance 9060 Sea Salt", "On Cloudmonster Triple Black", "Puma Palermo Special Verde").
|
||||
- El campo "name" debe ser ÚNICAMENTE el modelo limpio y comercial del producto (ej: "Adidas Samba OG Blanco Negro", "Nike Air Force 1 07 All White", "Yeezy Slide Onyx", "Carriel de Cuero Clásico Jericó", "Morral Deportivo Nike Brasilia", "Bolso Tote Monograma", "Gorra New Era NY Yankees").
|
||||
- Si el mensaje contiene nombres de grupos o proveedores (${forbiddenClause} o palabras como "Bodega", "Mayorista", "Distribuidora", "Proveedor", "Jovic", "CC Mío", "Team Emporio", etc.), o menciones de costo/precios de bodega, DEBES OMITIRLOS Y ELIMINARLOS POR COMPLETO DEL TÍTULO Y DE TODO TEXTO.
|
||||
- Si la zapatilla no tiene modelo exacto identificable, asigna un nombre comercial elegante (ej: "Sneakers Urban Edition"), pero JAMÁS inventes ni coloques el nombre de la bodega, del grupo ni el costo del producto.
|
||||
|
||||
${
|
||||
imagePayload
|
||||
? `🔴 INSTRUCCIÓN DE VISIÓN CRÍTICA:
|
||||
Observa con atención la IMAGEN del calzado y detecta los logos visuales de la marca:
|
||||
- ADIDAS: Las 3 Rayas diagonales laterales, logo del Trébol (Trefoil) o texto "adidas".
|
||||
- NIKE: Logo del Swoosh (chulo), logo Air Max, logo TN o inscripción "NIKE".
|
||||
- JORDAN: Logo del Jumpman (jugador saltando), logo Wings o siluetas Retro (Jordan 4, Jordan 1, etc.).
|
||||
- NEW BALANCE: La letra 'N' grande en el lateral, texto 'NB' o suela chunky con cortes característicos (como New Balance 9060, 550, 530, 1906).
|
||||
- ON RUNNING: Logo circular On de nubes, suela con orificios/módulos Cloudtec (Cloudmonster, Cloudtilt, Cloud 5).
|
||||
- PUMA: Línea curvada lateral "Formstrip", felino saltando o modelos planos retro (Palermo, Speedcat, Suede).
|
||||
- ASICS: Rayas cruzadas de tigre (Tiger Stripes) en el lateral, detalles en gel plateado/metalizado (Gel-Kayano 14, Gel-NYC).
|
||||
- VANS: La línea de ola Jazz Stripe lateral, suela waffle de goma o estilo chunky acolchado (Knu Skool, Old Skool).
|
||||
- CONVERSE: Parche redondo de estrella All Star o logo Star Chevron en el lateral.
|
||||
- SALOMON: Logo 'S' o sistema de cordones rápidos Quicklace (XT-6, Speedcross).
|
||||
- HOKA: Suela ultragruesa maximalista, logo de pájaro estilizado o texto Hoka.
|
||||
- BALENCIAGA: Silueta de capas futuristas o suela triple (Track, Triple S, 3XL).
|
||||
- ALEXANDER MCQUEEN: Silueta de cuero blanco minimalista con suela de plataforma gruesa.
|
||||
- GOLDEN GOOSE: Estrella lateral vintage envejecida/gastada.
|
||||
- CROCS: Zueco de espuma con agujeros o versiones Pollex / Echo.
|
||||
- DIESEL / OAKLEY / LACOSTE / TIMBERLAND / FILA / REEBOK / BAPE / LOUIS VUITTON / DIOR / GUCCI / PRADA: Identifica sus logos distintivos.
|
||||
Observa con atención la IMAGEN del producto y detecta los logos visuales de la marca y el tipo de artículo:
|
||||
- CALZADO / ZAPATILLAS: Zapatillas deportivas, sneakers (Nike Swoosh, Adidas 3 Rayas, Jordan Jumpman, NB, Puma, etc.).
|
||||
- CHANCLAS / SANDALIAS: Chanclas abiertas, slides de una pieza, zuecos Crocs, Yeezy slides.
|
||||
- CARRIELES / BANDOLERAS: Bolsos pequeños cruzados al pecho o cintura (crossbody, riñonera, carriel de cuero).
|
||||
- MALETINES / MORRALES: Mochilas de dos cargaderas para espalda, maletines para portátil o tulas.
|
||||
- BOLSOS: Bolsos de mano o de hombro para dama o unisex.
|
||||
- GORRAS: Gorras con visera plana o curva, sombreros.
|
||||
`
|
||||
: `Analiza el texto y las palabras clave del mensaje mayorista para identificar la marca y el modelo exacto.`
|
||||
: `Analiza el texto y las palabras clave del mensaje mayorista para identificar la categoría, la marca y el modelo exacto.`
|
||||
}
|
||||
|
||||
Extrae los siguientes campos en formato JSON estricto:
|
||||
- "brand": La marca del calzado ("Adidas", "Nike", "Jordan", "New Balance", "On Running", "Puma", "Asics", "Vans", "Converse", "Salomon", "Hoka", "Balenciaga", "Alexander McQueen", "Golden Goose", "Crocs", "Under Armour", "Lacoste", "Timberland", "Diesel", "Oakley", "Fila", "Reebok", "Bape", "Louis Vuitton", "Dior", "Gucci", "Prada", "Autry", "Veja"). Si no puedes identificar ninguna marca con certeza, responde "Urbano / Variado". ¡NUNCA COLOQUES NIKE POR DEFECTO SI EL LOGO O EL MODELO NO ES NIKE!
|
||||
- "name": Nombre comercial completo y limpio del modelo (ej: "Adidas Samba OG Blanco Negro", "New Balance 9060 Sea Salt", "On Cloudmonster All Black", "Puma Palermo Special Verde", "Nike Dunk Low Retro Panda", "Jordan 4 Retro Military Black", "Asics Gel-Kayano 14 Silver", "Vans Knu Skool Black White", "Salomon XT-6 Gore-Tex"). ¡CERO NOMBRES DE GRUPOS O PROVEEDORES!
|
||||
- "colorName": Nombre descriptivo del color o colorway (ej: "Blanco / Negro (Panda)", "Sea Salt / Gris", "Triple Black", "Verde / Goma", "Military Black").
|
||||
- "sizes": Array de números de tallas disponibles (ej: ["37", "38", "39", "40", "41", "42", "43"]).
|
||||
- "supplierPrice": Precio de compra mayorista como número entero en COP (ej: 75000, 80000, 85000, 110000). Analiza símbolos como 💸, 💰, $, 75k, 85.000, etc.
|
||||
- "category": Categoría ("Sneakers / Colección", "Urbano / Casual", "Running / Deportivo", "Outdoor / Trail").
|
||||
- "brand": La marca ("Adidas", "Nike", "Jordan", "New Balance", "On Running", "Puma", "Asics", "Vans", "Converse", "Crocs", "Tommy Hilfiger", "Lacoste", "Gucci", "Louis Vuitton", "Totto", "Vélez", etc.). Si no puedes identificar ninguna marca con certeza, responde "Urbano / Variado".
|
||||
- "name": Nombre comercial completo y limpio del modelo. ¡CERO NOMBRES DE GRUPOS O PROVEEDORES!
|
||||
- "category": Exactamente una de: "Zapatillas" | "Chanclas" | "Bolsos" | "Maletines" | "Carrieles" | "Gorras" | "Ropa" | "Accesorios".
|
||||
- "colorName": Nombre descriptivo del color o colorway.
|
||||
- "sizes": Array de tallas disponibles (para calzado números ej: ["37","38"], para accesorios ["Talla Única"], para ropa ["S","M","L"]).
|
||||
- "supplierPrice": Precio de compra mayorista como número entero en COP. Analiza símbolos como 💸, 💰, $, 75k, 85.000, etc.
|
||||
|
||||
Texto del mensaje de WhatsApp:
|
||||
"""
|
||||
@ -695,17 +830,21 @@ Responde ÚNICAMENTE con el objeto JSON sin bloques de código markdown ni texto
|
||||
const cleanJson = outputText.replace(/```json|```/gi, '').trim();
|
||||
const parsed = JSON.parse(cleanJson);
|
||||
|
||||
// Validación y categorización unificada
|
||||
const catDetection = detectCategory(`${parsed.name || ''} ${text}`, parsed.category);
|
||||
const finalCategory = catDetection.category;
|
||||
|
||||
// Validación y saneamiento del precio
|
||||
const supplierPrice =
|
||||
typeof parsed.supplierPrice === 'number' && parsed.supplierPrice >= 40000
|
||||
typeof parsed.supplierPrice === 'number' && parsed.supplierPrice >= 20000
|
||||
? parsed.supplierPrice
|
||||
: extractSupplierPrice(text);
|
||||
|
||||
// Validación de tallas
|
||||
// Validación de tallas dependiente de categoría
|
||||
const sizes =
|
||||
Array.isArray(parsed.sizes) && parsed.sizes.length > 0
|
||||
? parsed.sizes.map(String)
|
||||
: extractSizes(text);
|
||||
: extractSizes(text, finalCategory);
|
||||
|
||||
// Validación de marca: Si la IA no devolvió marca o vino vacía, usamos el detector maestro
|
||||
let brand = parsed.brand?.trim();
|
||||
@ -714,7 +853,6 @@ Responde ÚNICAMENTE con el objeto JSON sin bloques de código markdown ni texto
|
||||
brand = fallbackBrand.brand;
|
||||
}
|
||||
|
||||
// Doble verificación: si el nombre dice explícitamente Samba, 9060, Cloudmonster, Palermo, etc., corregir la marca
|
||||
const secondaryCheck = detectBrandFromText(`${parsed.name || ''} ${text}`);
|
||||
if (secondaryCheck.confidence >= 0.95 && secondaryCheck.brand !== 'Urbano / Variado') {
|
||||
brand = secondaryCheck.brand;
|
||||
@ -735,7 +873,7 @@ Responde ÚNICAMENTE con el objeto JSON sin bloques de código markdown ni texto
|
||||
sizes,
|
||||
supplierPrice,
|
||||
suggestedRetailPrice: calculateRetailPrice(supplierPrice, 35, 30000),
|
||||
category: parsed.category || 'Sneakers / Colección',
|
||||
category: finalCategory,
|
||||
confidence: imagePayload ? 0.99 : secondaryCheck.confidence || 0.94,
|
||||
rating: proof.rating,
|
||||
reviewsCount: proof.reviewsCount,
|
||||
|
||||
@ -307,8 +307,8 @@ export async function POST(request: Request) {
|
||||
originalPrice: Math.round(retailPrice * 1.35),
|
||||
sizes: customSizes || post.extractedProduct?.sizes || ['37', '38', '39', '40', '41', '42', '43'],
|
||||
images: Array.isArray(post.rawImages) && post.rawImages.length > 0 ? post.rawImages : ['https://images.unsplash.com/photo-1595950653106-6c9ebd614d3a?auto=format&fit=crop&w=800&q=80'],
|
||||
description: generateAttractiveProductCopy({ name: prodName, brand: prodBrand, retailPrice }),
|
||||
category: post.extractedProduct?.category || 'Calzado / Sneakers',
|
||||
description: generateAttractiveProductCopy({ name: prodName, brand: prodBrand, retailPrice, category: post.extractedProduct?.category }),
|
||||
category: post.extractedProduct?.category || 'Zapatillas',
|
||||
inStock: true,
|
||||
isActive: true,
|
||||
rating: proof.rating,
|
||||
@ -352,7 +352,7 @@ export async function POST(request: Request) {
|
||||
sizes: p.extractedProduct?.sizes || ['37', '38', '39', '40', '41', '42', '43'],
|
||||
images: p.rawImages || ['https://images.unsplash.com/photo-1595950653106-6c9ebd614d3a?auto=format&fit=crop&w=800&q=80'],
|
||||
description: generateAttractiveProductCopy({ name: prodName, brand: prodBrand, retailPrice }),
|
||||
category: p.extractedProduct?.category || 'Calzado / Sneakers',
|
||||
category: p.extractedProduct?.category || 'Zapatillas',
|
||||
inStock: true,
|
||||
isActive: true,
|
||||
rating: proof.rating,
|
||||
|
||||
@ -17,33 +17,67 @@ export function generateAttractiveProductCopy(product: {
|
||||
retailPrice?: number;
|
||||
forbiddenTerms?: (string | undefined | null)[];
|
||||
}): string {
|
||||
const brand = product.brand || 'Sneakers';
|
||||
const brand = product.brand || 'Colección Urbana';
|
||||
const cleanName = sanitizePublicationTitle(
|
||||
product.name || 'Zapatillas Exclusivas',
|
||||
product.name || 'Artículo Exclusivo',
|
||||
product.forbiddenTerms,
|
||||
brand
|
||||
);
|
||||
const cat = (product.category || '').toLowerCase();
|
||||
|
||||
const hooks = [
|
||||
`🔥 ¡Lleva tu outfit al siguiente nivel con las icónicas ${cleanName}! Diseñadas para quienes no negocian entre estilo de alto impacto y máxima comodidad para el día a día.`,
|
||||
`⚡ Las ${cleanName} son el drop definitivo que tu colección estaba esperando. Silueta imbatible, acabados prémium y una estética urbana que no pasa desapercibida.`,
|
||||
`💎 Edición Prémium ${brand}: ${cleanName}. Fabricadas con materiales de máxima durabilidad, textura prémium y una combinación de colores pensada para resaltar en cualquier ocasión.`,
|
||||
`🚀 Dale un upgrade a tus pasos con las exclusivas ${cleanName}. Comodidad extrema desde el primer uso, amortiguación reactiva y estilo 100% garantizado.`,
|
||||
];
|
||||
let hook = '';
|
||||
let bullet1 = '• 💎 *Calidad Prémium Garantizada:* Materiales de alto rendimiento, costuras reforzadas y acabados de lujo.';
|
||||
let bullet2 = '• ☁️ *Máximo Confort y Ergonomía:* Diseñado para acompañarte todo el día con total comodidad.';
|
||||
let cta = '⚠️ *Disponibilidad Limitada:* Pocas unidades disponibles en inventario nacional. ¡Asegura el tuyo ahora antes de que se agoten!';
|
||||
|
||||
const seed = cleanName.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0);
|
||||
const selectedHook = hooks[Math.abs(seed) % hooks.length];
|
||||
if (cat.includes('chancla') || cat.includes('slide') || cat.includes('sandalia')) {
|
||||
hook = `🔥 ¡Comodidad insuperable con las nuevas ${cleanName}! Diseñadas para descanso, piscina o estilo urbano relajado con materiales ultra livianos y amortiguación suave.`;
|
||||
bullet1 = '• 💎 *Material EVA / Suave de Alta Densidad:* Espuma ergonómica que no se deforma y absorbe el impacto al caminar.';
|
||||
bullet2 = '• 🌊 *Resistentes al Agua y Antideslizantes:* Suela con agarre óptimo para máxima seguridad y secado rápido.';
|
||||
cta = '⚠️ *Disponibilidad Limitada:* Pocas unidades disponibles por talla. ¡Pide tus chanclas hoy mismo!';
|
||||
} else if (cat.includes('carriel') || cat.includes('bandolera') || cat.includes('riñonera') || cat.includes('crossbody')) {
|
||||
hook = `🔥 ¡Lleva tus esenciales con el auténtico estilo y practicidad del ${cleanName}! Múltiples compartimientos, cierres de alta durabilidad y correa ajustable ergonómica.`;
|
||||
bullet1 = '• 💎 *Materiales de Alta Resistencia:* Acabados prémium, costuras de seguridad y cierres metálicos fluidos.';
|
||||
bullet2 = '• 👝 *Distribución Inteligente:* Espacio seguro para celular, billetera, llaves y documentos con fácil acceso.';
|
||||
cta = '⚠️ *Edición Exclusiva:* Pocas unidades en bodega. ¡Haz tu pedido y paga al recibir en tu casa!';
|
||||
} else if (cat.includes('malet') || cat.includes('morral') || cat.includes('mochila') || cat.includes('tula')) {
|
||||
hook = `🚀 ¡Organiza tu día con la máxima capacidad y resistencia del ${cleanName}! Ideal para estudio, trabajo, gimnasio o viajes de fin de semana.`;
|
||||
bullet1 = '• 💎 *Compartimiento Acolchado para Laptop:* Protección antichoque para tus dispositivos y pertenencias de valor.';
|
||||
bullet2 = '• 🎒 *Cargaderas Acolchadas Antitranspirantes:* Distribución ergonómica del peso para proteger tu espalda.';
|
||||
cta = '⚠️ *Alta Demanda:* Pocas unidades disponibles para despacho inmediato nacional.';
|
||||
} else if (cat.includes('bolso') || cat.includes('cartera')) {
|
||||
hook = `✨ ¡Elegancia, distinción y versatilidad con el ${cleanName}! El toque definitivo para complementar tu outfit en cualquier momento del día.`;
|
||||
bullet1 = '• 💎 *Textura Prémium y Detalles Finos:* Herrajes dorados/plateados de alta gama y forro interior reforzado.';
|
||||
bullet2 = '• 👜 *Espacio Ideal y Versátil:* Capacidad perfecta para tu día a día manteniendo una silueta estilizada.';
|
||||
cta = '⚠️ *Colección Limitada:* Stock muy reducido en Colombia. ¡Asegura tu bolso favorito antes de que se agote!';
|
||||
} else if (cat.includes('gorra') || cat.includes('cap')) {
|
||||
hook = `🧢 ¡Completa tu look con la exclusiva ${cleanName}! Silueta estructurada, visera impecable y bordados en alta definición.`;
|
||||
bullet1 = '• 💎 *Bordado 3D de Alta Densidad:* Acabados milimétricos y corona estructurada que no pierde su forma.';
|
||||
bullet2 = '• 🔒 *Ajuste Cómodo y Seguro:* Correa regulable para adaptarse perfectamente a cualquier contorno.';
|
||||
cta = '⚠️ *Drop Exclusivo:* Pocas piezas en inventario. ¡Pídela ahora con envío a toda Colombia!';
|
||||
} else if (cat.includes('ropa') || cat.includes('textil') || cat.includes('camiseta') || cat.includes('hoodie')) {
|
||||
hook = `🔥 ¡Estilo urbano auténtico con la prenda ${cleanName}! Silueta moderna, textura suave al tacto y máxima frescura.`;
|
||||
bullet1 = '• 💎 *Algodón Prémium / Tela Pesada:* No destiñe ni encoge tras las lavadas, manteniendo caída y color.';
|
||||
bullet2 = '• 👕 *Corte Streetwear Confort:* Ajuste ideal para lucir impecable en cualquier ocasión.';
|
||||
cta = '⚠️ *Tallas Limitadas:* Disponible para entrega inmediata a nivel nacional.';
|
||||
} else {
|
||||
// Por defecto: Zapatillas / Sneakers
|
||||
hook = `🔥 ¡Lleva tu outfit al siguiente nivel con las icónicas ${cleanName}! Diseñadas para quienes no negocian entre estilo de alto impacto y máxima comodidad para el día a día.`;
|
||||
bullet1 = '• 💎 *Calidad G5 / 1.1 Prémium:* Materiales de alto rendimiento, costuras reforzadas y terminaciones impecables.';
|
||||
bullet2 = '• ☁️ *Confort Inigualable:* Plantilla acolchada ergonómica para máxima suavidad al caminar o entrenar.';
|
||||
cta = '⚠️ *Disponibilidad Limitada:* Pocas unidades disponibles por talla en inventario nacional. ¡Asegura tu par ahora antes de que se agoten!';
|
||||
}
|
||||
|
||||
const rawCopy = `${selectedHook}
|
||||
const rawCopy = `${hook}
|
||||
|
||||
✨ *¿Por qué todos las eligen?*
|
||||
• 💎 *Calidad G5 / 1.1 Prémium:* Materiales de alto rendimiento, costuras reforzadas y terminaciones impecables.
|
||||
• ☁️ *Confort Inigualable:* Plantilla acolchada ergonómica para máxima suavidad al caminar o entrenar.
|
||||
✨ *¿Por qué todos lo eligen?*
|
||||
${bullet1}
|
||||
${bullet2}
|
||||
• 🚚 *Despacho Prioritario Nacional:* Despachamos en 24 horas hábiles a cualquier ciudad de Colombia.
|
||||
• 💵 *Pago 100% Contraentrega:* Realizas tu pedido con total tranquilidad y pagas al recibir en tu puerta.
|
||||
• 🛡️ *Garantía Directa:* Revisas tu producto y verificas tu talla con total respaldo.
|
||||
• 🛡️ *Garantía Directa:* Revisas tu producto al llegar con total respaldo.
|
||||
|
||||
⚠️ *Disponibilidad Limitada:* Pocas unidades disponibles por talla en inventario nacional. ¡Asegura tu par ahora antes de que se agoten!`;
|
||||
${cta}`;
|
||||
|
||||
return sanitizePublicationText(rawCopy, product.forbiddenTerms, product.supplierPrice);
|
||||
}
|
||||
|
||||
@ -5,12 +5,391 @@ import { getProductSocialProof } from './social-proof';
|
||||
import { generateAttractiveProductCopy } from './attractive-copy';
|
||||
import { sanitizePublicationTitle } from './sanitizer';
|
||||
|
||||
/**
|
||||
* Catálogo maestro de marcas y sus firmas de modelos y palabras clave exclusivas
|
||||
*/
|
||||
export interface BrandSignature {
|
||||
brand: string;
|
||||
aliases: RegExp[];
|
||||
models: { name: string; pattern: RegExp }[];
|
||||
}
|
||||
|
||||
export const BRAND_SIGNATURES: BrandSignature[] = [
|
||||
{
|
||||
brand: 'Adidas',
|
||||
aliases: [/\badidas\b/i, /\btrefoil\b/i, /\btr[eé]bol\b/i, /3\s*rayas|tres\s*rayas/i],
|
||||
models: [
|
||||
{ name: 'Adidas Samba OG', pattern: /\bsamba\b/i },
|
||||
{ name: 'Adidas Gazelle Indoor', pattern: /\bgazelle\b/i },
|
||||
{ name: 'Adidas Campus 00s', pattern: /\bcampus\b/i },
|
||||
{ name: 'Adidas Handball Spezial', pattern: /\bspezial\b/i },
|
||||
{ name: 'Adidas Forum Low', pattern: /\bforum\b/i },
|
||||
{ name: 'Adidas Bad Bunny', pattern: /\bbad\s*bunny\b/i },
|
||||
{ name: 'Adidas Stan Smith', pattern: /\bstan\s*smith\b/i },
|
||||
{ name: 'Adidas Superstar', pattern: /\bsuperstar\b/i },
|
||||
{ name: 'Adidas SL 72', pattern: /\bsl\s*72\b|\bsl72\b/i },
|
||||
{ name: 'Adidas Response CL', pattern: /\bresponse\s*cl\b/i },
|
||||
{ name: 'Adidas Yeezy Boost 350', pattern: /\byeezy\b|\b350\s*v2\b|\bfoam\s*runner\b/i },
|
||||
{ name: 'Adidas Astir', pattern: /\bastir\b/i },
|
||||
{ name: 'Adidas Ozelia', pattern: /\bozelia\b/i },
|
||||
{ name: 'Adidas Adizero', pattern: /\badizero\b/i },
|
||||
{ name: 'Adidas Country OG', pattern: /\bcountry\s*og\b/i },
|
||||
{ name: 'Adidas NMD R1', pattern: /\bnmd\b/i },
|
||||
{ name: 'Adidas Ultraboost', pattern: /\bultra\s*boost\b|\bultraboost\b/i },
|
||||
],
|
||||
},
|
||||
{
|
||||
brand: 'Jordan',
|
||||
aliases: [/\bjordan\b/i, /\bjumpman\b/i, /\bair\s*jordan\b/i, /\baj\d+\b/i],
|
||||
models: [
|
||||
{ name: 'Jordan 4 Retro', pattern: /\b(?:retro\s*4|aj4|jordan\s*4|retro\s*iv)\b/i },
|
||||
{ name: 'Jordan 1 Retro High OG', pattern: /\b(?:retro\s*1|aj1|jordan\s*1|retro\s*i)\b/i },
|
||||
{ name: 'Jordan 3 Retro', pattern: /\b(?:retro\s*3|aj3|jordan\s*3|retro\s*iii)\b/i },
|
||||
{ name: 'Jordan 11 Retro', pattern: /\b(?:retro\s*11|aj11|jordan\s*11|retro\s*xi)\b/i },
|
||||
{ name: 'Jordan 1 Low Travis Scott', pattern: /\btravis\s*scott\b|\bcactus\s*jack\b/i },
|
||||
{ name: 'Jordan Spizike Low', pattern: /\bspizike\b/i },
|
||||
{ name: 'Jordan 5 Retro', pattern: /\b(?:retro\s*5|aj5|jordan\s*5)\b/i },
|
||||
{ name: 'Jordan 6 Retro', pattern: /\b(?:retro\s*6|aj6|jordan\s*6)\b/i },
|
||||
],
|
||||
},
|
||||
{
|
||||
brand: 'New Balance',
|
||||
aliases: [/\bnew\s*balance\b/i, /\bnb\b/i],
|
||||
models: [
|
||||
{ name: 'New Balance 9060', pattern: /\b9060\b/i },
|
||||
{ name: 'New Balance 550', pattern: /\b550\b/i },
|
||||
{ name: 'New Balance 530', pattern: /\b530\b/i },
|
||||
{ name: 'New Balance 1906R', pattern: /\b1906\b|\b1906r\b|\b1906d\b/i },
|
||||
{ name: 'New Balance 2002R', pattern: /\b2002\b|\b2002r\b|\bprotection\s*pack\b/i },
|
||||
{ name: 'New Balance 327', pattern: /\b327\b/i },
|
||||
{ name: 'New Balance 574', pattern: /\b574\b/i },
|
||||
{ name: 'New Balance 990v6', pattern: /\b990\b|\b990v\d\b/i },
|
||||
{ name: 'New Balance 991', pattern: /\b991\b/i },
|
||||
{ name: 'New Balance 992', pattern: /\b992\b/i },
|
||||
{ name: 'New Balance 993', pattern: /\b993\b/i },
|
||||
{ name: 'New Balance 610', pattern: /\b610\b/i },
|
||||
{ name: 'New Balance CT302', pattern: /\bct302\b/i },
|
||||
],
|
||||
},
|
||||
{
|
||||
brand: 'On Running',
|
||||
aliases: [/\bon\s*running\b/i, /\bon\s*cloud\b/i, /\bcloud\b/i, /\bclould\b/i],
|
||||
models: [
|
||||
{ name: 'On Cloudmonster', pattern: /\bcloudmonster\b|\bcloud\s*monster\b/i },
|
||||
{ name: 'On Cloudtilt Loewe', pattern: /\bcloudtilt\b|\bcloud\s*tilt\b|\bloewe\b/i },
|
||||
{ name: 'On Cloud 5', pattern: /\bcloud\s*5\b/i },
|
||||
{ name: 'On Cloud X3', pattern: /\bcloud\s*x\b|\bcloud\s*x3\b/i },
|
||||
{ name: 'On Cloudsurfer', pattern: /\bcloudsurfer\b|\bcloud\s*surfer\b/i },
|
||||
{ name: 'On Cloudstratus', pattern: /\bcloudstratus\b|\bcloud\s*stratus\b/i },
|
||||
{ name: 'On Cloudrunner', pattern: /\bcloudrunner\b|\bcloud\s*runner\b/i },
|
||||
{ name: 'On The Roger Centre Court', pattern: /\broger\b|\bthe\s*roger\b/i },
|
||||
],
|
||||
},
|
||||
{
|
||||
brand: 'Puma',
|
||||
aliases: [/\bpuma\b/i],
|
||||
models: [
|
||||
{ name: 'Puma Palermo Special', pattern: /\bpalermo\b/i },
|
||||
{ name: 'Puma Speedcat OG', pattern: /\bspeedcat\b|\bspeed\s*cat\b/i },
|
||||
{ name: 'Puma Suede Classic', pattern: /\bsuede\b/i },
|
||||
{ name: 'Puma Caven 2.0', pattern: /\bcaven\b/i },
|
||||
{ name: 'Puma Slipstream', pattern: /\bslipstream\b/i },
|
||||
{ name: 'Puma Mayze Wedge', pattern: /\bmayze\b/i },
|
||||
{ name: 'Puma Easy Rider', pattern: /\beasy\s*rider\b/i },
|
||||
{ name: 'Puma RS-X Efekt', pattern: /\brs-x\b|\brsx\b/i },
|
||||
{ name: 'Puma CA Pro Classic', pattern: /\bca\s*pro\b/i },
|
||||
],
|
||||
},
|
||||
{
|
||||
brand: 'Asics',
|
||||
aliases: [/\basics\b/i],
|
||||
models: [
|
||||
{ name: 'Asics Gel-Kayano 14', pattern: /\bgel[\s-]*kayano\b|\bkayano\b/i },
|
||||
{ name: 'Asics Gel-NYC', pattern: /\bgel[\s-]*nyc\b|\bnyc\b/i },
|
||||
{ name: 'Asics Gel-1130', pattern: /\bgel[\s-]*1130\b|\b1130\b/i },
|
||||
{ name: 'Asics GT-2160', pattern: /\bgt[\s-]*2160\b|\b2160\b/i },
|
||||
{ name: 'Asics Gel-Quantum 360', pattern: /\bgel[\s-]*quantum\b|\bquantum\b/i },
|
||||
{ name: 'Asics Gel-Nimbus 9', pattern: /\bgel[\s-]*nimbus\b|\bnimbus\b/i },
|
||||
{ name: 'Asics Gel-Lyte III', pattern: /\bgel[\s-]*lyte\b/i },
|
||||
{ name: 'Asics Novablast 4', pattern: /\bnovablast\b/i },
|
||||
],
|
||||
},
|
||||
{
|
||||
brand: 'Vans',
|
||||
aliases: [/\bvans\b/i, /\boff\s*the\s*wall\b/i],
|
||||
models: [
|
||||
{ name: 'Vans Knu Skool Chunky', pattern: /\bknu\s*skool\b|\bknuskool\b|\bknu\b/i },
|
||||
{ name: 'Vans Old Skool Classic', pattern: /\bold\s*skool\b|\boldskool\b/i },
|
||||
{ name: 'Vans Sk8-Hi Reissue', pattern: /\bsk8\b|\bsk8-hi\b|\bsk8\s*hi\b/i },
|
||||
{ name: 'Vans Authentic Pro', pattern: /\bauthentic\b/i },
|
||||
{ name: 'Vans Era Classic', pattern: /\bera\b/i },
|
||||
{ name: 'Vans Slip-On Checkerboard', pattern: /\bslip[\s-]*on\b|\bcheckerboard\b/i },
|
||||
{ name: 'Vans UltraRange EXO', pattern: /\bultrarange\b|\bultra\s*range\b/i },
|
||||
],
|
||||
},
|
||||
{
|
||||
brand: 'Converse',
|
||||
aliases: [/\bconverse\b/i, /\ball\s*star\b/i],
|
||||
models: [
|
||||
{ name: 'Converse Chuck 70 Vintage', pattern: /\bchuck\s*70\b|\bchuck70\b/i },
|
||||
{ name: 'Converse Chuck Taylor All Star', pattern: /\bchuck\s*taylor\b|\ball\s*star\b/i },
|
||||
{ name: 'Converse Run Star Hike', pattern: /\brun\s*star\s*hike\b|\brun\s*star\b/i },
|
||||
{ name: 'Converse Run Star Motion', pattern: /\brun\s*star\s*motion\b/i },
|
||||
{ name: 'Converse Weapon Ox', pattern: /\bweapon\b/i },
|
||||
{ name: 'Converse One Star Pro', pattern: /\bone\s*star\b/i },
|
||||
],
|
||||
},
|
||||
{
|
||||
brand: 'Salomon',
|
||||
aliases: [/\bsalomon\b/i],
|
||||
models: [
|
||||
{ name: 'Salomon XT-6 Gore-Tex', pattern: /\bxt-6\b|\bxt6\b/i },
|
||||
{ name: 'Salomon XT-4 OG', pattern: /\bxt-4\b|\bxt4\b/i },
|
||||
{ name: 'Salomon Speedcross 6', pattern: /\bspeedcross\b/i },
|
||||
{ name: 'Salomon ACS Pro Advanced', pattern: /\bacs\s*pro\b/i },
|
||||
{ name: 'Salomon XA Pro 3D', pattern: /\bxa\s*pro\b/i },
|
||||
],
|
||||
},
|
||||
{
|
||||
brand: 'Hoka',
|
||||
aliases: [/\bhoka\b/i, /\bhoka\s*one\s*one\b/i],
|
||||
models: [
|
||||
{ name: 'Hoka Clifton 9', pattern: /\bclifton\b/i },
|
||||
{ name: 'Hoka Bondi 8', pattern: /\bbondi\b/i },
|
||||
{ name: 'Hoka Tor Ultra Low', pattern: /\btor\s*ultra\b/i },
|
||||
{ name: 'Hoka Speedgoat 5', pattern: /\bspeedgoat\b/i },
|
||||
{ name: 'Hoka Hopara Sandal', pattern: /\bhopara\b/i },
|
||||
{ name: 'Hoka Transport', pattern: /\btransport\b/i },
|
||||
],
|
||||
},
|
||||
{
|
||||
brand: 'Balenciaga',
|
||||
aliases: [/\bbalenciaga\b/i],
|
||||
models: [
|
||||
{ name: 'Balenciaga Track 3.0', pattern: /\btrack\b|\btrack\s*2\b|\btrack\s*3\b/i },
|
||||
{ name: 'Balenciaga Triple S Clear Sole', pattern: /\btriple\s*s\b|\btriples\b/i },
|
||||
{ name: 'Balenciaga 3XL Sneaker', pattern: /\b3xl\b/i },
|
||||
{ name: 'Balenciaga Defender Chunky', pattern: /\bdefender\b/i },
|
||||
{ name: 'Balenciaga Speed Trainer Knit', pattern: /\bspeed\s*trainer\b/i },
|
||||
],
|
||||
},
|
||||
{
|
||||
brand: 'Alexander McQueen',
|
||||
aliases: [/\bmcq|mcqueen|alexander\s*mcqueen\b/i],
|
||||
models: [
|
||||
{ name: 'Alexander McQueen Oversized', pattern: /\boversized\b|\bchunky\s*sole\b/i },
|
||||
{ name: 'Alexander McQueen Court Sneaker', pattern: /\bcourt\b/i },
|
||||
{ name: 'Alexander McQueen Tread Slick', pattern: /\btread\s*slick\b/i },
|
||||
],
|
||||
},
|
||||
{
|
||||
brand: 'Golden Goose',
|
||||
aliases: [/\bgolden\s*goose\b/i, /\bggdb\b/i],
|
||||
models: [
|
||||
{ name: 'Golden Goose Super-Star Classic', pattern: /\bsuper[\s-]*star\b|\bball[\s-]*star\b/i },
|
||||
{ name: 'Golden Goose Mid Star', pattern: /\bmid\s*star\b/i },
|
||||
{ name: 'Golden Goose Purestar', pattern: /\bpurestar\b/i },
|
||||
],
|
||||
},
|
||||
{
|
||||
brand: 'Crocs',
|
||||
aliases: [/\bcrocs\b/i],
|
||||
models: [
|
||||
{ name: 'Crocs Pollex Clog Salehe Bembury', pattern: /\bpollex\b|\bsalehe\b/i },
|
||||
{ name: 'Crocs Echo Clog', pattern: /\becho\s*clog\b|\becho\b/i },
|
||||
{ name: 'Crocs Mega Crush Clog', pattern: /\bmega\s*crush\b|\bcrush\b/i },
|
||||
{ name: 'Crocs Classic Clog', pattern: /\bclassic\s*clog\b|\bclog\b/i },
|
||||
],
|
||||
},
|
||||
{
|
||||
brand: 'Under Armour',
|
||||
aliases: [/\bunder\s*armour\b/i, /\bua\b/i],
|
||||
models: [
|
||||
{ name: 'Under Armour Curry Flow', pattern: /\bcurry\b/i },
|
||||
{ name: 'Under Armour Project Rock', pattern: /\bproject\s*rock\b/i },
|
||||
{ name: 'Under Armour HOVR Phantom', pattern: /\bhovr\b|\bphantom\b/i },
|
||||
],
|
||||
},
|
||||
{
|
||||
brand: 'Lacoste',
|
||||
aliases: [/\blacoste\b/i, /\bcocodrilo\b/i],
|
||||
models: [
|
||||
{ name: 'Lacoste Carnaby Pro', pattern: /\bcarnaby\b/i },
|
||||
{ name: 'Lacoste Europa Classic', pattern: /\beuropa\b/i },
|
||||
{ name: 'Lacoste T-Clip Urban', pattern: /\bt[\s-]*clip\b/i },
|
||||
{ name: 'Lacoste L001 Sneaker', pattern: /\bl001\b/i },
|
||||
],
|
||||
},
|
||||
{
|
||||
brand: 'Timberland',
|
||||
aliases: [/\btimberland\b/i, /\btimbs\b/i],
|
||||
models: [
|
||||
{ name: 'Timberland 6-Inch Premium Boot', pattern: /\b6[\s-]*inch\b|\byellow\s*boot\b|\bbotas?\b/i },
|
||||
{ name: 'Timberland Euro Trekker', pattern: /\beuro\s*trekker\b/i },
|
||||
],
|
||||
},
|
||||
{
|
||||
brand: 'Diesel',
|
||||
aliases: [/\bdiesel\b/i],
|
||||
models: [
|
||||
{ name: 'Diesel 1DR Sneaker', pattern: /\b1dr\b/i },
|
||||
{ name: 'Diesel S-Serendipity', pattern: /\bserendipity\b/i },
|
||||
{ name: 'Diesel S-Prototype', pattern: /\bprototype\b/i },
|
||||
],
|
||||
},
|
||||
{
|
||||
brand: 'Oakley',
|
||||
aliases: [/\boakley\b/i],
|
||||
models: [
|
||||
{ name: 'Oakley Chop Saw Mule', pattern: /\bchop\s*saw\b/i },
|
||||
{ name: 'Oakley Flesh Slip-On', pattern: /\bflesh\b/i },
|
||||
{ name: 'Oakley Teeth Sneaker', pattern: /\bteeth\b/i },
|
||||
],
|
||||
},
|
||||
{
|
||||
brand: 'Fila',
|
||||
aliases: [/\bfila\b/i],
|
||||
models: [
|
||||
{ name: 'Fila Disruptor II Chunky', pattern: /\bdisruptor\b/i },
|
||||
{ name: 'Fila Ray Tracer', pattern: /\bray\s*tracer\b/i },
|
||||
],
|
||||
},
|
||||
{
|
||||
brand: 'Reebok',
|
||||
aliases: [/\breebok\b/i],
|
||||
models: [
|
||||
{ name: 'Reebok Club C 85 Vintage', pattern: /\bclub\s*c\b|\bclubc\b/i },
|
||||
{ name: 'Reebok Classic Leather', pattern: /\bclassic\s*leather\b/i },
|
||||
{ name: 'Reebok BB 4000 II', pattern: /\bbb\s*4000\b/i },
|
||||
],
|
||||
},
|
||||
{
|
||||
brand: 'Bape',
|
||||
aliases: [/\bbape\b/i, /\ba\s*bathing\s*ape\b/i],
|
||||
models: [
|
||||
{ name: 'Bape Sta Low Classic', pattern: /\bbape\s*sta\b|\bbapesta\b/i },
|
||||
{ name: 'Bape Sk8 Sta', pattern: /\bsk8\s*sta\b/i },
|
||||
],
|
||||
},
|
||||
{
|
||||
brand: 'Louis Vuitton',
|
||||
aliases: [/\blouis\s*vuitton\b/i, /\blv\b/i],
|
||||
models: [
|
||||
{ name: 'Louis Vuitton LV Skate Sneaker', pattern: /\blv\s*skate\b|\bskate\b/i },
|
||||
{ name: 'Louis Vuitton LV Trainer', pattern: /\blv\s*trainer\b|\btrainer\b/i },
|
||||
],
|
||||
},
|
||||
{
|
||||
brand: 'Dior',
|
||||
aliases: [/\bdior\b/i],
|
||||
models: [
|
||||
{ name: 'Dior B22 Chunky Runner', pattern: /\bb22\b/i },
|
||||
{ name: 'Dior B30 Technical Mesh', pattern: /\bb30\b/i },
|
||||
{ name: 'Dior B23 High Top Oblique', pattern: /\bb23\b|\boblique\b/i },
|
||||
],
|
||||
},
|
||||
{
|
||||
brand: 'Gucci',
|
||||
aliases: [/\bgucci\b/i],
|
||||
models: [
|
||||
{ name: 'Gucci Rhyton Leather Chunky', pattern: /\brhyton\b/i },
|
||||
{ name: 'Gucci Ace Embroidered Sneaker', pattern: /\bace\b/i },
|
||||
{ name: 'Gucci Screener Vintage', pattern: /\bscreener\b/i },
|
||||
],
|
||||
},
|
||||
{
|
||||
brand: 'Prada',
|
||||
aliases: [/\bprada\b/i],
|
||||
models: [
|
||||
{ name: "Prada America's Cup", pattern: /\bamericas?\s*cup\b|\bcup\b/i },
|
||||
{ name: 'Prada Cloudbust Thunder', pattern: /\bcloudbust\b/i },
|
||||
{ name: 'Prada Downtown Leather', pattern: /\bdowntown\b/i },
|
||||
],
|
||||
},
|
||||
{
|
||||
brand: 'Autry',
|
||||
aliases: [/\bautry\b/i],
|
||||
models: [{ name: 'Autry Medalist Low Vintage', pattern: /\bmedalist\b|\bclc\b/i }],
|
||||
},
|
||||
{
|
||||
brand: 'Veja',
|
||||
aliases: [/\bveja\b/i],
|
||||
models: [
|
||||
{ name: 'Veja Campo Extra White', pattern: /\bcampo\b/i },
|
||||
{ name: 'Veja V-10 Leather', pattern: /\bv-10\b|\bv10\b/i },
|
||||
{ name: 'Veja V-12 B-Mesh', pattern: /\bv-12\b|\bv12\b/i },
|
||||
],
|
||||
},
|
||||
{
|
||||
brand: 'Nike',
|
||||
aliases: [/\bnike\b/i, /\bswoosh\b/i],
|
||||
models: [
|
||||
{ name: 'Nike Air Force 1 07', pattern: /\bair\s*force\b|\baf1\b|\baf-1\b|\bforce\s*1\b/i },
|
||||
{ name: 'Nike Dunk Low Retro Panda', pattern: /\bdunk\b|\bpanda\b|\bsb\s*dunk\b/i },
|
||||
{ name: 'Nike Air Max 90', pattern: /\bair\s*max\s*90\b|\bam90\b|\bmax\s*90\b/i },
|
||||
{ name: 'Nike Air Max 97', pattern: /\bair\s*max\s*97\b|\bam97\b|\bmax\s*97\b/i },
|
||||
{ name: 'Nike Air Max 270', pattern: /\bair\s*max\s*270\b|\bmax\s*270\b/i },
|
||||
{ name: 'Nike Air Max Plus TN', pattern: /\btn\b|\bair\s*max\s*plus\b|\btuned\b/i },
|
||||
{ name: 'Nike Vomero 5', pattern: /\bvomero\b|\bvomero\s*5\b/i },
|
||||
{ name: 'Nike P-6000 Retro', pattern: /\bp-6000\b|\bp6000\b/i },
|
||||
{ name: 'Nike Shox TL', pattern: /\bshox\b|\bshox\s*tl\b|\br4\b/i },
|
||||
{ name: 'Nike Cortez Classic', pattern: /\bcortez\b/i },
|
||||
{ name: 'Nike Blazer Mid 77', pattern: /\bblazer\b/i },
|
||||
{ name: 'Nike TC 7900 Chunky', pattern: /\btc\s*7900\b|\btc7900\b/i },
|
||||
{ name: 'Nike Gamma Force', pattern: /\bgamma\s*force\b/i },
|
||||
{ name: 'Nike Air VaporMax Plus', pattern: /\bvapormax\b|\bvapor\s*max\b/i },
|
||||
{ name: 'Nike Pegasus 40', pattern: /\bpegasus\b/i },
|
||||
{ name: 'Nike Killshot 2', pattern: /\bkillshot\b/i },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Detector determinístico y de alta precisión de marca y modelo desde texto
|
||||
*/
|
||||
export function detectBrandFromText(text: string): {
|
||||
brand: string;
|
||||
modelName?: string;
|
||||
confidence: number;
|
||||
} {
|
||||
const normalized = text.toLowerCase();
|
||||
|
||||
// 1. Prioridad: Buscar coincidencia de modelo exacto
|
||||
for (const item of BRAND_SIGNATURES) {
|
||||
for (const model of item.models) {
|
||||
if (model.pattern.test(normalized)) {
|
||||
return {
|
||||
brand: item.brand,
|
||||
modelName: model.name,
|
||||
confidence: 0.96,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Buscar marca por alias explícito
|
||||
for (const item of BRAND_SIGNATURES) {
|
||||
for (const alias of item.aliases) {
|
||||
if (alias.test(normalized)) {
|
||||
return {
|
||||
brand: item.brand,
|
||||
confidence: 0.88,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Si no hay coincidencia, NO asignar Nike arbitrariamente
|
||||
return {
|
||||
brand: 'Urbano / Variado',
|
||||
confidence: 0.4,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extractor robusto de precios mayoristas en COP desde mensajes de WhatsApp
|
||||
* Soporta emojis de dinero (💸, 💰, 💵, 💲), palabras clave, formatos con punto (75.000), 'k' (75k) y 'mil' (75 mil).
|
||||
*/
|
||||
export function extractSupplierPrice(text: string): number {
|
||||
// 1. Buscar patrones con símbolos o emojis de dinero y palabras clave
|
||||
const explicitPattern =
|
||||
/(?:💸|💰|💵|💲|precio|valor|costo|mayorista|mayor|mayoristas|\$)+[\s:=]*\$?\s*(\d{2,3}(?:\.\d{3})*|\d{2,3}\s*k|\d{2,3}\s*mil|\d{5,6})/i;
|
||||
const explicitMatch = text.match(explicitPattern);
|
||||
@ -27,34 +406,146 @@ export function extractSupplierPrice(text: string): number {
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Buscar números con formato de miles (ej: 75.000, 80.000, 100.000, 125.500)
|
||||
const dotThousands = text.match(/\b([4-9]\d|1\d{2}|2\d{2})\.(000|500)\b/);
|
||||
if (dotThousands) {
|
||||
const num = parseInt(dotThousands[0].replace(/\./g, ''), 10);
|
||||
if (!isNaN(num) && num >= 40000 && num <= 500000) return num;
|
||||
}
|
||||
|
||||
// 3. Buscar valores con 'k' o 'mil' (ej: 75k, 80 mil, 95k)
|
||||
const kMatch = text.match(/\b([4-9]\d|1\d{2}|2\d{2})\s*(?:k|mil)\b/i);
|
||||
if (kMatch) {
|
||||
const num = parseInt(kMatch[1], 10);
|
||||
if (!isNaN(num)) return num * 1000;
|
||||
}
|
||||
|
||||
// 4. Buscar números de 5 o 6 dígitos en rango de precio razonable (40000 a 300000)
|
||||
const rawNumbers = text.match(/\b(4[5-9]\d{3}|[5-9]\d{4}|1\d{5}|2\d{5})\b/g);
|
||||
if (rawNumbers && rawNumbers.length > 0) {
|
||||
const num = parseInt(rawNumbers[0], 10);
|
||||
if (!isNaN(num)) return num;
|
||||
}
|
||||
|
||||
return 75000; // Valor por defecto representativo para calzado nacional
|
||||
return 75000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extractor de tallas con normalización Unicode (soporta small-caps como ᴛᴀʟʟᴀs ᴅɪsᴘᴏɴɪʙʟᴇs:36/41/44)
|
||||
* Detección y Categorización Inteligente Multi-categoría
|
||||
* Identifica con precisión: Zapatillas, Chanclas, Bolsos, Maletines, Carrieles, Gorras, Ropa, Accesorios
|
||||
*/
|
||||
export function extractSizes(rawText: string): string[] {
|
||||
export interface CategoryDetection {
|
||||
category: string;
|
||||
categoryKey: 'zapatillas' | 'chanclas' | 'bolsos' | 'maletines' | 'carrieles' | 'gorras' | 'ropa' | 'accesorios';
|
||||
icon: string;
|
||||
confidence: number;
|
||||
}
|
||||
|
||||
export const OFFICIAL_CATEGORIES: { id: CategoryDetection['categoryKey']; label: string; icon: string }[] = [
|
||||
{ id: 'zapatillas', label: 'Zapatillas', icon: '👟' },
|
||||
{ id: 'chanclas', label: 'Chanclas / Sandalias', icon: '🩴' },
|
||||
{ id: 'bolsos', label: 'Bolsos / Carteras', icon: '👜' },
|
||||
{ id: 'maletines', label: 'Maletines / Morrales', icon: '🎒' },
|
||||
{ id: 'carrieles', label: 'Carrieles / Bandoleras', icon: '👝' },
|
||||
{ id: 'gorras', label: 'Gorras / Caps', icon: '🧢' },
|
||||
{ id: 'ropa', label: 'Ropa / Streetwear', icon: '👕' },
|
||||
{ id: 'accesorios', label: 'Accesorios', icon: '🕶️' },
|
||||
];
|
||||
|
||||
export function detectCategory(text: string, title?: string): CategoryDetection {
|
||||
const full = `${title || ''} ${text || ''}`.toLowerCase();
|
||||
|
||||
// 1. Chanclas / Sandalias / Slides / Crocs
|
||||
if (
|
||||
/\b(chancl[as]|sandali[as]|slide[s]?|croc[s]?|pantufl[as]|ojot[as]|adilette|chola[s]|chinela[s]|huarache[s]|alparagatas?|flip[\s-]*flops?|chancleta[s]?|chancletero)\b/i.test(
|
||||
full
|
||||
)
|
||||
) {
|
||||
return { category: 'Chanclas', categoryKey: 'chanclas', icon: '🩴', confidence: 0.98 };
|
||||
}
|
||||
|
||||
// 2. Carrieles / Bandoleras / Riñoneras / Crossbody / Pecheras / Canguros
|
||||
if (
|
||||
/\b(carriel(es)?|bandolera[s]?|ri[ñn]onera[s]?|pechera[s]?|crossbody|waist[\s-]*bag|fanny[\s-]*pack|chest[\s-]*bag|canguro[s]?|mariconera[s]?|shoulder[\s-]*bag|koala[s]?|manbag)\b/i.test(
|
||||
full
|
||||
)
|
||||
) {
|
||||
return { category: 'Carrieles', categoryKey: 'carrieles', icon: '👝', confidence: 0.98 };
|
||||
}
|
||||
|
||||
// 3. Maletines / Morrales / Mochilas / Backpacks / Tulas
|
||||
if (
|
||||
/\b(malet[ií]n(es)?|morral(es)?|mochila[s]?|backpack[s]?|tula[s]?|maleta[s]?|bolso\s*deportivo|duffle|gym[\s-]*bag|equipaje)\b/i.test(
|
||||
full
|
||||
)
|
||||
) {
|
||||
return { category: 'Maletines', categoryKey: 'maletines', icon: '🎒', confidence: 0.98 };
|
||||
}
|
||||
|
||||
// 4. Bolsos / Carteras / Tote
|
||||
if (
|
||||
/\b(bolso[s]?|cartera[s]?|tote([\s-]*bag)?|clutch|satchel|baguette|monedero[s]?|shopping[\s-]*bag|handbag[s]?|bolsa[s]?\s*de\s*mano)\b/i.test(
|
||||
full
|
||||
)
|
||||
) {
|
||||
return { category: 'Bolsos', categoryKey: 'bolsos', icon: '👜', confidence: 0.97 };
|
||||
}
|
||||
|
||||
// 5. Gorras / Sombreros / Accesorios para la cabeza
|
||||
if (
|
||||
/\b(gorra[s]?|gorro[s]?|cap[s]?|snapback|bucket[\s-]*hat|beanie|sombrero[s]?|visera[s]?)\b/i.test(
|
||||
full
|
||||
)
|
||||
) {
|
||||
return { category: 'Gorras', categoryKey: 'gorras', icon: '🧢', confidence: 0.98 };
|
||||
}
|
||||
|
||||
// 6. Ropa / Textil
|
||||
if (
|
||||
/\b(camiset[as]|camibuso[s]?|buzo[s]?|hoodie[s]?|chaqueta[s]?|sudadera[s]?|pantalon(es)?|short[s]?|jogger[s]?|polo[s]?|conjunto[s]?|bermuda[s]?|overol[es]?)\b/i.test(
|
||||
full
|
||||
)
|
||||
) {
|
||||
return { category: 'Ropa', categoryKey: 'ropa', icon: '👕', confidence: 0.97 };
|
||||
}
|
||||
|
||||
// 7. Accesorios varios
|
||||
if (
|
||||
/\b(billetera[s]?|cintur[oó]n(es)?|correa[s]?|reloj(es)?|gafa[s]?|medias?|calcet[ií]n(es)?)\b/i.test(
|
||||
full
|
||||
)
|
||||
) {
|
||||
return { category: 'Accesorios', categoryKey: 'accesorios', icon: '🕶️', confidence: 0.95 };
|
||||
}
|
||||
|
||||
// 8. Por defecto: Zapatillas
|
||||
return { category: 'Zapatillas', categoryKey: 'zapatillas', icon: '👟', confidence: 0.92 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Extractor de tallas con normalización Unicode y distinción por categoría
|
||||
*/
|
||||
export function extractSizes(rawText: string, category?: string): string[] {
|
||||
const normCat = (category || '').toLowerCase();
|
||||
const isAccessory =
|
||||
normCat.includes('bolso') ||
|
||||
normCat.includes('malet') ||
|
||||
normCat.includes('carriel') ||
|
||||
normCat.includes('gorra') ||
|
||||
normCat.includes('accesorio');
|
||||
|
||||
// Si es un carriel, bolso, maletín o gorra, buscar si dice Talla Única o estándar
|
||||
if (isAccessory) {
|
||||
if (/\b(talla\s*[\u00fa]nica|talla\s*unica|unitalla|ajustable|est[aá]ndar|standard)\b/i.test(rawText)) {
|
||||
return ['Talla Única'];
|
||||
}
|
||||
}
|
||||
|
||||
// Si es Ropa, buscar tallas S, M, L, XL, XXL
|
||||
if (normCat.includes('ropa')) {
|
||||
const textSizes = rawText.match(/\b(xs|s|m|l|xl|xxl|2xl|3xl)\b/gi);
|
||||
if (textSizes && textSizes.length > 0) {
|
||||
return Array.from(new Set(textSizes.map((s) => s.toUpperCase())));
|
||||
}
|
||||
}
|
||||
|
||||
const normalized = rawText
|
||||
.normalize('NFKD')
|
||||
.replace(/[ᴛ]/g, 't')
|
||||
@ -71,7 +562,6 @@ export function extractSizes(rawText: string): string[] {
|
||||
|
||||
const sizes: string[] = [];
|
||||
|
||||
// Rango explícito (ej: 36 a 43, 36 al 43, 38-43)
|
||||
const rangeMatch = normalized.match(
|
||||
/(?:tallas?|eur|talla|disponibles?)?[^\d\n]*\b(3[4-9]|4[0-6])\s*(?:a|al|-)\s*(3[4-9]|4[0-6])\b/i
|
||||
);
|
||||
@ -84,11 +574,9 @@ export function extractSizes(rawText: string): string[] {
|
||||
}
|
||||
}
|
||||
|
||||
// Sección de tallas o texto completo
|
||||
const tallaSection = normalized.match(/(?:tallas?|eur|disponibles?|talla)[^\n]*/i);
|
||||
const textToScan = tallaSection ? tallaSection[0] : normalized;
|
||||
|
||||
// Extraer números individuales de 2 dígitos entre 34 y 46
|
||||
const matches = textToScan.match(/(?:^|[^\d])(3[4-9]|4[0-6])(?=[^\d]|$)/g);
|
||||
if (matches) {
|
||||
matches.forEach((m) => {
|
||||
@ -100,6 +588,12 @@ export function extractSizes(rawText: string): string[] {
|
||||
}
|
||||
|
||||
if (sizes.length === 0) {
|
||||
if (isAccessory) {
|
||||
return ['Talla Única'];
|
||||
}
|
||||
if (normCat.includes('ropa')) {
|
||||
return ['S', 'M', 'L', 'XL'];
|
||||
}
|
||||
return ['37', '38', '39', '40', '41', '42', '43'];
|
||||
}
|
||||
|
||||
@ -107,7 +601,8 @@ export function extractSizes(rawText: string): string[] {
|
||||
}
|
||||
|
||||
/**
|
||||
* Limpia y formatea el nombre del producto desde el mensaje del proveedor
|
||||
* Limpia y formatea el nombre del producto garantizando que NUNCA se incluyan nombres
|
||||
* de proveedores, bodegas, canales o grupos de WhatsApp.
|
||||
*/
|
||||
export function cleanProductName(
|
||||
text: string,
|
||||
@ -126,9 +621,15 @@ export function cleanProductName(
|
||||
lower.includes('bodega mayorista') ||
|
||||
lower.includes('proveedor') ||
|
||||
lower.includes('canal mayorista') ||
|
||||
lower.includes('canal de difusión') ||
|
||||
lower.includes('canal de difusion') ||
|
||||
lower.includes('grupo mayorista') ||
|
||||
lower.includes('grupo whatsapp') ||
|
||||
lower.includes('team emporio') ||
|
||||
lower.includes('notificaciones')
|
||||
lower.includes('notificaciones') ||
|
||||
lower.includes('medios y prensa') ||
|
||||
lower.includes('calzado mayorista') ||
|
||||
lower.includes('san andresito')
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
@ -147,33 +648,77 @@ export function cleanProductName(
|
||||
return sanitized;
|
||||
}
|
||||
}
|
||||
return sanitizePublicationTitle(
|
||||
`${detectedBrand} Urban Edition`,
|
||||
extraForbidden,
|
||||
detectedBrand
|
||||
);
|
||||
|
||||
const catDetect = detectCategory(text);
|
||||
let defaultGeneric = `${detectedBrand} Urban Edition`;
|
||||
if (detectedBrand === 'Urbano / Variado') {
|
||||
if (catDetect.category === 'Chanclas') defaultGeneric = 'Sandalia / Slide Confort';
|
||||
else if (catDetect.category === 'Carrieles') defaultGeneric = 'Carriel / Bandolera Urbana';
|
||||
else if (catDetect.category === 'Maletines') defaultGeneric = 'Morral / Maletín Deportivo';
|
||||
else if (catDetect.category === 'Bolsos') defaultGeneric = 'Bolso Elegante Casual';
|
||||
else if (catDetect.category === 'Gorras') defaultGeneric = 'Gorra Urbana Clásica';
|
||||
else if (catDetect.category === 'Ropa') defaultGeneric = 'Prenda Streetwear Urbana';
|
||||
else if (catDetect.category === 'Accesorios') defaultGeneric = 'Accesorio Urbano Prémium';
|
||||
else defaultGeneric = 'Zapatilla Urbana Nacional';
|
||||
} else {
|
||||
if (catDetect.category === 'Chanclas') defaultGeneric = `${detectedBrand} Slide Confort`;
|
||||
else if (catDetect.category === 'Carrieles') defaultGeneric = `${detectedBrand} Carriel / Bandolera`;
|
||||
else if (catDetect.category === 'Maletines') defaultGeneric = `${detectedBrand} Morral Deportivo`;
|
||||
else if (catDetect.category === 'Bolsos') defaultGeneric = `${detectedBrand} Bolso Casual`;
|
||||
else if (catDetect.category === 'Gorras') defaultGeneric = `${detectedBrand} Gorra Clásica`;
|
||||
else if (catDetect.category === 'Ropa') defaultGeneric = `${detectedBrand} Colección Streetwear`;
|
||||
else defaultGeneric = `${detectedBrand} Urban Edition`;
|
||||
}
|
||||
|
||||
return sanitizePublicationTitle(defaultGeneric, extraForbidden, detectedBrand);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper para extraer base64 y mimeType de una imagen en cualquier formato
|
||||
*/
|
||||
function extractBase64Payload(imageInput?: string): { mimeType: string; data: string } | null {
|
||||
if (!imageInput || typeof imageInput !== 'string') return null;
|
||||
|
||||
const dataUriMatch = imageInput.match(/^data:([^;]+);base64,(.+)$/);
|
||||
if (dataUriMatch) {
|
||||
return {
|
||||
mimeType: dataUriMatch[1] || 'image/jpeg',
|
||||
data: dataUriMatch[2],
|
||||
};
|
||||
}
|
||||
|
||||
if (imageInput.length > 100 && !imageInput.startsWith('http') && !imageInput.includes(' ')) {
|
||||
return {
|
||||
mimeType: 'image/jpeg',
|
||||
data: imageInput,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parser Inteligente de Mensajes y Visión Multimodal con Gemini y Categorización Automática
|
||||
* Reconoce zapatillas, chanclas, bolsos, maletines, carrieles, gorras, ropa y accesorios.
|
||||
*
|
||||
* REGLA ESTRICTA: NI en el título NI en el texto de la publicación se debe incluir el nombre
|
||||
* del grupo de WhatsApp ni el proveedor del producto ni el costo.
|
||||
*/
|
||||
export async function parseSupplierMessage(
|
||||
text: string,
|
||||
imageBase64OrUrl?: string,
|
||||
extraForbidden: (string | undefined | null)[] = []
|
||||
): Promise<ExtractedProductData> {
|
||||
const apiKey = process.env.GEMINI_API_KEY;
|
||||
const categoryDetection = detectCategory(text);
|
||||
|
||||
// Fallback determinístico con catálogo maestro de marcas y categorías
|
||||
const fallbackExtract = (): ExtractedProductData => {
|
||||
const price = extractSupplierPrice(text);
|
||||
const sizes = extractSizes(text);
|
||||
|
||||
let brand = 'Nike';
|
||||
if (/adidas|samba|campus|bad bunny|forum|gazelle|stan smith/i.test(text)) brand = 'Adidas';
|
||||
else if (/jordan|air jordan|aj1|aj4|aj3/i.test(text)) brand = 'Jordan';
|
||||
else if (/new balance|nb|9060|550|530|1906/i.test(text)) brand = 'New Balance';
|
||||
else if (/puma|palermo|suede|speedcat/i.test(text)) brand = 'Puma';
|
||||
else if (/asics|gel kayano|gel nyc/i.test(text)) brand = 'Asics';
|
||||
else if (/vans|old skool|knu skool/i.test(text)) brand = 'Vans';
|
||||
else if (/coach/i.test(text)) brand = 'Coach';
|
||||
|
||||
const rawClean = cleanProductName(text, brand, extraForbidden);
|
||||
const sizes = extractSizes(text, categoryDetection.category);
|
||||
const brandDetection = detectBrandFromText(text);
|
||||
const brand = brandDetection.brand;
|
||||
const rawClean = brandDetection.modelName || cleanProductName(text, brand, extraForbidden);
|
||||
const cleanName = sanitizePublicationTitle(rawClean, extraForbidden, brand);
|
||||
const proof = getProductSocialProof({ name: cleanName, brand });
|
||||
|
||||
@ -183,8 +728,8 @@ export async function parseSupplierMessage(
|
||||
sizes,
|
||||
supplierPrice: price,
|
||||
suggestedRetailPrice: calculateRetailPrice(price, 35, 30000),
|
||||
category: 'Calzado / Sneakers',
|
||||
confidence: 0.92,
|
||||
category: categoryDetection.category,
|
||||
confidence: brandDetection.confidence,
|
||||
rating: proof.rating,
|
||||
reviewsCount: proof.reviewsCount,
|
||||
salesCount: proof.salesCount,
|
||||
@ -198,64 +743,145 @@ export async function parseSupplierMessage(
|
||||
|
||||
try {
|
||||
const ai = new GoogleGenAI({ apiKey });
|
||||
const prompt = `Eres un asistente experto en e-commerce y calzado deportivo en Colombia.
|
||||
Analiza el siguiente mensaje extraído de un grupo mayorista de WhatsApp.
|
||||
Extrae los siguientes campos en formato JSON estricto:
|
||||
- "name": Nombre comercial limpio del modelo (ej: "Adidas Samba OG", "Nike Dunk Low Retro Panda", "New Balance 9060").
|
||||
- "brand": Marca comercial ("Adidas", "Nike", "Jordan", "New Balance", "Puma", "Asics", "Vans", etc.).
|
||||
- "sizes": Array de números de tallas disponibles (ej: ["36", "41", "44"] o ["38", "39", "40", "41", "42", "43"]). Normaliza caracteres especiales o fuentes estilizadas.
|
||||
- "supplierPrice": Precio de compra mayorista como número entero en COP (ej: 75000, 80000, 100000, 120000). Presta mucha atención a símbolos como 💸, 💰, $, 75.000, 75k, etc.
|
||||
- "category": Categoría ("Sneakers / Colección", "Urbano / Casual", "Running / Deportivo", etc.).
|
||||
const imagePayload = extractBase64Payload(imageBase64OrUrl);
|
||||
|
||||
Mensaje del grupo:
|
||||
const forbiddenTermsStr = extraForbidden.filter(Boolean).join(', ');
|
||||
const forbiddenClause = forbiddenTermsStr
|
||||
? `Términos específicos PROHIBIDOS a omitir absolutamente: "${forbiddenTermsStr}".`
|
||||
: '';
|
||||
|
||||
const systemPrompt = `Eres un experto mundial en reconocimiento y categorización de calzado, marroquinería y artículos urbanos para e-commerce en Colombia.
|
||||
Tu misión es identificar con MÁXIMA PRECISIÓN la marca, el modelo exacto y la CATEGORÍA del producto.
|
||||
|
||||
CATEGORÍAS OFICIALES OBLIGATORIAS:
|
||||
Debes clasificar el producto en EXACTAMENTE una de las siguientes categorías en el campo "category":
|
||||
- "Zapatillas": Zapatillas deportivas, tenis urbanos, sneakers, calzado deportivo o casual.
|
||||
- "Chanclas": Chanclas, sandalias, slides, crocs, zuecos, pantuflas, ojotas.
|
||||
- "Bolsos": Bolsos para dama, carteras, tote bags, carteras de mano o de hombro.
|
||||
- "Maletines": Maletines ejecutivos, mochilas, morrales, backpacks, tulas deportivas.
|
||||
- "Carrieles": Carrieles tradicionales o modernos, bandoleras, riñoneras, pecheras, canguros, crossbody.
|
||||
- "Gorras": Gorras, gorros, caps, sombreros.
|
||||
- "Ropa": Camisetas, buzos, hoodies, chaquetas, sudaderas, shorts, pantalones.
|
||||
- "Accesorios": Billeteras, cinturones, gafas, relojes.
|
||||
|
||||
⚠️ REGLA CRÍTICA DE CERO PROVEEDORES / CERO GRUPOS / CERO COSTOS (ESTRICTO):
|
||||
- NI EN EL TÍTULO ("name") NI EN NINGÚN TEXTO COMERCIAL DE LA PUBLICACIÓN SE DEBE INCLUIR EL NOMBRE DEL GRUPO DE WHATSAPP, CANAL, PROVEEDOR, BODEGA MAYORISTA, DISTRIBUIDOR, NI EL COSTO DEL PRODUCTO (PRECIO DE COMPRA / MAYORISTA).
|
||||
- El costo del producto SOLO debe extraerse en el campo numérico interno "supplierPrice" para el cálculo de margen, pero JAMÁS debe figurar en el título ("name") ni en ninguna descripción de la publicación.
|
||||
- El campo "name" debe ser ÚNICAMENTE el modelo limpio y comercial del producto (ej: "Adidas Samba OG Blanco Negro", "Nike Air Force 1 07 All White", "Yeezy Slide Onyx", "Carriel de Cuero Clásico Jericó", "Morral Deportivo Nike Brasilia", "Bolso Tote Monograma", "Gorra New Era NY Yankees").
|
||||
- Si el mensaje contiene nombres de grupos o proveedores (${forbiddenClause} o palabras como "Bodega", "Mayorista", "Distribuidora", "Proveedor", "Jovic", "CC Mío", "Team Emporio", etc.), o menciones de costo/precios de bodega, DEBES OMITIRLOS Y ELIMINARLOS POR COMPLETO DEL TÍTULO Y DE TODO TEXTO.
|
||||
|
||||
${
|
||||
imagePayload
|
||||
? `🔴 INSTRUCCIÓN DE VISIÓN CRÍTICA:
|
||||
Observa con atención la IMAGEN del producto y detecta los logos visuales de la marca y el tipo de artículo:
|
||||
- CALZADO / ZAPATILLAS: Zapatillas deportivas, sneakers (Nike Swoosh, Adidas 3 Rayas, Jordan Jumpman, NB, Puma, etc.).
|
||||
- CHANCLAS / SANDALIAS: Chanclas abiertas, slides de una pieza, zuecos Crocs, Yeezy slides.
|
||||
- CARRIELES / BANDOLERAS: Bolsos pequeños cruzados al pecho o cintura (crossbody, riñonera, carriel de cuero).
|
||||
- MALETINES / MORRALES: Mochilas de dos cargaderas para espalda, maletines para portátil o tulas.
|
||||
- BOLSOS: Bolsos de mano o de hombro para dama o unisex.
|
||||
- GORRAS: Gorras con visera plana o curva, sombreros.
|
||||
`
|
||||
: `Analiza el texto y las palabras clave del mensaje mayorista para identificar la categoría, la marca y el modelo exacto.`
|
||||
}
|
||||
|
||||
Extrae los siguientes campos en formato JSON estricto:
|
||||
- "brand": La marca ("Adidas", "Nike", "Jordan", "New Balance", "On Running", "Puma", "Asics", "Vans", "Converse", "Crocs", "Tommy Hilfiger", "Lacoste", "Gucci", "Louis Vuitton", "Totto", "Vélez", etc.). Si no puedes identificar ninguna marca con certeza, responde "Urbano / Variado".
|
||||
- "name": Nombre comercial completo y limpio del modelo. ¡CERO NOMBRES DE GRUPOS O PROVEEDORES!
|
||||
- "category": Exactamente una de: "Zapatillas" | "Chanclas" | "Bolsos" | "Maletines" | "Carrieles" | "Gorras" | "Ropa" | "Accesorios".
|
||||
- "colorName": Nombre descriptivo del color o colorway.
|
||||
- "sizes": Array de tallas disponibles (para calzado números ej: ["37","38"], para accesorios ["Talla Única"], para ropa ["S","M","L"]).
|
||||
- "supplierPrice": Precio de compra mayorista como número entero en COP. Analiza símbolos como 💸, 💰, $, 75k, 85.000, etc.
|
||||
|
||||
Texto del mensaje de WhatsApp:
|
||||
"""
|
||||
${text}
|
||||
"""
|
||||
|
||||
Responde ÚNICAMENTE con el objeto JSON sin bloques de código markdown ni texto adicional.`;
|
||||
|
||||
const response = await ai.models.generateContent({
|
||||
model: 'gemini-2.5-flash',
|
||||
contents: prompt,
|
||||
});
|
||||
const contents: any[] = [];
|
||||
if (imagePayload) {
|
||||
contents.push({
|
||||
inlineData: {
|
||||
mimeType: imagePayload.mimeType,
|
||||
data: imagePayload.data,
|
||||
},
|
||||
});
|
||||
}
|
||||
contents.push(systemPrompt);
|
||||
|
||||
// Ejecutar llamada a Gemini 3.5 Flash (con fallback a gemini-3.6-flash si es necesario)
|
||||
let outputText = '';
|
||||
try {
|
||||
const response = await ai.models.generateContent({
|
||||
model: 'gemini-3.5-flash',
|
||||
contents,
|
||||
});
|
||||
outputText = response.text || '';
|
||||
} catch (primaryErr: any) {
|
||||
console.warn('Reintentando con gemini-3.6-flash:', primaryErr?.message);
|
||||
const fallbackResponse = await ai.models.generateContent({
|
||||
model: 'gemini-3.6-flash',
|
||||
contents,
|
||||
});
|
||||
outputText = fallbackResponse.text || '';
|
||||
}
|
||||
|
||||
const outputText = response.text || '';
|
||||
const cleanJson = outputText.replace(/```json|```/gi, '').trim();
|
||||
const parsed = JSON.parse(cleanJson);
|
||||
|
||||
// Validación y categorización unificada
|
||||
const catDetection = detectCategory(`${parsed.name || ''} ${text}`, parsed.category);
|
||||
const finalCategory = catDetection.category;
|
||||
|
||||
// Validación y saneamiento del precio
|
||||
const supplierPrice =
|
||||
typeof parsed.supplierPrice === 'number' && parsed.supplierPrice >= 40000
|
||||
typeof parsed.supplierPrice === 'number' && parsed.supplierPrice >= 20000
|
||||
? parsed.supplierPrice
|
||||
: extractSupplierPrice(text);
|
||||
|
||||
// Validación de tallas dependiente de categoría
|
||||
const sizes =
|
||||
Array.isArray(parsed.sizes) && parsed.sizes.length > 0
|
||||
? parsed.sizes.map(String)
|
||||
: extractSizes(text);
|
||||
: extractSizes(text, finalCategory);
|
||||
|
||||
let brand = parsed.brand || 'Nike';
|
||||
if (/samba|campus|bad bunny|forum|gazelle/i.test(parsed.name || text)) brand = 'Adidas';
|
||||
if (/9060|550|1906/i.test(parsed.name || text)) brand = 'New Balance';
|
||||
// Validación de marca: Si la IA no devolvió marca o vino vacía, usamos el detector maestro
|
||||
let brand = parsed.brand?.trim();
|
||||
if (!brand || brand.toLowerCase() === 'desconocida' || brand.toLowerCase() === 'generica') {
|
||||
const fallbackBrand = detectBrandFromText(parsed.name || text);
|
||||
brand = fallbackBrand.brand;
|
||||
}
|
||||
|
||||
const rawName = parsed.name || cleanProductName(text, brand, extraForbidden);
|
||||
const cleanName = sanitizePublicationTitle(rawName, extraForbidden, brand);
|
||||
const secondaryCheck = detectBrandFromText(`${parsed.name || ''} ${text}`);
|
||||
if (secondaryCheck.confidence >= 0.95 && secondaryCheck.brand !== 'Urbano / Variado') {
|
||||
brand = secondaryCheck.brand;
|
||||
}
|
||||
|
||||
const rawExtractedName =
|
||||
parsed.name ||
|
||||
secondaryCheck.modelName ||
|
||||
cleanProductName(text, brand, extraForbidden);
|
||||
|
||||
const cleanName = sanitizePublicationTitle(rawExtractedName, extraForbidden, brand);
|
||||
const proof = getProductSocialProof({ name: cleanName, brand });
|
||||
|
||||
return {
|
||||
name: cleanName,
|
||||
brand,
|
||||
colorName: parsed.colorName,
|
||||
sizes,
|
||||
supplierPrice,
|
||||
suggestedRetailPrice: calculateRetailPrice(supplierPrice, 35, 30000),
|
||||
category: parsed.category || 'Sneakers',
|
||||
confidence: 0.98,
|
||||
category: finalCategory,
|
||||
confidence: imagePayload ? 0.99 : secondaryCheck.confidence || 0.94,
|
||||
rating: proof.rating,
|
||||
reviewsCount: proof.reviewsCount,
|
||||
salesCount: proof.salesCount,
|
||||
originBadge: proof.badge,
|
||||
};
|
||||
} catch (err) {
|
||||
console.warn('Fallo llamada Gemini, utilizando extractor regex de alta precisión:', err);
|
||||
console.warn('Error en llamada Gemini, utilizando extractor maestro de alta precisión:', err);
|
||||
return fallbackExtract();
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user