Listado-mercado / app.py
EdwinV08's picture
Update app.py
64d9431 verified
"""
🛒 Lista de Compras Inteligente con IA
Aplicación web para gestionar listas de compras con clasificación automática
usando inteligencia artificial.
Autor: Edwin Villa
Licencia: MIT
"""
import torch
from sentence_transformers import SentenceTransformer
import numpy as np
from typing import List, Dict, Tuple
import gradio as gr
from datetime import datetime
import pandas as pd
import json
# ========================= CLASIFICADOR =========================
class ProductClassifier:
"""Clasificador de productos usando embeddings semánticos"""
def __init__(self):
print("🔄 Cargando modelo de IA...")
self.model = SentenceTransformer('hiiamsid/sentence_similarity_spanish_es')
self.categories = {
"Lácteos": [
"leche", "yogurt", "queso", "mantequilla", "crema de leche",
"kumis", "arequipe", "leche condensada", "queso crema", "natilla"
],
"Frutas y Verduras": [
"manzana", "banana", "naranja", "tomate", "lechuga", "zanahoria",
"papa", "cebolla", "aguacate", "limón", "plátano", "yuca", "cilantro",
"arveja", "habichuela", "brócoli", "espinaca", "fresa", "mango", "piña"
],
"Cárnicos": [
"pollo", "carne de res", "cerdo", "pescado", "salchicha", "jamón",
"chorizo", "pechuga", "costilla", "lomo", "atún", "salmón", "molida"
],
"Harinas y Cereales": [
"pan", "arroz", "pasta", "avena", "cereal", "harina", "tortilla",
"arepa", "galletas", "tostadas", "quinoa", "lentejas", "frijoles"
],
"Aseo Personal": [
"jabón", "shampoo", "crema dental", "desodorante", "papel higiénico",
"toallas sanitarias", "cuchillas de afeitar", "enjuague bucal", "cepillo"
],
"Limpieza Hogar": [
"detergente", "suavizante", "cloro", "limpiador", "escoba", "trapeador",
"esponja", "jabón de platos", "ambientador", "desinfectante"
],
"Mascotas": [
"comida para perro", "comida para gato", "arena para gato",
"snacks para mascotas", "shampoo para mascotas", "juguetes para mascotas"
],
"Bebidas": [
"agua", "jugo", "gaseosa", "café", "té", "cerveza", "vino",
"energizante", "agua con gas", "chocolate en polvo", "cola"
],
"Panadería": [
"pan tajado", "pan integral", "croissant", "ponqué", "torta",
"donas", "muffin", "pan dulce", "pandebono"
],
"Snacks y Dulces": [
"papas fritas", "chocolatina", "caramelos", "chicles", "maní",
"doritos", "galletas dulces", "helado", "gomitas", "cheetos"
]
}
# Pre-calcular embeddings de categorías
self.category_embeddings = {}
for category, products in self.categories.items():
embeddings = self.model.encode(products)
self.category_embeddings[category] = np.mean(embeddings, axis=0)
print("✅ Modelo cargado correctamente")
def classify(self, product: str) -> str:
"""Clasifica un producto en una categoría"""
product_lower = product.lower().strip()
# Búsqueda exacta primero
for category, products in self.categories.items():
for prod in products:
if prod in product_lower or product_lower in prod:
return category
# Similaridad semántica
product_embedding = self.model.encode([product_lower])[0]
similarities = {}
for category, cat_embedding in self.category_embeddings.items():
similarity = np.dot(product_embedding, cat_embedding) / (
np.linalg.norm(product_embedding) * np.linalg.norm(cat_embedding)
)
similarities[category] = similarity
best_category = max(similarities, key=similarities.get)
if similarities[best_category] < 0.3:
return "Otros"
return best_category
# ========================= APLICACIÓN =========================
class ShoppingListApp:
"""Aplicación principal de lista de compras"""
def __init__(self):
self.classifier = ProductClassifier()
self.shopping_list = []
self.next_id = 0
def add_product(self, product_name: str, quantity: str):
"""Agrega un producto a la lista"""
if not product_name.strip():
return self.get_dataframe(), self.get_filtered_dataframe(""), self.get_list_display(), "⚠️ Por favor ingresa un producto"
category = self.classifier.classify(product_name)
self.shopping_list.append({
"ID": self.next_id,
"Producto": product_name.strip(),
"Cantidad": quantity.strip() if quantity.strip() else "1",
"Categoría": category,
"Comprado": False
})
self.next_id += 1
message = f"✅ '{product_name}' agregado a {category}"
return self.get_dataframe(), self.get_filtered_dataframe(""), self.get_list_display(), message
def update_product(self, selected_data, new_product, new_quantity, new_category):
"""Actualiza un producto existente"""
if selected_data is None or len(selected_data) == 0:
return self.get_dataframe(), self.get_filtered_dataframe(""), self.get_list_display(), "⚠️ Por favor selecciona un producto"
product_id = selected_data.iloc[0]['ID']
for item in self.shopping_list:
if item["ID"] == product_id:
if new_product.strip():
item["Producto"] = new_product.strip()
if new_quantity.strip():
item["Cantidad"] = new_quantity.strip()
if new_category and new_category != "Sin cambiar":
item["Categoría"] = new_category
return self.get_dataframe(), self.get_filtered_dataframe(""), self.get_list_display(), f"✏️ Producto actualizado"
return self.get_dataframe(), self.get_filtered_dataframe(""), self.get_list_display(), "❌ Error al actualizar"
def delete_selected(self, selected_data):
"""Elimina el producto seleccionado"""
if selected_data is None or len(selected_data) == 0:
return self.get_dataframe(), self.get_filtered_dataframe(""), self.get_list_display(), "⚠️ Por favor selecciona un producto"
product_id = selected_data.iloc[0]['ID']
product_name = selected_data.iloc[0]['Producto']
self.shopping_list = [item for item in self.shopping_list if item["ID"] != product_id]
return self.get_dataframe(), self.get_filtered_dataframe(""), self.get_list_display(), f"🗑️ '{product_name}' eliminado"
def toggle_purchased(self, selected_data):
"""Marca/desmarca como comprado"""
if selected_data is None or len(selected_data) == 0:
return self.get_dataframe(), self.get_filtered_dataframe(""), self.get_list_display(), "⚠️ Por favor selecciona un producto"
product_id = selected_data.iloc[0]['ID']
for item in self.shopping_list:
if item["ID"] == product_id:
item["Comprado"] = not item["Comprado"]
status = "comprado" if item["Comprado"] else "pendiente"
return self.get_dataframe(), self.get_filtered_dataframe(""), self.get_list_display(), f"✓ Producto marcado como {status}"
return self.get_dataframe(), self.get_filtered_dataframe(""), self.get_list_display(), ""
def clear_list(self):
"""Limpia toda la lista"""
self.shopping_list = []
return self.get_dataframe(), self.get_filtered_dataframe(""), self.get_list_display(), "🗑️ Lista limpiada"
def filter_by_category(self, category_filter):
"""Filtra productos por categoría"""
return self.get_filtered_dataframe(category_filter)
def search_products(self, search_term):
"""Busca productos por nombre"""
if not search_term.strip():
return self.get_dataframe()
search_lower = search_term.lower().strip()
filtered = [item for item in self.shopping_list
if search_lower in item["Producto"].lower()]
if not filtered:
return pd.DataFrame(columns=["ID", "Producto", "Cantidad", "Categoría", "Comprado"])
df = pd.DataFrame(filtered)
df['Comprado'] = df['Comprado'].apply(lambda x: '✓' if x else '○')
return df
def get_dataframe(self):
"""Retorna DataFrame completo"""
if not self.shopping_list:
return pd.DataFrame(columns=["ID", "Producto", "Cantidad", "Categoría", "Comprado"])
df = pd.DataFrame(self.shopping_list)
df['Comprado'] = df['Comprado'].apply(lambda x: '✓' if x else '○')
return df
def get_filtered_dataframe(self, category_filter):
"""Retorna DataFrame filtrado por categoría"""
if not self.shopping_list:
return pd.DataFrame(columns=["ID", "Producto", "Cantidad", "Categoría", "Comprado"])
if category_filter == "Todas" or not category_filter:
return self.get_dataframe()
filtered = [item for item in self.shopping_list if item["Categoría"] == category_filter]
if not filtered:
return pd.DataFrame(columns=["ID", "Producto", "Cantidad", "Categoría", "Comprado"])
df = pd.DataFrame(filtered)
df['Comprado'] = df['Comprado'].apply(lambda x: '✓' if x else '○')
return df
def get_list_display(self):
"""Genera HTML de vista previa organizada"""
if not self.shopping_list:
return "<div style='text-align: center; padding: 40px; color: #666;'><h3>📝 Tu lista está vacía</h3><p>Comienza agregando productos</p></div>"
categories = {}
for item in self.shopping_list:
cat = item["Categoría"]
if cat not in categories:
categories[cat] = []
categories[cat].append(item)
html = "<div style='font-family: Arial, sans-serif;'>"
total_items = len(self.shopping_list)
purchased_items = sum(1 for item in self.shopping_list if item["Comprado"])
progress = (purchased_items / total_items * 100) if total_items > 0 else 0
html += f"""
<div style='background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white; padding: 20px; border-radius: 10px; margin-bottom: 20px;'>
<h2 style='margin: 0 0 10px 0;'>🛒 Mi Lista de Compras</h2>
<div style='display: flex; justify-content: space-around; margin-top: 15px;'>
<div>
<div style='font-size: 24px; font-weight: bold;'>{total_items}</div>
<div style='font-size: 12px;'>Productos</div>
</div>
<div>
<div style='font-size: 24px; font-weight: bold;'>{purchased_items}</div>
<div style='font-size: 12px;'>Comprados</div>
</div>
<div>
<div style='font-size: 24px; font-weight: bold;'>{progress:.0f}%</div>
<div style='font-size: 12px;'>Progreso</div>
</div>
</div>
<div style='background: rgba(255,255,255,0.2); height: 10px; border-radius: 5px; margin-top: 15px;'>
<div style='background: white; height: 100%; width: {progress}%; border-radius: 5px;'></div>
</div>
</div>
"""
category_colors = {
"Lácteos": "#4A90E2",
"Frutas y Verduras": "#7ED321",
"Cárnicos": "#E24A4A",
"Harinas y Cereales": "#F5A623",
"Aseo Personal": "#BD10E0",
"Limpieza Hogar": "#50E3C2",
"Mascotas": "#B8E986",
"Bebidas": "#4A90E2",
"Panadería": "#F8E71C",
"Snacks y Dulces": "#FF6B9D",
"Otros": "#9013FE"
}
for category in sorted(categories.keys()):
items = categories[category]
color = category_colors.get(category, "#999")
html += f"""
<div style='margin-bottom: 20px; border: 2px solid {color}; border-radius: 10px; overflow: hidden;'>
<div style='background: {color}; color: white; padding: 10px 15px; font-weight: bold;'>
{category} ({len(items)})
</div>
<div style='padding: 10px;'>
"""
for item in items:
checked = "✓" if item["Comprado"] else "○"
style = "text-decoration: line-through; opacity: 0.6;" if item["Comprado"] else ""
bg_color = "#f0f0f0" if item["Comprado"] else "white"
html += f"""
<div style='background: {bg_color}; padding: 12px; margin: 8px 0; border-radius: 8px;
border: 1px solid #e0e0e0; {style}'>
<div style='display: flex; align-items: center; justify-content: space-between;'>
<div style='display: flex; align-items: center; flex: 1;'>
<span style='font-size: 24px; margin-right: 10px;'>{checked}</span>
<div>
<div style='font-weight: 500; font-size: 16px;'>{item["Producto"]}</div>
<div style='color: #666; font-size: 14px;'>Cantidad: {item["Cantidad"]}</div>
</div>
</div>
<div style='color: #999; font-size: 12px; background: #eee; padding: 4px 8px; border-radius: 4px;'>
ID: {item["ID"]}
</div>
</div>
</div>
"""
html += "</div></div>"
html += "</div>"
return html
def export_list(self):
"""Exporta la lista a formato texto"""
if not self.shopping_list:
return "La lista está vacía"
text = f"🛒 LISTA DE COMPRAS - {datetime.now().strftime('%d/%m/%Y %H:%M')}\n"
text += "=" * 50 + "\n\n"
categories = {}
for item in self.shopping_list:
cat = item["Categoría"]
if cat not in categories:
categories[cat] = []
categories[cat].append(item)
for category in sorted(categories.keys()):
text += f"\n📦 {category.upper()}\n"
text += "-" * 30 + "\n"
for item in categories[category]:
status = "✓" if item["Comprado"] else "○"
text += f"{status} {item['Producto']} - Cantidad: {item['Cantidad']}\n"
total = len(self.shopping_list)
purchased = sum(1 for item in self.shopping_list if item["Comprado"])
text += f"\n\n📊 RESUMEN: {purchased}/{total} productos comprados ({purchased/total*100:.0f}%)\n"
return text
def save_list_json(self):
"""Guarda la lista en formato JSON"""
if not self.shopping_list:
return "La lista está vacía", ""
data = {
"fecha": datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
"productos": self.shopping_list
}
json_str = json.dumps(data, indent=2, ensure_ascii=False)
filename = f"lista_compras_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
return f"✅ Lista guardada como {filename}", json_str
def load_list_json(self, file_content):
"""Carga una lista desde JSON"""
try:
# Verificar si el contenido está vacío
if not file_content or not file_content.strip():
return (self.get_dataframe(), self.get_filtered_dataframe(""),
self.get_list_display(), "⚠️ Por favor pega el contenido JSON primero")
# Intentar parsear el JSON
data = json.loads(file_content.strip())
# Verificar estructura
if "productos" not in data:
return (self.get_dataframe(), self.get_filtered_dataframe(""),
self.get_list_display(), "❌ El JSON no tiene la estructura correcta (falta 'productos')")
self.shopping_list = data["productos"]
# Actualizar next_id
if self.shopping_list:
self.next_id = max(item["ID"] for item in self.shopping_list) + 1
else:
return (self.get_dataframe(), self.get_filtered_dataframe(""),
self.get_list_display(), "⚠️ El archivo JSON está vacío")
return (self.get_dataframe(), self.get_filtered_dataframe(""),
self.get_list_display(), f"✅ Lista cargada con {len(self.shopping_list)} productos")
except json.JSONDecodeError as e:
return (self.get_dataframe(), self.get_filtered_dataframe(""),
self.get_list_display(), f"❌ JSON inválido: {str(e)}")
except KeyError as e:
return (self.get_dataframe(), self.get_filtered_dataframe(""),
self.get_list_display(), f"❌ Falta el campo: {str(e)}")
except Exception as e:
return (self.get_dataframe(), self.get_filtered_dataframe(""),
self.get_list_display(), f"❌ Error al cargar: {str(e)}")
def load_list_from_file(self, file_path):
"""Carga una lista desde un archivo JSON"""
try:
if file_path is None:
return (self.get_dataframe(), self.get_filtered_dataframe(""),
self.get_list_display(), "⚠️ Por favor selecciona un archivo primero")
# Leer el archivo
with open(file_path, 'r', encoding='utf-8') as f:
file_content = f.read()
# Usar la función de carga existente
return self.load_list_json(file_content)
except FileNotFoundError:
return (self.get_dataframe(), self.get_filtered_dataframe(""),
self.get_list_display(), "❌ Archivo no encontrado")
except Exception as e:
return (self.get_dataframe(), self.get_filtered_dataframe(""),
self.get_list_display(), f"❌ Error al leer archivo: {str(e)}")
# ========================= INTERFAZ GRADIO =========================
app = ShoppingListApp()
category_options = ["Sin cambiar", "Lácteos", "Frutas y Verduras", "Cárnicos",
"Harinas y Cereales", "Aseo Personal", "Limpieza Hogar",
"Mascotas", "Bebidas", "Panadería", "Snacks y Dulces", "Otros"]
filter_options = ["Todas"] + category_options[1:]
with gr.Blocks(theme=gr.themes.Soft(), title="🛒 Lista de Compras Inteligente",
css=".gradio-container {max-width: 1400px !important}") as demo:
gr.Markdown("""
# 🛒 Lista de Compras Inteligente con IA
### Agrega productos, edítalos y organiza tu lista de compras con clasificación automática por IA
""")
with gr.Row():
# COLUMNA IZQUIERDA
with gr.Column(scale=1):
gr.Markdown("### ➕ Agregar Nuevo Producto")
product_input = gr.Textbox(
label="Producto",
placeholder="Ej: leche, manzanas, detergente...",
)
quantity_input = gr.Textbox(
label="Cantidad",
placeholder="Ej: 2, 1kg, 500g...",
value="1"
)
add_btn = gr.Button("➕ Agregar a la Lista", variant="primary", size="lg")
status_msg = gr.Textbox(label="Estado", interactive=False, show_label=False)
gr.Markdown("---")
gr.Markdown("### 🔍 Buscar y Filtrar")
search_box = gr.Textbox(
label="Buscar producto",
placeholder="Escribe para buscar..."
)
category_filter = gr.Dropdown(
choices=filter_options,
value="Todas",
label="Filtrar por categoría"
)
gr.Markdown("---")
gr.Markdown("### 📋 Productos")
gr.Markdown("*Haz clic en una fila para seleccionarla*")
products_table = gr.Dataframe(
value=app.get_dataframe(),
label="Lista de Productos",
interactive=False,
row_count=10,
col_count=(5, "fixed"),
wrap=True
)
gr.Markdown("---")
gr.Markdown("### ✏️ Editar Producto Seleccionado")
with gr.Row():
edit_product = gr.Textbox(
label="Nuevo nombre",
placeholder="Dejar vacío para no cambiar"
)
edit_quantity = gr.Textbox(
label="Nueva cantidad",
placeholder="Dejar vacío para no cambiar"
)
edit_category = gr.Dropdown(
choices=category_options,
value="Sin cambiar",
label="Nueva categoría"
)
with gr.Row():
update_btn = gr.Button("💾 Guardar Cambios", variant="primary")
toggle_btn = gr.Button("✓ Marcar/Desmarcar")
with gr.Row():
delete_btn = gr.Button("🗑️ Eliminar", variant="stop")
clear_btn = gr.Button("🗑️ Limpiar Todo", variant="stop")
# COLUMNA DERECHA
with gr.Column(scale=1):
gr.Markdown("### 👁️ Vista Previa por Categorías")
list_display = gr.HTML(value=app.get_list_display())
gr.Markdown("---")
gr.Markdown("### 💾 Guardar y Exportar")
with gr.Row():
export_btn = gr.Button("📥 Exportar como Texto", variant="secondary")
save_json_btn = gr.Button("💾 Guardar como JSON", variant="secondary")
export_output = gr.Textbox(label="Salida", lines=12, visible=False)
gr.Markdown("### 📂 Cargar Lista Guardada")
gr.Markdown("**Opción 1: Pegar JSON**")
load_file = gr.Textbox(
label="Contenido JSON",
placeholder='Pega aquí el JSON completo que guardaste, ejemplo:\n{\n "fecha": "2024-12-13 10:30:00",\n "productos": [...]\n}',
lines=5
)
load_btn = gr.Button("📂 Cargar desde Texto", variant="secondary")
gr.Markdown("**Opción 2: Subir Archivo**")
load_file_upload = gr.File(
label="Subir archivo JSON",
file_types=[".json"],
type="filepath"
)
load_file_btn = gr.Button("📂 Cargar desde Archivo", variant="secondary")
# Event handlers
add_btn.click(
fn=app.add_product,
inputs=[product_input, quantity_input],
outputs=[products_table, products_table, list_display, status_msg]
).then(
fn=lambda: ("", "1"),
outputs=[product_input, quantity_input]
)
product_input.submit(
fn=app.add_product,
inputs=[product_input, quantity_input],
outputs=[products_table, products_table, list_display, status_msg]
).then(
fn=lambda: ("", "1"),
outputs=[product_input, quantity_input]
)
search_box.change(
fn=app.search_products,
inputs=[search_box],
outputs=[products_table]
)
category_filter.change(
fn=app.filter_by_category,
inputs=[category_filter],
outputs=[products_table]
)
update_btn.click(
fn=app.update_product,
inputs=[products_table, edit_product, edit_quantity, edit_category],
outputs=[products_table, products_table, list_display, status_msg]
).then(
fn=lambda: ("", "", "Sin cambiar"),
outputs=[edit_product, edit_quantity, edit_category]
)
toggle_btn.click(
fn=app.toggle_purchased,
inputs=[products_table],
outputs=[products_table, products_table, list_display, status_msg]
)
delete_btn.click(
fn=app.delete_selected,
inputs=[products_table],
outputs=[products_table, products_table, list_display, status_msg]
)
clear_btn.click(
fn=app.clear_list,
outputs=[products_table, products_table, list_display, status_msg]
)
export_btn.click(
fn=app.export_list,
outputs=export_output
).then(
fn=lambda: gr.update(visible=True),
outputs=export_output
)
save_json_btn.click(
fn=app.save_list_json,
outputs=[status_msg, export_output]
).then(
fn=lambda: gr.update(visible=True),
outputs=export_output
)
load_btn.click(
fn=app.load_list_json,
inputs=[load_file],
outputs=[products_table, products_table, list_display, status_msg]
)
load_file_btn.click(
fn=app.load_list_from_file,
inputs=[load_file_upload],
outputs=[products_table, products_table, list_display, status_msg]
)
if __name__ == "__main__":
print("🚀 Iniciando aplicación...")
demo.launch()