Spaces:
Sleeping
Sleeping
Upload 12 files
Browse files- .gitattributes +1 -0
- paginas/__init__.py +0 -0
- paginas/conexionMysql.py +24 -0
- paginas/conexionTest.py +1 -0
- paginas/dashboard.py +813 -0
- paginas/dashboardDemo.py +916 -0
- paginas/demo.py +27 -0
- paginas/demokaleido.py +5 -0
- paginas/images/Logo dashboard.png +3 -0
- paginas/images/Logo general.png +0 -0
- paginas/images/Logo.png +0 -0
- paginas/login.py +59 -0
- paginas/userManagement.py +32 -0
.gitattributes
CHANGED
|
@@ -34,3 +34,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
images/Logo[[:space:]]dashboard.png filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
images/Logo[[:space:]]dashboard.png filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
paginas/images/Logo[[:space:]]dashboard.png filter=lfs diff=lfs merge=lfs -text
|
paginas/__init__.py
ADDED
|
File without changes
|
paginas/conexionMysql.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from contextlib import contextmanager
|
| 2 |
+
import MySQLdb
|
| 3 |
+
import os
|
| 4 |
+
from dotenv import load_dotenv
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
load_dotenv()
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
@contextmanager
|
| 12 |
+
def get_db_connection():
|
| 13 |
+
connection = MySQLdb.connect(
|
| 14 |
+
host=os.environ["DB_HOST"],
|
| 15 |
+
port=int(os.environ["DB_PORT"]),
|
| 16 |
+
user=os.environ["DB_USER"],
|
| 17 |
+
passwd=os.environ["DB_PASSWORD"],
|
| 18 |
+
db=os.environ["DB_NAME"]
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
try:
|
| 22 |
+
yield connection
|
| 23 |
+
finally:
|
| 24 |
+
connection.close()
|
paginas/conexionTest.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
|
paginas/dashboard.py
ADDED
|
@@ -0,0 +1,813 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import plotly.express as px
|
| 4 |
+
import random
|
| 5 |
+
import time
|
| 6 |
+
import joblib
|
| 7 |
+
import os
|
| 8 |
+
import statsmodels
|
| 9 |
+
from dotenv import load_dotenv
|
| 10 |
+
import os
|
| 11 |
+
from groq import Groq
|
| 12 |
+
import html
|
| 13 |
+
from pydub import AudioSegment
|
| 14 |
+
import tempfile
|
| 15 |
+
from io import BytesIO
|
| 16 |
+
import tempfile
|
| 17 |
+
#from langchain.agents.agent_toolkits import create_csv_agent
|
| 18 |
+
#from langchain_groq import ChatGroq
|
| 19 |
+
# ===========================
|
| 20 |
+
# Función para generar datos ficticios
|
| 21 |
+
# ===========================
|
| 22 |
+
def generar_datos():
|
| 23 |
+
meses = [
|
| 24 |
+
"Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio",
|
| 25 |
+
"Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"
|
| 26 |
+
]
|
| 27 |
+
paises = ["México", "Colombia", "Argentina", "Chile", "Perú"]
|
| 28 |
+
data = [
|
| 29 |
+
{"mes": mes, "pais": pais, "Total": random.randint(100, 1000)}
|
| 30 |
+
for mes in meses for pais in paises
|
| 31 |
+
]
|
| 32 |
+
return pd.DataFrame(data), meses, paises
|
| 33 |
+
|
| 34 |
+
# ===========================
|
| 35 |
+
# Función para el dashboard principal
|
| 36 |
+
# ===========================
|
| 37 |
+
def mostrar_dashboard():
|
| 38 |
+
# Cargar variables desde el archivo .env
|
| 39 |
+
load_dotenv()
|
| 40 |
+
|
| 41 |
+
# Acceder a la clave
|
| 42 |
+
groq_key = os.getenv("GROQ_API_KEY")
|
| 43 |
+
client = Groq(api_key=groq_key)
|
| 44 |
+
|
| 45 |
+
dfDatos, meses, paises = generar_datos()
|
| 46 |
+
|
| 47 |
+
# Opciones del selectbox
|
| 48 |
+
lista_opciones = ['5 años', '3 años', '1 año', '5 meses']
|
| 49 |
+
|
| 50 |
+
# Mostrar barra lateral
|
| 51 |
+
mostrar_sidebar(client)
|
| 52 |
+
|
| 53 |
+
# Título principal
|
| 54 |
+
st.header(':bar_chart: Dashboard Sales')
|
| 55 |
+
|
| 56 |
+
# Mostrar métricas
|
| 57 |
+
#mostrar_metricas()
|
| 58 |
+
|
| 59 |
+
# Mostrar gráficos
|
| 60 |
+
mostrar_graficos(lista_opciones)
|
| 61 |
+
|
| 62 |
+
# ===========================
|
| 63 |
+
# Configuración inicial de la página
|
| 64 |
+
# ===========================
|
| 65 |
+
#def configurar_pagina():
|
| 66 |
+
#st.set_page_config(
|
| 67 |
+
# page_title="Dashboard Sales",
|
| 68 |
+
# page_icon=":smile:",
|
| 69 |
+
# layout="wide",
|
| 70 |
+
# initial_sidebar_state="expanded"
|
| 71 |
+
#)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
# ===========================
|
| 76 |
+
# Función para la barra lateral
|
| 77 |
+
# ===========================
|
| 78 |
+
def mostrar_sidebar(client):
|
| 79 |
+
sidebar_logo = r"paginas\images\Logo general.png"
|
| 80 |
+
main_body_logo = r"paginas\images\Logo.png"
|
| 81 |
+
sidebar_logo_dashboard = r"paginas\images\Logo dashboard.png"
|
| 82 |
+
|
| 83 |
+
st.logo(sidebar_logo, size="large", icon_image=main_body_logo)
|
| 84 |
+
|
| 85 |
+
st.sidebar.image(sidebar_logo_dashboard)
|
| 86 |
+
st.sidebar.title('🧠 GenAI Forecast')
|
| 87 |
+
|
| 88 |
+
loadCSV()
|
| 89 |
+
|
| 90 |
+
archivo_csv = "df_articles.csv"
|
| 91 |
+
chatBotProtech(client)
|
| 92 |
+
downloadCSV(archivo_csv)
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
# Mostrar la tabla solo si se ha subido un archivo válido
|
| 96 |
+
'''
|
| 97 |
+
if 'archivo_subido' in st.session_state and st.session_state.archivo_subido: # Verificamos si el archivo ha sido subido y es válido
|
| 98 |
+
st.sidebar.markdown("Vista previa del archivo CSV:")
|
| 99 |
+
# Usar st.dataframe() para que ocupe todo el ancho disponible
|
| 100 |
+
st.sidebar.dataframe(st.session_state.df_subido, use_container_width=True) # Mostrar la tabla con el archivo subido
|
| 101 |
+
'''
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
if st.sidebar.button("Cerrar Sesión"):
|
| 106 |
+
cerrar_sesion()
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
# ===========================
|
| 110 |
+
# Función para métricas principales
|
| 111 |
+
# ===========================
|
| 112 |
+
'''
|
| 113 |
+
def mostrar_metricas():
|
| 114 |
+
c1, c2, c3, c4, c5 = st.columns(5)
|
| 115 |
+
valores = [89, 78, 67, 56, 45]
|
| 116 |
+
for i, col in enumerate([c1, c2, c3, c4, c5]):
|
| 117 |
+
valor1 = valores[i]
|
| 118 |
+
valor2 = valor1 - 10 # Simulación de variación
|
| 119 |
+
variacion = valor1 - valor2
|
| 120 |
+
unidad = "unidades" if i < 4 else "%"
|
| 121 |
+
col.metric(f"Productos vendidos", f'{valor1:,.0f} {unidad}', f'{variacion:,.0f}')
|
| 122 |
+
'''
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
# Función para obtener los meses relevantes
|
| 126 |
+
def obtener_meses_relevantes(df):
|
| 127 |
+
# Extraemos los años y meses de la columna 'Date'
|
| 128 |
+
df['Year'] = pd.to_datetime(df['orddt']).dt.year
|
| 129 |
+
df['Month'] = pd.to_datetime(df['orddt']).dt.month
|
| 130 |
+
|
| 131 |
+
# Encontramos el primer y último año en el dataset
|
| 132 |
+
primer_ano = df['Year'].min()
|
| 133 |
+
ultimo_ano = df['Year'].max()
|
| 134 |
+
|
| 135 |
+
meses_relevantes = []
|
| 136 |
+
nombres_meses_relevantes = []
|
| 137 |
+
|
| 138 |
+
# Recorrer todos los años dentro del rango
|
| 139 |
+
for ano in range(primer_ano, ultimo_ano + 1):
|
| 140 |
+
for mes in [1, 4, 7, 10]: # Meses relevantes: enero (1), abril (4), julio (7), octubre (10)
|
| 141 |
+
if mes in df[df['Year'] == ano]['Month'].values:
|
| 142 |
+
# Obtener el nombre del mes
|
| 143 |
+
nombre_mes = pd.to_datetime(f"{ano}-{mes}-01").strftime('%B') # Mes en formato textual (Enero, Abril, etc.)
|
| 144 |
+
meses_relevantes.append(f"{nombre_mes}-{ano}")
|
| 145 |
+
nombres_meses_relevantes.append(f"{nombre_mes}-{ano}")
|
| 146 |
+
|
| 147 |
+
return meses_relevantes, nombres_meses_relevantes
|
| 148 |
+
|
| 149 |
+
# ===========================
|
| 150 |
+
# Función para gráficos
|
| 151 |
+
# ===========================
|
| 152 |
+
def mostrar_graficos(lista_opciones):
|
| 153 |
+
|
| 154 |
+
"""
|
| 155 |
+
c1, c2 = st.columns([20, 80])
|
| 156 |
+
|
| 157 |
+
with c1:
|
| 158 |
+
filtroAnios = st.selectbox('Año', options=lista_opciones)
|
| 159 |
+
|
| 160 |
+
with c2:
|
| 161 |
+
st.markdown("### :pushpin: Ventas actuales")
|
| 162 |
+
# Si hay un archivo válido subido
|
| 163 |
+
if "archivo_subido" in st.session_state and st.session_state.archivo_subido:
|
| 164 |
+
# Cargar datos del archivo subido
|
| 165 |
+
df = st.session_state.df_subido.copy()
|
| 166 |
+
df['Date'] = pd.to_datetime(df['Date'])
|
| 167 |
+
df['Mes-Año'] = df['Date'].dt.strftime('%B-%Y') # Formato deseado
|
| 168 |
+
df = df.sort_values('Date') # Ordenar por fecha
|
| 169 |
+
|
| 170 |
+
# Obtener los meses relevantes del dataset
|
| 171 |
+
meses_relevantes, nombres_meses_relevantes = obtener_meses_relevantes(df)
|
| 172 |
+
|
| 173 |
+
# Crear la gráfica
|
| 174 |
+
fig = px.line(
|
| 175 |
+
df,
|
| 176 |
+
x='Mes-Año',
|
| 177 |
+
y='Sale',
|
| 178 |
+
title='Ventas mensuales (Archivo Subido)',
|
| 179 |
+
labels={'Mes-Año': 'Mes-Año', 'Sale': 'Ventas'},
|
| 180 |
+
)
|
| 181 |
+
else:
|
| 182 |
+
# Datos por defecto
|
| 183 |
+
df = pd.DataFrame({
|
| 184 |
+
"Mes-Año": ["Enero-2024", "Febrero-2024", "Marzo-2024", "Abril-2024", "Mayo-2024", "Junio-2024", "Julio-2024", "Agosto-2024", "Septiembre-2024", "Octubre-2024", "Noviembre-2024", "Diciembre-2024"],
|
| 185 |
+
"Sale": [100, 150, 120, 200, 250, 220, 280, 300, 350, 400, 450, 500],
|
| 186 |
+
})
|
| 187 |
+
|
| 188 |
+
# Obtener los meses relevantes
|
| 189 |
+
meses_relevantes = ["Enero-2024", "Abril-2024", "Julio-2024", "Octubre-2024"]
|
| 190 |
+
nombres_meses_relevantes = ["Enero-2024", "Abril-2024", "Julio-2024", "Octubre-2024"]
|
| 191 |
+
|
| 192 |
+
# Crear la gráfica
|
| 193 |
+
fig = px.line(
|
| 194 |
+
df,
|
| 195 |
+
x='Mes-Año',
|
| 196 |
+
y='Sale',
|
| 197 |
+
title='Ventas mensuales (Datos por defecto)',
|
| 198 |
+
labels={'Mes-Año': 'Mes-Año', 'Sale': 'Ventas'},
|
| 199 |
+
line_shape='linear' # Línea continua
|
| 200 |
+
)
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
fig.update_xaxes(tickangle=-45) # Ajustar ángulo de etiquetas en X
|
| 204 |
+
|
| 205 |
+
# Mejorar el diseño de la gráfica
|
| 206 |
+
fig = mejorar_diseno_grafica(fig, meses_relevantes, nombres_meses_relevantes)
|
| 207 |
+
st.plotly_chart(fig, use_container_width=True) # Evita que ocupe todo el ancho
|
| 208 |
+
|
| 209 |
+
# Gráfica 2: Ventas actuales y proyectadas
|
| 210 |
+
st.markdown("### :chart_with_upwards_trend: Pronóstico")
|
| 211 |
+
mostrar_ventas_proyectadas(filtroAnios)
|
| 212 |
+
"""
|
| 213 |
+
if "archivo_subido" not in st.session_state or not st.session_state.archivo_subido:
|
| 214 |
+
st.warning("Por favor, sube un archivo CSV válido para visualizar los gráficos.")
|
| 215 |
+
return
|
| 216 |
+
|
| 217 |
+
df = st.session_state.df_subido.copy()
|
| 218 |
+
|
| 219 |
+
# Fila 1: 3 gráficas
|
| 220 |
+
col1, col2, col3 = st.columns(3)
|
| 221 |
+
with col1:
|
| 222 |
+
fig1 = px.histogram(df, x='sales', title='Distribución de Ventas')
|
| 223 |
+
st.plotly_chart(fig1, use_container_width=True)
|
| 224 |
+
|
| 225 |
+
with col2:
|
| 226 |
+
fig2 = px.box(df, x='segmt', y='sales', title='Ventas por Segmento')
|
| 227 |
+
st.plotly_chart(fig2, use_container_width=True)
|
| 228 |
+
|
| 229 |
+
with col3:
|
| 230 |
+
print("")
|
| 231 |
+
|
| 232 |
+
# Fila 2: 2 gráficas
|
| 233 |
+
col4, col5 = st.columns(2)
|
| 234 |
+
with col4:
|
| 235 |
+
fig4 = px.pie(df, names='categ', values='sales', title='Ventas por Categoría')
|
| 236 |
+
st.plotly_chart(fig4, use_container_width=True)
|
| 237 |
+
|
| 238 |
+
with col5:
|
| 239 |
+
|
| 240 |
+
# Agrupar por nombre de producto y sumar las ventas
|
| 241 |
+
top_productos = (
|
| 242 |
+
df.groupby('prdna')['sales']
|
| 243 |
+
.sum()
|
| 244 |
+
.sort_values(ascending=False)
|
| 245 |
+
.head(10)
|
| 246 |
+
.reset_index()
|
| 247 |
+
)
|
| 248 |
+
|
| 249 |
+
# Crear gráfica de barras horizontales
|
| 250 |
+
fig5 = px.bar(
|
| 251 |
+
top_productos,
|
| 252 |
+
x='sales',
|
| 253 |
+
y='prdna',
|
| 254 |
+
orientation='h',
|
| 255 |
+
title='Top 10 productos más vendidos',
|
| 256 |
+
labels={'sales': 'Ventas', 'prdna': 'Producto'},
|
| 257 |
+
color='sales',
|
| 258 |
+
color_continuous_scale='Blues'
|
| 259 |
+
)
|
| 260 |
+
|
| 261 |
+
fig5.update_layout(yaxis={'categoryorder': 'total ascending'})
|
| 262 |
+
st.plotly_chart(fig5, use_container_width=True)
|
| 263 |
+
|
| 264 |
+
col6, col7 = st.columns(2)
|
| 265 |
+
with col6:
|
| 266 |
+
# Fuera del sistema de columnas
|
| 267 |
+
tabla = df.pivot_table(index='state', columns='subct', values='sales', aggfunc='sum').fillna(0)
|
| 268 |
+
|
| 269 |
+
if not tabla.empty:
|
| 270 |
+
tabla = tabla.astype(float)
|
| 271 |
+
fig6 = px.imshow(
|
| 272 |
+
tabla.values,
|
| 273 |
+
labels=dict(x="Categoría", y="Estado", color="Ventas"),
|
| 274 |
+
x=tabla.columns,
|
| 275 |
+
y=tabla.index,
|
| 276 |
+
text_auto=True,
|
| 277 |
+
title="Mapa de Calor: Ventas por Estado y Categoría"
|
| 278 |
+
)
|
| 279 |
+
|
| 280 |
+
# Ajuste del tamaño de la figura
|
| 281 |
+
# fig6.update_layout(height=600, width=1000) # Puedes ajustar según tu pantalla
|
| 282 |
+
st.plotly_chart(fig6, use_container_width=True)
|
| 283 |
+
else:
|
| 284 |
+
st.warning("No hay datos suficientes para mostrar el mapa de calor.")
|
| 285 |
+
|
| 286 |
+
|
| 287 |
+
with col7:
|
| 288 |
+
fig7 = px.bar(df.groupby('state')['sales'].sum().reset_index(), x='state', y='sales', title='Ventas por Estado')
|
| 289 |
+
st.plotly_chart(fig7, use_container_width=True)
|
| 290 |
+
|
| 291 |
+
# -------------------------------
|
| 292 |
+
# CARGA DE CSV Y GUARDADO EN SESIÓN
|
| 293 |
+
# -------------------------------
|
| 294 |
+
|
| 295 |
+
def loadCSV():
|
| 296 |
+
columnas_requeridas = [
|
| 297 |
+
'rowid','ordid','orddt',
|
| 298 |
+
'shpdt','segmt','state',
|
| 299 |
+
'cono','prodid','categ',
|
| 300 |
+
'subct','prdna','sales'
|
| 301 |
+
]
|
| 302 |
+
with st.sidebar.expander("📁 Subir archivo"):
|
| 303 |
+
uploaded_file = st.file_uploader("Sube un archivo CSV:", type=["csv"], key="upload_csv")
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
if uploaded_file is not None:
|
| 307 |
+
# Reseteamos el estado de 'descargado' cuando se sube un archivo
|
| 308 |
+
st.session_state.descargado = False
|
| 309 |
+
st.session_state.archivo_subido = False # Reinicia el estado
|
| 310 |
+
try:
|
| 311 |
+
# Leer el archivo subido
|
| 312 |
+
df = pd.read_csv(uploaded_file)
|
| 313 |
+
|
| 314 |
+
# Verificar que las columnas estén presentes y en el orden correcto
|
| 315 |
+
if list(df.columns) == columnas_requeridas:
|
| 316 |
+
st.session_state.df_subido = df
|
| 317 |
+
st.session_state.archivo_subido = True
|
| 318 |
+
aviso = st.sidebar.success("✅ Archivo subido correctamente.")
|
| 319 |
+
time.sleep(3)
|
| 320 |
+
aviso.empty()
|
| 321 |
+
|
| 322 |
+
|
| 323 |
+
else:
|
| 324 |
+
st.session_state.archivo_subido = False
|
| 325 |
+
aviso = st.sidebar.error(f"El archivo no tiene las columnas requeridas: {columnas_requeridas}.")
|
| 326 |
+
time.sleep(3)
|
| 327 |
+
aviso.empty()
|
| 328 |
+
|
| 329 |
+
except Exception as e:
|
| 330 |
+
aviso = st.sidebar.error(f"Error al procesar el archivo: {str(e)}")
|
| 331 |
+
time.sleep(3)
|
| 332 |
+
aviso.empty()
|
| 333 |
+
|
| 334 |
+
# ===========================
|
| 335 |
+
# Función para descargar archivo CSV
|
| 336 |
+
# ===========================
|
| 337 |
+
def downloadCSV(archivo_csv):
|
| 338 |
+
# Verificamos si el archivo ya ha sido descargado
|
| 339 |
+
if 'descargado' not in st.session_state:
|
| 340 |
+
st.session_state.descargado = False
|
| 341 |
+
|
| 342 |
+
if not st.session_state.descargado:
|
| 343 |
+
|
| 344 |
+
# Usamos st.spinner para mostrar un estado de descarga inicial
|
| 345 |
+
#with st.spinner("Preparando archivo para descarga..."):
|
| 346 |
+
# time.sleep(2) # Simulación de preparación del archivo
|
| 347 |
+
# Botón de descarga
|
| 348 |
+
descarga = st.sidebar.download_button(
|
| 349 |
+
label="Descargar archivo CSV",
|
| 350 |
+
data=open(archivo_csv, "rb"),
|
| 351 |
+
file_name="ventas.csv",
|
| 352 |
+
mime="text/csv"
|
| 353 |
+
)
|
| 354 |
+
|
| 355 |
+
if descarga:
|
| 356 |
+
# Marcamos el archivo como descargado
|
| 357 |
+
st.session_state.descargado = True
|
| 358 |
+
aviso = st.sidebar.success("¡Descarga completada!")
|
| 359 |
+
# Hacer que el mensaje desaparezca después de 2 segundos
|
| 360 |
+
time.sleep(3)
|
| 361 |
+
aviso.empty()
|
| 362 |
+
else:
|
| 363 |
+
aviso = st.sidebar.success("¡Ya has descargado el archivo!")
|
| 364 |
+
time.sleep(3)
|
| 365 |
+
aviso.empty()
|
| 366 |
+
|
| 367 |
+
# -------------------------------
|
| 368 |
+
# CREACIÓN DE AGENTE CSV
|
| 369 |
+
# -------------------------------
|
| 370 |
+
'''
|
| 371 |
+
def createCSVAgent(client, df):
|
| 372 |
+
temp_csv = tempfile.NamedTemporaryFile(delete=False, suffix=".csv")
|
| 373 |
+
df.to_csv(temp_csv.name, index=False)
|
| 374 |
+
agent = create_csv_agent(
|
| 375 |
+
client,
|
| 376 |
+
temp_csv.name,
|
| 377 |
+
verbose=False,
|
| 378 |
+
handle_parsing_errors=True
|
| 379 |
+
)
|
| 380 |
+
return agent
|
| 381 |
+
'''
|
| 382 |
+
'''
|
| 383 |
+
def callCSVAgent(client, prompt):
|
| 384 |
+
if "df_csv" not in st.session_state:
|
| 385 |
+
return "No hay CSV cargado aún."
|
| 386 |
+
|
| 387 |
+
df = st.session_state.df_csv
|
| 388 |
+
agente = createCSVAgent(client, df)
|
| 389 |
+
|
| 390 |
+
try:
|
| 391 |
+
respuesta = agente.run(prompt)
|
| 392 |
+
except Exception as e:
|
| 393 |
+
respuesta = f"Error al procesar la pregunta: {e}"
|
| 394 |
+
|
| 395 |
+
return respuesta
|
| 396 |
+
'''
|
| 397 |
+
|
| 398 |
+
# -------------------------------
|
| 399 |
+
# FUNCIÓN PARA DETECTAR REFERENCIA AL CSV
|
| 400 |
+
# -------------------------------
|
| 401 |
+
def detectedReferenceToCSV(prompt: str) -> bool:
|
| 402 |
+
palabras_clave = ["csv", "archivo", "contenido cargado", "file", "dataset"]
|
| 403 |
+
prompt_lower = prompt.lower()
|
| 404 |
+
return any(palabra in prompt_lower for palabra in palabras_clave)
|
| 405 |
+
|
| 406 |
+
# ===========================
|
| 407 |
+
# Función para interactuar con el bot
|
| 408 |
+
# ===========================
|
| 409 |
+
def chatBotProtech(client):
|
| 410 |
+
with st.sidebar.expander("📁 Chatbot"):
|
| 411 |
+
|
| 412 |
+
# Inicializar estados
|
| 413 |
+
if "chat_history" not in st.session_state:
|
| 414 |
+
st.session_state.chat_history = []
|
| 415 |
+
|
| 416 |
+
if "audio_data" not in st.session_state:
|
| 417 |
+
st.session_state.audio_data = None
|
| 418 |
+
|
| 419 |
+
if "transcripcion" not in st.session_state:
|
| 420 |
+
st.session_state.transcripcion = ""
|
| 421 |
+
|
| 422 |
+
if "mostrar_grabador" not in st.session_state:
|
| 423 |
+
st.session_state.mostrar_grabador = True
|
| 424 |
+
|
| 425 |
+
# Contenedor para mensajes
|
| 426 |
+
messages = st.container(height=400)
|
| 427 |
+
|
| 428 |
+
|
| 429 |
+
# CSS: estilo tipo Messenger
|
| 430 |
+
st.markdown("""
|
| 431 |
+
<style>
|
| 432 |
+
.chat-message {
|
| 433 |
+
display: flex;
|
| 434 |
+
align-items: flex-start;
|
| 435 |
+
margin: 10px 0;
|
| 436 |
+
}
|
| 437 |
+
.chat-message.user {
|
| 438 |
+
justify-content: flex-end;
|
| 439 |
+
}
|
| 440 |
+
.chat-message.assistant {
|
| 441 |
+
justify-content: flex-start;
|
| 442 |
+
}
|
| 443 |
+
.chat-icon {
|
| 444 |
+
width: 30px;
|
| 445 |
+
height: 30px;
|
| 446 |
+
border-radius: 50%;
|
| 447 |
+
background-color: #ccc;
|
| 448 |
+
display: flex;
|
| 449 |
+
align-items: center;
|
| 450 |
+
justify-content: center;
|
| 451 |
+
font-size: 18px;
|
| 452 |
+
margin: 0 5px;
|
| 453 |
+
}
|
| 454 |
+
.chat-bubble {
|
| 455 |
+
max-width: 70%;
|
| 456 |
+
padding: 10px 15px;
|
| 457 |
+
border-radius: 15px;
|
| 458 |
+
font-size: 14px;
|
| 459 |
+
line-height: 1.5;
|
| 460 |
+
word-wrap: break-word;
|
| 461 |
+
}
|
| 462 |
+
.chat-bubble.user {
|
| 463 |
+
background-color: #DCF8C6;
|
| 464 |
+
color: black;
|
| 465 |
+
border-top-right-radius: 0;
|
| 466 |
+
}
|
| 467 |
+
.chat-bubble.assistant {
|
| 468 |
+
background-color: #F1F0F0;
|
| 469 |
+
color: black;
|
| 470 |
+
border-top-left-radius: 0;
|
| 471 |
+
}
|
| 472 |
+
</style>
|
| 473 |
+
""", unsafe_allow_html=True)
|
| 474 |
+
|
| 475 |
+
# Mostrar historial de mensajes
|
| 476 |
+
with messages:
|
| 477 |
+
st.header("🤖 ChatBot Protech")
|
| 478 |
+
for message in st.session_state.chat_history:
|
| 479 |
+
role = message["role"]
|
| 480 |
+
content = html.escape(message["content"]) # Escapar contenido HTML
|
| 481 |
+
bubble_class = "user" if role == "user" else "assistant"
|
| 482 |
+
icon = "👤" if role == "user" else "🤖"
|
| 483 |
+
|
| 484 |
+
# Mostrar el mensaje en una sola burbuja con ícono en el mismo bloque
|
| 485 |
+
st.markdown(f"""
|
| 486 |
+
<div class="chat-message {bubble_class}">
|
| 487 |
+
<div class="chat-icon">{icon}</div>
|
| 488 |
+
<div class="chat-bubble {bubble_class}">{content}</div>
|
| 489 |
+
</div>
|
| 490 |
+
""", unsafe_allow_html=True)
|
| 491 |
+
|
| 492 |
+
# --- Manejar transcripción como mensaje automático ---
|
| 493 |
+
if st.session_state.transcripcion:
|
| 494 |
+
prompt = st.session_state.transcripcion
|
| 495 |
+
st.session_state.transcripcion = ""
|
| 496 |
+
|
| 497 |
+
st.session_state.chat_history.append({"role": "user", "content": prompt})
|
| 498 |
+
|
| 499 |
+
with messages:
|
| 500 |
+
st.markdown(f"""
|
| 501 |
+
<div class="chat-message user">
|
| 502 |
+
<div class="chat-bubble user">{html.escape(prompt)}</div>
|
| 503 |
+
<div class="chat-icon">👤</div>
|
| 504 |
+
</div>
|
| 505 |
+
""", unsafe_allow_html=True)
|
| 506 |
+
|
| 507 |
+
with messages:
|
| 508 |
+
with st.spinner("Pensando..."):
|
| 509 |
+
completion = callDeepseek(client, prompt)
|
| 510 |
+
response = ""
|
| 511 |
+
response_placeholder = st.empty()
|
| 512 |
+
|
| 513 |
+
for chunk in completion:
|
| 514 |
+
content = chunk.choices[0].delta.content or ""
|
| 515 |
+
response += content
|
| 516 |
+
response_placeholder.markdown(f"""
|
| 517 |
+
<div class="chat-message assistant">
|
| 518 |
+
<div class="chat-icon">🤖</div>
|
| 519 |
+
<div class="chat-bubble assistant">{response}</div>
|
| 520 |
+
</div>
|
| 521 |
+
""", unsafe_allow_html=True)
|
| 522 |
+
|
| 523 |
+
st.session_state.chat_history.append({"role": "assistant", "content": response})
|
| 524 |
+
|
| 525 |
+
# Captura del input tipo chat
|
| 526 |
+
if prompt := st.chat_input("Escribe algo..."):
|
| 527 |
+
st.session_state.chat_history.append({"role": "user", "content": prompt})
|
| 528 |
+
|
| 529 |
+
# Mostrar mensaje del usuario escapado
|
| 530 |
+
with messages:
|
| 531 |
+
|
| 532 |
+
st.markdown(f"""
|
| 533 |
+
<div class="chat-message user">
|
| 534 |
+
<div class="chat-bubble user">{prompt}</div>
|
| 535 |
+
<div class="chat-icon">👤</div>
|
| 536 |
+
</div>
|
| 537 |
+
""", unsafe_allow_html=True)
|
| 538 |
+
|
| 539 |
+
# Mostrar respuesta del asistente
|
| 540 |
+
with messages:
|
| 541 |
+
with st.spinner("Pensando..."):
|
| 542 |
+
completion = callDeepseek(client, prompt)
|
| 543 |
+
response = ""
|
| 544 |
+
response_placeholder = st.empty()
|
| 545 |
+
|
| 546 |
+
for chunk in completion:
|
| 547 |
+
content = chunk.choices[0].delta.content or ""
|
| 548 |
+
response += content
|
| 549 |
+
|
| 550 |
+
response_placeholder.markdown(f"""
|
| 551 |
+
<div class="chat-message assistant">
|
| 552 |
+
<div class="chat-icon">🤖</div>
|
| 553 |
+
<div class="chat-bubble assistant">{response}</div>
|
| 554 |
+
</div>
|
| 555 |
+
""", unsafe_allow_html=True)
|
| 556 |
+
|
| 557 |
+
st.session_state.chat_history.append({"role": "assistant", "content": response})
|
| 558 |
+
|
| 559 |
+
# Grabación de audio (solo si está habilitada)
|
| 560 |
+
if st.session_state.mostrar_grabador and st.session_state.audio_data is None:
|
| 561 |
+
audio_data = st.audio_input("Graba tu voz aquí 🎤")
|
| 562 |
+
if audio_data:
|
| 563 |
+
st.session_state.audio_data = audio_data
|
| 564 |
+
st.session_state.mostrar_grabador = False # Ocultar input después de grabar
|
| 565 |
+
st.rerun() # Forzar recarga para ocultar input y evitar que reaparezca el audio cargado
|
| 566 |
+
|
| 567 |
+
# Mostrar controles solo si hay audio cargado
|
| 568 |
+
if st.session_state.audio_data:
|
| 569 |
+
st.audio(st.session_state.audio_data, format="audio/wav")
|
| 570 |
+
col1, col2 = st.columns(2)
|
| 571 |
+
|
| 572 |
+
with col1:
|
| 573 |
+
if st.button("✅ Aceptar grabación"):
|
| 574 |
+
with st.spinner("Convirtiendo y transcribiendo..."):
|
| 575 |
+
m4a_path = converter_bytes_m4a(st.session_state.audio_data)
|
| 576 |
+
|
| 577 |
+
with open(m4a_path, "rb") as f:
|
| 578 |
+
texto = callWhisper(client, m4a_path, f)
|
| 579 |
+
|
| 580 |
+
os.remove(m4a_path)
|
| 581 |
+
|
| 582 |
+
st.session_state.transcripcion = texto
|
| 583 |
+
st.session_state.audio_data = None
|
| 584 |
+
st.session_state.mostrar_grabador = True
|
| 585 |
+
st.rerun()
|
| 586 |
+
|
| 587 |
+
with col2:
|
| 588 |
+
if st.button("❌ Descartar grabación"):
|
| 589 |
+
st.session_state.audio_data = None
|
| 590 |
+
st.session_state.transcripcion = ""
|
| 591 |
+
st.session_state.mostrar_grabador = True
|
| 592 |
+
st.rerun()
|
| 593 |
+
|
| 594 |
+
# Mostrar transcripción como texto previo al input si existe
|
| 595 |
+
'''
|
| 596 |
+
if st.session_state.transcripcion:
|
| 597 |
+
st.info(f"📝 Transcripción: {st.session_state.transcripcion}")
|
| 598 |
+
# Prellenar el input simuladamente
|
| 599 |
+
prompt = st.session_state.transcripcion
|
| 600 |
+
st.session_state.transcripcion = "" # Limpiar
|
| 601 |
+
st.rerun() # Simular que se envió el mensaje
|
| 602 |
+
'''
|
| 603 |
+
|
| 604 |
+
#def speechRecognition():
|
| 605 |
+
#audio_value = st.audio_input("Record a voice message")
|
| 606 |
+
|
| 607 |
+
def callDeepseek(client, prompt):
|
| 608 |
+
completion = client.chat.completions.create(
|
| 609 |
+
#model="meta-llama/llama-4-scout-17b-16e-instruct",
|
| 610 |
+
model = "deepseek-r1-distill-llama-70b",
|
| 611 |
+
messages=[{"role": "user", "content": prompt}],
|
| 612 |
+
temperature=0.6,
|
| 613 |
+
max_tokens=1024,
|
| 614 |
+
top_p=1,
|
| 615 |
+
stream=True,
|
| 616 |
+
)
|
| 617 |
+
return completion
|
| 618 |
+
|
| 619 |
+
def callWhisper(client, filename_audio,file):
|
| 620 |
+
transcription = client.audio.transcriptions.create(
|
| 621 |
+
file=(filename_audio, file.read()),
|
| 622 |
+
model="whisper-large-v3",
|
| 623 |
+
response_format="verbose_json",
|
| 624 |
+
)
|
| 625 |
+
return transcription.text
|
| 626 |
+
|
| 627 |
+
def converter_bytes_m4a(audio_bytes: BytesIO) -> str:
|
| 628 |
+
"""
|
| 629 |
+
Convierte un audio en bytes (WAV, etc.) a un archivo M4A temporal.
|
| 630 |
+
Retorna la ruta del archivo .m4a temporal.
|
| 631 |
+
"""
|
| 632 |
+
# Asegurarse de que el cursor del stream esté al inicio
|
| 633 |
+
audio_bytes.seek(0)
|
| 634 |
+
|
| 635 |
+
# Leer el audio desde BytesIO usando pydub
|
| 636 |
+
audio = AudioSegment.from_file(audio_bytes)
|
| 637 |
+
|
| 638 |
+
# Crear archivo temporal para guardar como .m4a
|
| 639 |
+
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".m4a")
|
| 640 |
+
m4a_path = temp_file.name
|
| 641 |
+
temp_file.close() # Cerramos para que pydub pueda escribirlo
|
| 642 |
+
|
| 643 |
+
# Exportar a M4A usando formato compatible con ffmpeg
|
| 644 |
+
audio.export(m4a_path, format="ipod") # 'ipod' genera .m4a
|
| 645 |
+
|
| 646 |
+
return m4a_path
|
| 647 |
+
# ===========================
|
| 648 |
+
# Función para cargar el modelo SARIMA
|
| 649 |
+
# ===========================
|
| 650 |
+
"""def cargar_modelo_sarima(ruta_modelo):
|
| 651 |
+
# Cargar el modelo utilizando joblib
|
| 652 |
+
modelo = joblib.load(ruta_modelo)
|
| 653 |
+
return modelo"""
|
| 654 |
+
|
| 655 |
+
# ===========================
|
| 656 |
+
# Función para obtener el número de periodos basado en el filtro
|
| 657 |
+
# ===========================
|
| 658 |
+
def obtener_periodos(filtro):
|
| 659 |
+
opciones_periodos = {
|
| 660 |
+
'5 años': 60,
|
| 661 |
+
'3 años': 36,
|
| 662 |
+
'1 año': 12,
|
| 663 |
+
'5 meses': 5
|
| 664 |
+
}
|
| 665 |
+
return opciones_periodos.get(filtro, 12)
|
| 666 |
+
|
| 667 |
+
# ===========================
|
| 668 |
+
# Función para mostrar ventas actuales y proyectadas
|
| 669 |
+
# ===========================
|
| 670 |
+
"""
|
| 671 |
+
def mostrar_ventas_proyectadas(filtro):
|
| 672 |
+
ruta_modelo = os.path.join("arima_sales_model.pkl")
|
| 673 |
+
modelo_sarima = cargar_modelo_sarima(ruta_modelo)
|
| 674 |
+
|
| 675 |
+
if "archivo_subido" in st.session_state and st.session_state.archivo_subido:
|
| 676 |
+
# Cargar datos del archivo subido
|
| 677 |
+
df = st.session_state.df_subido.copy()
|
| 678 |
+
df['Date'] = pd.to_datetime(df['Date'])
|
| 679 |
+
df = df.sort_values('Date')
|
| 680 |
+
|
| 681 |
+
# Generar predicciones
|
| 682 |
+
periodos = obtener_periodos(filtro)
|
| 683 |
+
predicciones = generar_predicciones(modelo_sarima, df, periodos)
|
| 684 |
+
|
| 685 |
+
# Redondear y formatear las ventas
|
| 686 |
+
df['Sale'] = df['Sale'].round(2).apply(lambda x: f"{x:,.2f}") # Formato con 2 decimales y comas
|
| 687 |
+
predicciones = [round(val, 2) for val in predicciones] # Redondear predicciones
|
| 688 |
+
|
| 689 |
+
# Preparar datos para graficar
|
| 690 |
+
df['Tipo'] = 'Ventas Actuales'
|
| 691 |
+
df_pred = pd.DataFrame({
|
| 692 |
+
'Date': pd.date_range(df['Date'].max(), periods=periodos + 1, freq='ME')[1:],
|
| 693 |
+
'Sale': predicciones,
|
| 694 |
+
'Tipo': 'Ventas Pronosticadas'
|
| 695 |
+
})
|
| 696 |
+
|
| 697 |
+
df_grafico = pd.concat([df[['Date', 'Sale', 'Tipo']], df_pred])
|
| 698 |
+
else:
|
| 699 |
+
st.warning("Por favor, sube un archivo CSV válido para generasr predicciones.")
|
| 700 |
+
return
|
| 701 |
+
|
| 702 |
+
# Crear gráfica
|
| 703 |
+
fig = px.line(
|
| 704 |
+
df_grafico,
|
| 705 |
+
x='Date',
|
| 706 |
+
y='Sale',
|
| 707 |
+
color='Tipo',
|
| 708 |
+
title='Ventas pronosticadas (Ventas vs Mes)',
|
| 709 |
+
labels={'Date': 'Fecha', 'Sale': 'Ventas', 'Tipo': 'Serie'}
|
| 710 |
+
)
|
| 711 |
+
|
| 712 |
+
# Centramos el título del gráfico
|
| 713 |
+
fig.update_layout(
|
| 714 |
+
title={
|
| 715 |
+
'text': "Ventas Actuales y Pronosticadas",
|
| 716 |
+
|
| 717 |
+
'x': 0.5, # Centrado horizontal
|
| 718 |
+
'xanchor': 'center', # Asegura el anclaje central
|
| 719 |
+
'yanchor': 'top' # Anclaje superior (opcional)
|
| 720 |
+
},
|
| 721 |
+
title_font=dict(size=18, family="Arial, sans-serif", color='black'),
|
| 722 |
+
)
|
| 723 |
+
|
| 724 |
+
fig.update_xaxes(tickangle=-45)
|
| 725 |
+
|
| 726 |
+
# Mejorar el diseño de la leyenda
|
| 727 |
+
fig.update_layout(
|
| 728 |
+
legend=dict(
|
| 729 |
+
title="Leyenda", # Título de la leyenda
|
| 730 |
+
title_font=dict(size=12, color="black"),
|
| 731 |
+
font=dict(size=10, color="black"),
|
| 732 |
+
bgcolor="rgba(240,240,240,0.8)", # Fondo semitransparente
|
| 733 |
+
bordercolor="gray",
|
| 734 |
+
borderwidth=1,
|
| 735 |
+
orientation="h", # Leyenda horizontal
|
| 736 |
+
yanchor="top",
|
| 737 |
+
y=-0.3, # Ajustar la posición vertical
|
| 738 |
+
xanchor="right",
|
| 739 |
+
x=0.5 # Centrar horizontalmente
|
| 740 |
+
)
|
| 741 |
+
)
|
| 742 |
+
|
| 743 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 744 |
+
"""
|
| 745 |
+
# ===========================
|
| 746 |
+
# Función para generar predicciones
|
| 747 |
+
# ===========================
|
| 748 |
+
def generar_predicciones(modelo, df, periodos):
|
| 749 |
+
ventas = df['Sale']
|
| 750 |
+
predicciones = modelo.forecast(steps=periodos)
|
| 751 |
+
return predicciones
|
| 752 |
+
|
| 753 |
+
# Función para mejorar el diseño de las gráficas
|
| 754 |
+
def mejorar_diseno_grafica(fig, meses_relevantes, nombres_meses_relevantes):
|
| 755 |
+
fig.update_layout(
|
| 756 |
+
title={
|
| 757 |
+
'text': "Ventas vs Mes",
|
| 758 |
+
|
| 759 |
+
'x': 0.5, # Centrado horizontal
|
| 760 |
+
'xanchor': 'center', # Asegura el anclaje central
|
| 761 |
+
'yanchor': 'top' # Anclaje superior (opcional)
|
| 762 |
+
},
|
| 763 |
+
title_font=dict(size=18, family="Arial, sans-serif", color='black'),
|
| 764 |
+
xaxis=dict(
|
| 765 |
+
title='Mes-Año',
|
| 766 |
+
title_font=dict(size=14, family="Arial, sans-serif", color='black'),
|
| 767 |
+
tickangle=-45, # Rotar las etiquetas
|
| 768 |
+
showgrid=True,
|
| 769 |
+
gridwidth=0.5,
|
| 770 |
+
gridcolor='lightgrey',
|
| 771 |
+
showline=True,
|
| 772 |
+
linecolor='black',
|
| 773 |
+
linewidth=2,
|
| 774 |
+
tickmode='array', # Controla qué etiquetas mostrar
|
| 775 |
+
tickvals=meses_relevantes, # Selecciona solo los meses relevantes
|
| 776 |
+
ticktext=nombres_meses_relevantes, # Meses seleccionados
|
| 777 |
+
tickfont=dict(size=10), # Reducir el tamaño de la fuente de las etiquetas
|
| 778 |
+
),
|
| 779 |
+
yaxis=dict(
|
| 780 |
+
title='Ventas',
|
| 781 |
+
title_font=dict(size=14, family="Arial, sans-serif", color='black'),
|
| 782 |
+
showgrid=True,
|
| 783 |
+
gridwidth=0.5,
|
| 784 |
+
gridcolor='lightgrey',
|
| 785 |
+
showline=True,
|
| 786 |
+
linecolor='black',
|
| 787 |
+
linewidth=2
|
| 788 |
+
),
|
| 789 |
+
plot_bgcolor='white', # Fondo blanco
|
| 790 |
+
paper_bgcolor='white', # Fondo del lienzo de la gráfica
|
| 791 |
+
font=dict(family="Arial, sans-serif", size=12, color="black"),
|
| 792 |
+
showlegend=False, # Desactivar la leyenda si no es necesaria
|
| 793 |
+
margin=dict(l=50, r=50, t=50, b=50) # Márgenes ajustados
|
| 794 |
+
)
|
| 795 |
+
|
| 796 |
+
|
| 797 |
+
|
| 798 |
+
return fig
|
| 799 |
+
|
| 800 |
+
# ===========================
|
| 801 |
+
# Función para cerrar sesión
|
| 802 |
+
# ===========================
|
| 803 |
+
def cerrar_sesion():
|
| 804 |
+
st.session_state.logged_in = False
|
| 805 |
+
st.session_state.usuario = None
|
| 806 |
+
st.session_state.pagina_actual = "login"
|
| 807 |
+
st.session_state.archivo_subido = False # Limpiar el archivo subido al cerrar sesión
|
| 808 |
+
st.session_state.df_subido = None # Limpiar datos del archivo
|
| 809 |
+
# Eliminar parámetros de la URL usando st.query_params
|
| 810 |
+
st.query_params.clear() # Método correcto para limpiar parámetros de consulta
|
| 811 |
+
|
| 812 |
+
# Redirigir a la página de login
|
| 813 |
+
st.rerun()
|
paginas/dashboardDemo.py
ADDED
|
@@ -0,0 +1,916 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import plotly.express as px
|
| 4 |
+
import random
|
| 5 |
+
import time
|
| 6 |
+
import joblib
|
| 7 |
+
import os
|
| 8 |
+
import statsmodels
|
| 9 |
+
from dotenv import load_dotenv
|
| 10 |
+
import os
|
| 11 |
+
from groq import Groq
|
| 12 |
+
import html
|
| 13 |
+
from pydub import AudioSegment
|
| 14 |
+
import tempfile
|
| 15 |
+
from io import BytesIO
|
| 16 |
+
from fpdf import FPDF
|
| 17 |
+
from PIL import Image
|
| 18 |
+
from math import ceil
|
| 19 |
+
from datetime import datetime
|
| 20 |
+
from sklearn.metrics import r2_score
|
| 21 |
+
#from langchain.agents.agent_toolkits import create_csv_agent
|
| 22 |
+
#from langchain_groq import ChatGroq
|
| 23 |
+
# ===========================
|
| 24 |
+
# Función para generar datos ficticios
|
| 25 |
+
# ===========================
|
| 26 |
+
def generar_datos():
|
| 27 |
+
meses = [
|
| 28 |
+
"Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio",
|
| 29 |
+
"Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"
|
| 30 |
+
]
|
| 31 |
+
paises = ["México", "Colombia", "Argentina", "Chile", "Perú"]
|
| 32 |
+
data = [
|
| 33 |
+
{"mes": mes, "pais": pais, "Total": random.randint(100, 1000)}
|
| 34 |
+
for mes in meses for pais in paises
|
| 35 |
+
]
|
| 36 |
+
return pd.DataFrame(data), meses, paises
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
# ===========================
|
| 40 |
+
# Función para el dashboard principal
|
| 41 |
+
# ===========================
|
| 42 |
+
def mostrar_dashboard():
|
| 43 |
+
# Cargar variables desde el archivo .env
|
| 44 |
+
load_dotenv()
|
| 45 |
+
|
| 46 |
+
# Acceder a la clave
|
| 47 |
+
groq_key = os.getenv("GROQ_API_KEY")
|
| 48 |
+
client = Groq(api_key=groq_key)
|
| 49 |
+
|
| 50 |
+
dfDatos, meses, paises = generar_datos()
|
| 51 |
+
|
| 52 |
+
# Opciones del selectbox
|
| 53 |
+
lista_opciones = ['5 años', '3 años', '1 año', '5 meses']
|
| 54 |
+
|
| 55 |
+
# Mostrar barra lateral
|
| 56 |
+
mostrar_sidebar(client)
|
| 57 |
+
|
| 58 |
+
# Título principal
|
| 59 |
+
st.header(':bar_chart: Dashboard Sales')
|
| 60 |
+
|
| 61 |
+
# Mostrar gráficos
|
| 62 |
+
mostrar_graficos(lista_opciones)
|
| 63 |
+
|
| 64 |
+
# ===========================
|
| 65 |
+
# Configuración inicial de la página
|
| 66 |
+
# ===========================
|
| 67 |
+
#def configurar_pagina():
|
| 68 |
+
#st.set_page_config(
|
| 69 |
+
# page_title="Dashboard Sales",
|
| 70 |
+
# page_icon=":smile:",
|
| 71 |
+
# layout="wide",
|
| 72 |
+
# initial_sidebar_state="expanded"
|
| 73 |
+
#)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
# ===========================
|
| 78 |
+
# Función para la barra lateral
|
| 79 |
+
# ===========================
|
| 80 |
+
def mostrar_sidebar(client):
|
| 81 |
+
sidebar_logo = r"paginas\images\Logo general.png"
|
| 82 |
+
main_body_logo = r"paginas\images\Logo.png"
|
| 83 |
+
sidebar_logo_dashboard = r"paginas\images\Logo dashboard.png"
|
| 84 |
+
|
| 85 |
+
st.logo(sidebar_logo, size="large", icon_image=main_body_logo)
|
| 86 |
+
|
| 87 |
+
st.sidebar.image(sidebar_logo_dashboard)
|
| 88 |
+
st.sidebar.title('🧠 GenAI Forecast')
|
| 89 |
+
|
| 90 |
+
uploaded_file = selectedFile()
|
| 91 |
+
verifyFile(uploaded_file)
|
| 92 |
+
archivo_csv = "df_articles.csv"
|
| 93 |
+
chatBotProtech(client)
|
| 94 |
+
downloadCSV(archivo_csv)
|
| 95 |
+
closeSession()
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def closeSession():
|
| 100 |
+
if st.sidebar.button("Cerrar Sesión"):
|
| 101 |
+
cerrar_sesion()
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def guardar_graficas_como_imagen(figuras: dict):
|
| 105 |
+
rutas_imagenes = []
|
| 106 |
+
temp_dir = tempfile.gettempdir()
|
| 107 |
+
|
| 108 |
+
for nombre, figura in figuras.items():
|
| 109 |
+
ruta_png = os.path.join(temp_dir, f"{nombre}.png")
|
| 110 |
+
ruta_jpeg = os.path.join(temp_dir, f"{nombre}.jpg")
|
| 111 |
+
|
| 112 |
+
# Guardar como PNG primero
|
| 113 |
+
figura.write_image(ruta_png, width=900, height=500, engine="kaleido")
|
| 114 |
+
|
| 115 |
+
# Convertir a JPEG usando PIL
|
| 116 |
+
with Image.open(ruta_png) as img:
|
| 117 |
+
rgb_img = img.convert("RGB") # Asegura formato compatible con JPEG
|
| 118 |
+
rgb_img.save(ruta_jpeg, "JPEG", quality=95)
|
| 119 |
+
|
| 120 |
+
rutas_imagenes.append((nombre, ruta_jpeg))
|
| 121 |
+
|
| 122 |
+
# Opcional: borrar el PNG temporal
|
| 123 |
+
os.remove(ruta_png)
|
| 124 |
+
|
| 125 |
+
return rutas_imagenes
|
| 126 |
+
|
| 127 |
+
def generateHeaderPDF(pdf):
|
| 128 |
+
# Logo
|
| 129 |
+
logo_path = r"paginas\images\Logo general.png"
|
| 130 |
+
if os.path.exists(logo_path):
|
| 131 |
+
pdf.image(logo_path, x=7, y=6, w=35)
|
| 132 |
+
|
| 133 |
+
# Título centrado
|
| 134 |
+
pdf.set_font('Arial', 'B', 16)
|
| 135 |
+
pdf.set_xy(5, 10)
|
| 136 |
+
pdf.cell(w=0, h=10, txt="Reporte del Dashboard de Ventas", border=0, ln=0, align='C')
|
| 137 |
+
|
| 138 |
+
# Fecha lado derecho
|
| 139 |
+
fecha = datetime.now().strftime("%d/%m/%Y")
|
| 140 |
+
pdf.set_xy(-40, 5)
|
| 141 |
+
pdf.set_font('Arial', '', 10)
|
| 142 |
+
pdf.cell(w=30, h=10, txt=fecha, border=0, ln=0, align='R')
|
| 143 |
+
|
| 144 |
+
pdf.ln(15)
|
| 145 |
+
|
| 146 |
+
def generateFooterPDF(pdf):
|
| 147 |
+
pdf.set_y(-30)
|
| 148 |
+
pdf.set_font('Arial', 'I', 8)
|
| 149 |
+
pdf.set_text_color(100)
|
| 150 |
+
pdf.cell(0, 5, "PRO TECHNOLOGY SOLUTIONS S.A.C - Área de ventas", 0, 1, 'C')
|
| 151 |
+
pdf.cell(0, 5, "Reporte generado automáticamente por el sistema de análisis", 0, 1, 'C')
|
| 152 |
+
pdf.cell(0, 5, f"Página {pdf.page_no()}", 0, 0, 'C')
|
| 153 |
+
|
| 154 |
+
def generateContentPDF(pdf, imagenes):
|
| 155 |
+
for i in range(0, len(imagenes), 2):
|
| 156 |
+
pdf.add_page()
|
| 157 |
+
|
| 158 |
+
generateHeaderPDF(pdf)
|
| 159 |
+
|
| 160 |
+
# Primera imagen
|
| 161 |
+
titulo1, ruta1 = imagenes[i]
|
| 162 |
+
if os.path.exists(ruta1):
|
| 163 |
+
img1 = Image.open(ruta1).convert("RGB")
|
| 164 |
+
ruta_color1 = ruta1.replace(".png", "_color.png")
|
| 165 |
+
img1.save(ruta_color1)
|
| 166 |
+
pdf.image(ruta_color1, x=10, y=30, w=180)
|
| 167 |
+
|
| 168 |
+
# Segunda imagen
|
| 169 |
+
if i + 1 < len(imagenes):
|
| 170 |
+
titulo2, ruta2 = imagenes[i + 1]
|
| 171 |
+
if os.path.exists(ruta2):
|
| 172 |
+
img2 = Image.open(ruta2).convert("RGB")
|
| 173 |
+
ruta_color2 = ruta2.replace(".png", "_color.png")
|
| 174 |
+
img2.save(ruta_color2)
|
| 175 |
+
pdf.image(ruta_color2, x=10, y=150, w=180)
|
| 176 |
+
|
| 177 |
+
generateFooterPDF(pdf)
|
| 178 |
+
|
| 179 |
+
def generar_reporte_dashboard(imagenes):
|
| 180 |
+
pdf = FPDF(orientation='P', unit='mm', format='A4')
|
| 181 |
+
pdf.set_auto_page_break(auto=True, margin=15)
|
| 182 |
+
|
| 183 |
+
generateContentPDF(pdf, imagenes)
|
| 184 |
+
|
| 185 |
+
ruta_pdf = "reporte.pdf"
|
| 186 |
+
pdf.output(ruta_pdf)
|
| 187 |
+
return ruta_pdf
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
# Función para obtener los meses relevantes
|
| 191 |
+
def obtener_meses_relevantes(df):
|
| 192 |
+
# Extraemos los años y meses de la columna 'Date'
|
| 193 |
+
df['Year'] = pd.to_datetime(df['orddt']).dt.year
|
| 194 |
+
df['Month'] = pd.to_datetime(df['orddt']).dt.month
|
| 195 |
+
|
| 196 |
+
# Encontramos el primer y último año en el dataset
|
| 197 |
+
primer_ano = df['Year'].min()
|
| 198 |
+
ultimo_ano = df['Year'].max()
|
| 199 |
+
|
| 200 |
+
meses_relevantes = []
|
| 201 |
+
nombres_meses_relevantes = []
|
| 202 |
+
|
| 203 |
+
# Recorrer todos los años dentro del rango
|
| 204 |
+
for ano in range(primer_ano, ultimo_ano + 1):
|
| 205 |
+
for mes in [1, 4, 7, 10]: # Meses relevantes: enero (1), abril (4), julio (7), octubre (10)
|
| 206 |
+
if mes in df[df['Year'] == ano]['Month'].values:
|
| 207 |
+
# Obtener el nombre del mes
|
| 208 |
+
nombre_mes = pd.to_datetime(f"{ano}-{mes}-01").strftime('%B') # Mes en formato textual (Enero, Abril, etc.)
|
| 209 |
+
meses_relevantes.append(f"{nombre_mes}-{ano}")
|
| 210 |
+
nombres_meses_relevantes.append(f"{nombre_mes}-{ano}")
|
| 211 |
+
|
| 212 |
+
return meses_relevantes, nombres_meses_relevantes
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
# ===========================
|
| 216 |
+
# Función para gráficos
|
| 217 |
+
# ===========================
|
| 218 |
+
def mostrar_graficos(lista_opciones):
|
| 219 |
+
if "archivo_subido" not in st.session_state or not st.session_state.archivo_subido:
|
| 220 |
+
st.warning("Por favor, sube un archivo CSV válido para visualizar los gráficos.")
|
| 221 |
+
return
|
| 222 |
+
|
| 223 |
+
df = st.session_state.df_subido.copy()
|
| 224 |
+
|
| 225 |
+
# --- Tarjetas con métricas clave ---
|
| 226 |
+
# Tasa de crecimiento por fecha si existe
|
| 227 |
+
total_ventas = df["sales"].sum()
|
| 228 |
+
promedio_ventas = df["sales"].mean()
|
| 229 |
+
|
| 230 |
+
st.subheader("📈 Resumen General")
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
# Tasa de crecimiento por fecha si existe
|
| 234 |
+
df['orddt'] = pd.to_datetime(df['orddt'], errors='coerce')
|
| 235 |
+
|
| 236 |
+
#Total de ventas
|
| 237 |
+
total_ventas = df['sales'].sum()
|
| 238 |
+
promedio_ventas = df['sales'].mean()
|
| 239 |
+
total_registros = df.shape[0]
|
| 240 |
+
|
| 241 |
+
# Tasa de crecimiento
|
| 242 |
+
df_filtrado = df.dropna(subset=['orddt'])
|
| 243 |
+
df_filtrado['mes_anio'] = df_filtrado['orddt'].dt.to_period('M')
|
| 244 |
+
ventas_por_mes = df_filtrado.groupby('mes_anio')['sales'].sum().sort_index()
|
| 245 |
+
|
| 246 |
+
tasa_crecimiento = None
|
| 247 |
+
if len(ventas_por_mes) >= 2:
|
| 248 |
+
primera_venta = ventas_por_mes.iloc[0]
|
| 249 |
+
ultima_venta = ventas_por_mes.iloc[-1]
|
| 250 |
+
if primera_venta != 0:
|
| 251 |
+
tasa_crecimiento = ((ultima_venta - primera_venta) / primera_venta) * 100
|
| 252 |
+
|
| 253 |
+
tarjetas = [
|
| 254 |
+
{"titulo": "Total de Ventas", "valor": abreviar_monto(total_ventas), "color": "#4CAF50"},
|
| 255 |
+
{"titulo": "Promedio de Ventas", "valor": f"${promedio_ventas:,.0f}", "color": "#2196F3"},
|
| 256 |
+
{"titulo": "Ventas registradas", "valor": total_registros, "color": "#9C27B0"},
|
| 257 |
+
{"titulo": "Tasa de crecimiento", "valor": f"{tasa_crecimiento:.2f}%" if tasa_crecimiento is not None else "N/A", "color": "#FF5722"},
|
| 258 |
+
]
|
| 259 |
+
|
| 260 |
+
col1, col2, col3, col4 = st.columns(4)
|
| 261 |
+
cols = [col1, col2, col3, col4]
|
| 262 |
+
|
| 263 |
+
for i, tarjeta in enumerate(tarjetas):
|
| 264 |
+
with cols[i]:
|
| 265 |
+
st.markdown(f"""
|
| 266 |
+
<div style='background-color:{tarjeta["color"]}; padding:20px; border-radius:10px; color:white; text-align:center;'>
|
| 267 |
+
<h4 style='margin:0;'>{tarjeta["titulo"]}</h4>
|
| 268 |
+
<h2 style='margin:0;'>{tarjeta["valor"]}</h2>
|
| 269 |
+
</div>
|
| 270 |
+
""", unsafe_allow_html=True)
|
| 271 |
+
|
| 272 |
+
st.markdown("---")
|
| 273 |
+
|
| 274 |
+
# Opciones de modelos (incluye una opción por defecto)
|
| 275 |
+
opciones_modelos = ["(Sin predicción)"] + ["LightGBM", "XGBoost",
|
| 276 |
+
"HistGradientBoosting",
|
| 277 |
+
"MLPRegressor", "GradientBoosting",
|
| 278 |
+
"RandomForest", "CatBoost"]
|
| 279 |
+
|
| 280 |
+
col_select, col_plot = st.columns([1, 5])
|
| 281 |
+
|
| 282 |
+
with col_select:
|
| 283 |
+
modelo_seleccionado = st.selectbox("Selecciona un modelo", opciones_modelos)
|
| 284 |
+
|
| 285 |
+
with col_plot.container(border=True):
|
| 286 |
+
if modelo_seleccionado == "(Sin predicción)":
|
| 287 |
+
if modelo_seleccionado == "(Sin predicción)":
|
| 288 |
+
df_real = df.copy()
|
| 289 |
+
df_real = df_real.dropna(subset=["orddt", "sales"])
|
| 290 |
+
|
| 291 |
+
fig_real = px.scatter(
|
| 292 |
+
df_real,
|
| 293 |
+
x="orddt",
|
| 294 |
+
y="sales",
|
| 295 |
+
trendline="ols", # Línea de regresión
|
| 296 |
+
color_discrete_sequence=["#1f77b4"],
|
| 297 |
+
trendline_color_override="orange",
|
| 298 |
+
labels={"sales": "Ventas", "orddt": "Fecha"},
|
| 299 |
+
title="Ventas Reales (Dispersión + Tendencia)",
|
| 300 |
+
width=600,
|
| 301 |
+
height=400
|
| 302 |
+
)
|
| 303 |
+
|
| 304 |
+
fig_real.update_traces(marker=dict(size=6), selector=dict(mode='markers'))
|
| 305 |
+
fig_real.update_layout(
|
| 306 |
+
template="plotly_white",
|
| 307 |
+
margin=dict(l=40, r=40, t=60, b=40),
|
| 308 |
+
legend_title_text="Datos",
|
| 309 |
+
showlegend=True
|
| 310 |
+
)
|
| 311 |
+
|
| 312 |
+
st.plotly_chart(fig_real, use_container_width=True)
|
| 313 |
+
|
| 314 |
+
else:
|
| 315 |
+
# Cargar modelo .pkl correspondiente
|
| 316 |
+
modelo_path = f"regressionmodels/{modelo_seleccionado.lower()}.pkl"
|
| 317 |
+
modelo = joblib.load(modelo_path)
|
| 318 |
+
|
| 319 |
+
# Preparar datos
|
| 320 |
+
df_pred = df.copy()
|
| 321 |
+
df_pred = df_pred.dropna(subset=["orddt"])
|
| 322 |
+
X_nuevo = df_pred.drop(columns=["sales"]) # Asegúrate que coincida con el modelo
|
| 323 |
+
y_pred = modelo.predict(X_nuevo)
|
| 324 |
+
df_pred["pred"] = y_pred
|
| 325 |
+
|
| 326 |
+
# Calcular precisión del modelo
|
| 327 |
+
r2 = r2_score(df_pred["sales"], df_pred["pred"])
|
| 328 |
+
|
| 329 |
+
# Gráfico de dispersión con línea de regresión
|
| 330 |
+
fig_pred = px.scatter(
|
| 331 |
+
df_pred,
|
| 332 |
+
x="sales",
|
| 333 |
+
y="pred",
|
| 334 |
+
trendline="ols",
|
| 335 |
+
color_discrete_sequence=["#1f77b4"],
|
| 336 |
+
trendline_color_override="orange",
|
| 337 |
+
labels={"sales": "Ventas Reales", "pred": "Ventas Predichas"},
|
| 338 |
+
title=f"Ventas Reales vs Predicción ({modelo_seleccionado})<br><sup>Precisión (R²): {r2:.3f}</sup>",
|
| 339 |
+
width=600, height=400
|
| 340 |
+
)
|
| 341 |
+
fig_pred.update_traces(marker=dict(size=6), selector=dict(mode='markers'))
|
| 342 |
+
fig_pred.update_layout(
|
| 343 |
+
legend_title_text='Datos',
|
| 344 |
+
template="plotly_white",
|
| 345 |
+
showlegend=True
|
| 346 |
+
)
|
| 347 |
+
st.plotly_chart(fig_pred, use_container_width=True)
|
| 348 |
+
|
| 349 |
+
|
| 350 |
+
|
| 351 |
+
# Fila 1: 3 gráficas
|
| 352 |
+
col1, col2 = st.columns(2)
|
| 353 |
+
with col1:
|
| 354 |
+
with col1.container(border=True):
|
| 355 |
+
fig1 = px.histogram(df, x='sales', title='Distribución de Ventas',
|
| 356 |
+
color_discrete_sequence=['#1f77b4'])
|
| 357 |
+
|
| 358 |
+
fig1.update_layout(
|
| 359 |
+
template="plotly_white",
|
| 360 |
+
margin=dict(l=40, r=40, t=60, b=40),
|
| 361 |
+
width=600,
|
| 362 |
+
height=400,
|
| 363 |
+
legend_title_text="Leyenda"
|
| 364 |
+
)
|
| 365 |
+
fig1.update_traces(marker=dict(line=dict(width=0.5, color='white')))
|
| 366 |
+
|
| 367 |
+
st.plotly_chart(fig1, use_container_width=True)
|
| 368 |
+
|
| 369 |
+
with col2:
|
| 370 |
+
with col2.container(border=True):
|
| 371 |
+
fig2 = px.box(df, x='segmt', y='sales', title='Ventas por Segmento',
|
| 372 |
+
color='segmt', color_discrete_sequence=px.colors.qualitative.Plotly)
|
| 373 |
+
st.plotly_chart(fig2, use_container_width=True)
|
| 374 |
+
|
| 375 |
+
# Fila 2: 2 gráficas
|
| 376 |
+
col4, col5 = st.columns(2)
|
| 377 |
+
with col4:
|
| 378 |
+
with col4.container(border=True):
|
| 379 |
+
fig4 = px.pie(df, names='categ', values='sales', title='Ventas por Categoría',
|
| 380 |
+
color_discrete_sequence=px.colors.qualitative.Set3)
|
| 381 |
+
st.plotly_chart(fig4, use_container_width=True)
|
| 382 |
+
|
| 383 |
+
with col5:
|
| 384 |
+
top_productos = (
|
| 385 |
+
df.groupby('prdna')['sales']
|
| 386 |
+
.sum()
|
| 387 |
+
.sort_values(ascending=False)
|
| 388 |
+
.head(10)
|
| 389 |
+
.reset_index()
|
| 390 |
+
)
|
| 391 |
+
with col5.container(border=True):
|
| 392 |
+
fig5 = px.bar(
|
| 393 |
+
top_productos,
|
| 394 |
+
x='sales',
|
| 395 |
+
y='prdna',
|
| 396 |
+
orientation='h',
|
| 397 |
+
title='Top 10 productos más vendidos',
|
| 398 |
+
labels={'sales': 'Ventas', 'prdna': 'Producto'},
|
| 399 |
+
color='sales',
|
| 400 |
+
color_continuous_scale='Blues'
|
| 401 |
+
)
|
| 402 |
+
|
| 403 |
+
fig5.update_layout(yaxis={'categoryorder': 'total ascending'})
|
| 404 |
+
st.plotly_chart(fig5, use_container_width=True)
|
| 405 |
+
|
| 406 |
+
col6, col7 = st.columns(2)
|
| 407 |
+
with col6:
|
| 408 |
+
with col6.container(border=True):
|
| 409 |
+
tabla = df.pivot_table(index='state', columns='subct', values='sales', aggfunc='sum').fillna(0)
|
| 410 |
+
|
| 411 |
+
if not tabla.empty:
|
| 412 |
+
tabla = tabla.astype(float)
|
| 413 |
+
fig6 = px.imshow(
|
| 414 |
+
tabla.values,
|
| 415 |
+
labels=dict(x="Categoría", y="Estado", color="Ventas"),
|
| 416 |
+
x=tabla.columns,
|
| 417 |
+
y=tabla.index,
|
| 418 |
+
text_auto=True,
|
| 419 |
+
title="Mapa de Calor: Ventas por distrito y categoría",
|
| 420 |
+
color_continuous_scale="Viridis"
|
| 421 |
+
)
|
| 422 |
+
st.plotly_chart(fig6, use_container_width=True)
|
| 423 |
+
else:
|
| 424 |
+
st.warning("No hay datos suficientes para mostrar el mapa de calor.")
|
| 425 |
+
|
| 426 |
+
with col7:
|
| 427 |
+
ventas_estado = df.groupby('state')['sales'].sum().reset_index()
|
| 428 |
+
with col7.container(border=True):
|
| 429 |
+
fig7 = px.bar(ventas_estado, x='state', y='sales', title='Ventas por distrito',
|
| 430 |
+
color='sales', color_continuous_scale='Teal')
|
| 431 |
+
st.plotly_chart(fig7, use_container_width=True)
|
| 432 |
+
|
| 433 |
+
if st.button("📄 Generar Reporte PDF del Dashboard"):
|
| 434 |
+
figs = [fig1, fig2, fig4, fig5, fig6, fig7]
|
| 435 |
+
|
| 436 |
+
figuras = {}
|
| 437 |
+
for fig in figs:
|
| 438 |
+
titulo = fig.layout.title.text or "Sin Título"
|
| 439 |
+
figuras[titulo] = fig
|
| 440 |
+
|
| 441 |
+
st.info("Generando imágenes de las gráficas...")
|
| 442 |
+
imagenes = guardar_graficas_como_imagen(figuras)
|
| 443 |
+
st.info("Generando PDF...")
|
| 444 |
+
ruta_pdf = generar_reporte_dashboard(imagenes)
|
| 445 |
+
|
| 446 |
+
with open(ruta_pdf, "rb") as f:
|
| 447 |
+
st.download_button("⬇️ Descargar Reporte PDF", f, file_name="reporte_dashboard.pdf")
|
| 448 |
+
|
| 449 |
+
|
| 450 |
+
|
| 451 |
+
def abreviar_monto(valor):
|
| 452 |
+
if valor >= 1_000_000:
|
| 453 |
+
return f"${valor / 1_000_000:.2f}M"
|
| 454 |
+
elif valor >= 1_000:
|
| 455 |
+
return f"${valor / 1_000:.2f}K"
|
| 456 |
+
else:
|
| 457 |
+
return f"${valor:.2f}"
|
| 458 |
+
|
| 459 |
+
# -------------------------------
|
| 460 |
+
# CARGA DE CSV Y GUARDADO EN SESIÓN
|
| 461 |
+
# -------------------------------
|
| 462 |
+
|
| 463 |
+
def loadCSV():
|
| 464 |
+
columnas_requeridas = [
|
| 465 |
+
'rowid','ordid','orddt','shpdt',
|
| 466 |
+
'segmt','state','cono','prodid',
|
| 467 |
+
'categ','subct','prdna','sales',
|
| 468 |
+
'order_month','order_day','order_year',
|
| 469 |
+
'order_dayofweek','shipping_delay'
|
| 470 |
+
]
|
| 471 |
+
with st.sidebar.expander("📁 Subir archivo"):
|
| 472 |
+
uploaded_file = st.file_uploader("Sube un archivo CSV:", type=["csv"], key="upload_csv")
|
| 473 |
+
|
| 474 |
+
if uploaded_file is not None:
|
| 475 |
+
# Reseteamos el estado de 'descargado' cuando se sube un archivo
|
| 476 |
+
st.session_state.descargado = False
|
| 477 |
+
st.session_state.archivo_subido = False # Reinicia el estado
|
| 478 |
+
try:
|
| 479 |
+
# Leer el archivo subido
|
| 480 |
+
df = pd.read_csv(uploaded_file)
|
| 481 |
+
|
| 482 |
+
# Verificar que las columnas estén presentes y en el orden correcto
|
| 483 |
+
if list(df.columns) == columnas_requeridas:
|
| 484 |
+
st.session_state.df_subido = df
|
| 485 |
+
st.session_state.archivo_subido = True
|
| 486 |
+
aviso = st.sidebar.success("✅ Archivo subido correctamente.")
|
| 487 |
+
time.sleep(3)
|
| 488 |
+
aviso.empty()
|
| 489 |
+
|
| 490 |
+
|
| 491 |
+
else:
|
| 492 |
+
st.session_state.archivo_subido = False
|
| 493 |
+
aviso = st.sidebar.error(f"El archivo no tiene las columnas requeridas: {columnas_requeridas}.")
|
| 494 |
+
time.sleep(3)
|
| 495 |
+
aviso.empty()
|
| 496 |
+
|
| 497 |
+
except Exception as e:
|
| 498 |
+
aviso = st.sidebar.error(f"Error al procesar el archivo: {str(e)}")
|
| 499 |
+
time.sleep(3)
|
| 500 |
+
aviso.empty()
|
| 501 |
+
|
| 502 |
+
# -------------------------------
|
| 503 |
+
# Mostrar uploader y manejar estado
|
| 504 |
+
# -------------------------------
|
| 505 |
+
def selectedFile():
|
| 506 |
+
with st.sidebar.expander("📁 Subir archivo"):
|
| 507 |
+
uploaded_file = st.file_uploader("Sube un archivo CSV:", type=["csv"], key="upload_csv")
|
| 508 |
+
|
| 509 |
+
if uploaded_file is not None:
|
| 510 |
+
st.session_state.descargado = False
|
| 511 |
+
st.session_state.archivo_subido = False
|
| 512 |
+
return uploaded_file
|
| 513 |
+
return None
|
| 514 |
+
|
| 515 |
+
# -------------------------------
|
| 516 |
+
# Procesar y validar archivo (con cache)
|
| 517 |
+
# -------------------------------
|
| 518 |
+
@st.cache_data
|
| 519 |
+
def loadCSV(uploaded_file):
|
| 520 |
+
columnas_requeridas = [
|
| 521 |
+
'rowid','ordid','orddt','shpdt',
|
| 522 |
+
'segmt','state','cono','prodid',
|
| 523 |
+
'categ','subct','prdna','sales',
|
| 524 |
+
'order_month','order_day','order_year',
|
| 525 |
+
'order_dayofweek','shipping_delay'
|
| 526 |
+
]
|
| 527 |
+
|
| 528 |
+
df = pd.read_csv(uploaded_file)
|
| 529 |
+
|
| 530 |
+
if list(df.columns) == columnas_requeridas:
|
| 531 |
+
return df, None
|
| 532 |
+
else:
|
| 533 |
+
return None, f"❌ El archivo no tiene las columnas requeridas: {columnas_requeridas}"
|
| 534 |
+
|
| 535 |
+
# -------------------------------
|
| 536 |
+
# Procesar y validar archivo (con cache)
|
| 537 |
+
# -------------------------------
|
| 538 |
+
def verifyFile(uploadedFile):
|
| 539 |
+
if uploadedFile:
|
| 540 |
+
try:
|
| 541 |
+
df, error = loadCSV(uploadedFile)
|
| 542 |
+
if error is None:
|
| 543 |
+
st.session_state.df_subido = df
|
| 544 |
+
st.session_state.archivo_subido = True
|
| 545 |
+
aviso = st.sidebar.success("✅ Archivo subido correctamente.")
|
| 546 |
+
else:
|
| 547 |
+
aviso = st.sidebar.error(error)
|
| 548 |
+
time.sleep(3)
|
| 549 |
+
aviso.empty()
|
| 550 |
+
|
| 551 |
+
except Exception as e:
|
| 552 |
+
aviso = st.sidebar.error(f"⚠️ Error al procesar el archivo: {str(e)}")
|
| 553 |
+
time.sleep(3)
|
| 554 |
+
aviso.empty()
|
| 555 |
+
|
| 556 |
+
# ===========================
|
| 557 |
+
# Función para descargar archivo CSV
|
| 558 |
+
# ===========================
|
| 559 |
+
def downloadCSV(archivo_csv):
|
| 560 |
+
# Verificamos si el archivo ya ha sido descargado
|
| 561 |
+
if 'descargado' not in st.session_state:
|
| 562 |
+
st.session_state.descargado = False
|
| 563 |
+
|
| 564 |
+
if not st.session_state.descargado:
|
| 565 |
+
descarga = st.sidebar.download_button(
|
| 566 |
+
label="Descargar archivo CSV",
|
| 567 |
+
data=open(archivo_csv, "rb"),
|
| 568 |
+
file_name="ventas.csv",
|
| 569 |
+
mime="text/csv"
|
| 570 |
+
)
|
| 571 |
+
if descarga:
|
| 572 |
+
# Marcamos el archivo como descargado
|
| 573 |
+
st.session_state.descargado = True
|
| 574 |
+
aviso = st.sidebar.success("¡Descarga completada!")
|
| 575 |
+
# Hacer que el mensaje desaparezca después de 2 segundos
|
| 576 |
+
time.sleep(3)
|
| 577 |
+
aviso.empty()
|
| 578 |
+
else:
|
| 579 |
+
aviso = st.sidebar.success("¡Ya has descargado el archivo!")
|
| 580 |
+
time.sleep(3)
|
| 581 |
+
aviso.empty()
|
| 582 |
+
|
| 583 |
+
# -------------------------------
|
| 584 |
+
# FUNCIÓN PARA DETECTAR REFERENCIA AL CSV
|
| 585 |
+
# -------------------------------
|
| 586 |
+
def detectedReferenceToCSV(prompt: str) -> bool:
|
| 587 |
+
palabras_clave = ["csv", "archivo", "contenido cargado", "file", "dataset"]
|
| 588 |
+
prompt_lower = prompt.lower()
|
| 589 |
+
return any(palabra in prompt_lower for palabra in palabras_clave)
|
| 590 |
+
|
| 591 |
+
# ===========================
|
| 592 |
+
# Función para interactuar con el bot
|
| 593 |
+
# ===========================
|
| 594 |
+
def chatBotProtech(client):
|
| 595 |
+
with st.sidebar.expander("📁 Chatbot"):
|
| 596 |
+
|
| 597 |
+
# Inicializar estados
|
| 598 |
+
if "chat_history" not in st.session_state:
|
| 599 |
+
st.session_state.chat_history = []
|
| 600 |
+
|
| 601 |
+
if "audio_data" not in st.session_state:
|
| 602 |
+
st.session_state.audio_data = None
|
| 603 |
+
|
| 604 |
+
if "transcripcion" not in st.session_state:
|
| 605 |
+
st.session_state.transcripcion = ""
|
| 606 |
+
|
| 607 |
+
if "mostrar_grabador" not in st.session_state:
|
| 608 |
+
st.session_state.mostrar_grabador = True
|
| 609 |
+
|
| 610 |
+
# Contenedor para mensajes
|
| 611 |
+
messages = st.container(height=400)
|
| 612 |
+
|
| 613 |
+
|
| 614 |
+
# CSS: estilo tipo Messenger
|
| 615 |
+
st.markdown("""
|
| 616 |
+
<style>
|
| 617 |
+
.chat-message {
|
| 618 |
+
display: flex;
|
| 619 |
+
align-items: flex-start;
|
| 620 |
+
margin: 10px 0;
|
| 621 |
+
}
|
| 622 |
+
.chat-message.user {
|
| 623 |
+
justify-content: flex-end;
|
| 624 |
+
}
|
| 625 |
+
.chat-message.assistant {
|
| 626 |
+
justify-content: flex-start;
|
| 627 |
+
}
|
| 628 |
+
.chat-icon {
|
| 629 |
+
width: 30px;
|
| 630 |
+
height: 30px;
|
| 631 |
+
border-radius: 50%;
|
| 632 |
+
background-color: #ccc;
|
| 633 |
+
display: flex;
|
| 634 |
+
align-items: center;
|
| 635 |
+
justify-content: center;
|
| 636 |
+
font-size: 18px;
|
| 637 |
+
margin: 0 5px;
|
| 638 |
+
}
|
| 639 |
+
.chat-bubble {
|
| 640 |
+
max-width: 70%;
|
| 641 |
+
padding: 10px 15px;
|
| 642 |
+
border-radius: 15px;
|
| 643 |
+
font-size: 14px;
|
| 644 |
+
line-height: 1.5;
|
| 645 |
+
word-wrap: break-word;
|
| 646 |
+
}
|
| 647 |
+
.chat-bubble.user {
|
| 648 |
+
background-color: #DCF8C6;
|
| 649 |
+
color: black;
|
| 650 |
+
border-top-right-radius: 0;
|
| 651 |
+
}
|
| 652 |
+
.chat-bubble.assistant {
|
| 653 |
+
background-color: #F1F0F0;
|
| 654 |
+
color: black;
|
| 655 |
+
border-top-left-radius: 0;
|
| 656 |
+
}
|
| 657 |
+
</style>
|
| 658 |
+
""", unsafe_allow_html=True)
|
| 659 |
+
|
| 660 |
+
# Mostrar historial de mensajes
|
| 661 |
+
with messages:
|
| 662 |
+
st.header("🤖 ChatBot Protech")
|
| 663 |
+
for message in st.session_state.chat_history:
|
| 664 |
+
role = message["role"]
|
| 665 |
+
content = html.escape(message["content"]) # Escapar contenido HTML
|
| 666 |
+
bubble_class = "user" if role == "user" else "assistant"
|
| 667 |
+
icon = "👤" if role == "user" else "🤖"
|
| 668 |
+
|
| 669 |
+
# Mostrar el mensaje en una sola burbuja con ícono en el mismo bloque
|
| 670 |
+
st.markdown(f"""
|
| 671 |
+
<div class="chat-message {bubble_class}">
|
| 672 |
+
<div class="chat-icon">{icon}</div>
|
| 673 |
+
<div class="chat-bubble {bubble_class}">{content}</div>
|
| 674 |
+
</div>
|
| 675 |
+
""", unsafe_allow_html=True)
|
| 676 |
+
|
| 677 |
+
# --- Manejar transcripción como mensaje automático ---
|
| 678 |
+
if st.session_state.transcripcion:
|
| 679 |
+
prompt = st.session_state.transcripcion
|
| 680 |
+
st.session_state.transcripcion = ""
|
| 681 |
+
|
| 682 |
+
st.session_state.chat_history.append({"role": "user", "content": prompt})
|
| 683 |
+
|
| 684 |
+
with messages:
|
| 685 |
+
st.markdown(f"""
|
| 686 |
+
<div class="chat-message user">
|
| 687 |
+
<div class="chat-bubble user">{html.escape(prompt)}</div>
|
| 688 |
+
<div class="chat-icon">👤</div>
|
| 689 |
+
</div>
|
| 690 |
+
""", unsafe_allow_html=True)
|
| 691 |
+
|
| 692 |
+
with messages:
|
| 693 |
+
with st.spinner("Pensando..."):
|
| 694 |
+
completion = callDeepseek(client, prompt)
|
| 695 |
+
response = ""
|
| 696 |
+
response_placeholder = st.empty()
|
| 697 |
+
|
| 698 |
+
for chunk in completion:
|
| 699 |
+
content = chunk.choices[0].delta.content or ""
|
| 700 |
+
response += content
|
| 701 |
+
response_placeholder.markdown(f"""
|
| 702 |
+
<div class="chat-message assistant">
|
| 703 |
+
<div class="chat-icon">🤖</div>
|
| 704 |
+
<div class="chat-bubble assistant">{response}</div>
|
| 705 |
+
</div>
|
| 706 |
+
""", unsafe_allow_html=True)
|
| 707 |
+
|
| 708 |
+
st.session_state.chat_history.append({"role": "assistant", "content": response})
|
| 709 |
+
|
| 710 |
+
# Captura del input tipo chat
|
| 711 |
+
if prompt := st.chat_input("Escribe algo..."):
|
| 712 |
+
st.session_state.chat_history.append({"role": "user", "content": prompt})
|
| 713 |
+
|
| 714 |
+
# Mostrar mensaje del usuario escapado
|
| 715 |
+
with messages:
|
| 716 |
+
|
| 717 |
+
st.markdown(f"""
|
| 718 |
+
<div class="chat-message user">
|
| 719 |
+
<div class="chat-bubble user">{prompt}</div>
|
| 720 |
+
<div class="chat-icon">👤</div>
|
| 721 |
+
</div>
|
| 722 |
+
""", unsafe_allow_html=True)
|
| 723 |
+
|
| 724 |
+
# Mostrar respuesta del asistente
|
| 725 |
+
with messages:
|
| 726 |
+
with st.spinner("Pensando..."):
|
| 727 |
+
completion = callDeepseek(client, prompt)
|
| 728 |
+
response = ""
|
| 729 |
+
response_placeholder = st.empty()
|
| 730 |
+
|
| 731 |
+
for chunk in completion:
|
| 732 |
+
content = chunk.choices[0].delta.content or ""
|
| 733 |
+
response += content
|
| 734 |
+
|
| 735 |
+
response_placeholder.markdown(f"""
|
| 736 |
+
<div class="chat-message assistant">
|
| 737 |
+
<div class="chat-icon">🤖</div>
|
| 738 |
+
<div class="chat-bubble assistant">{response}</div>
|
| 739 |
+
</div>
|
| 740 |
+
""", unsafe_allow_html=True)
|
| 741 |
+
|
| 742 |
+
st.session_state.chat_history.append({"role": "assistant", "content": response})
|
| 743 |
+
|
| 744 |
+
# Grabación de audio (solo si está habilitada)
|
| 745 |
+
if st.session_state.mostrar_grabador and st.session_state.audio_data is None:
|
| 746 |
+
audio_data = st.audio_input("Graba tu voz aquí 🎤")
|
| 747 |
+
if audio_data:
|
| 748 |
+
st.session_state.audio_data = audio_data
|
| 749 |
+
st.session_state.mostrar_grabador = False # Ocultar input después de grabar
|
| 750 |
+
st.rerun() # Forzar recarga para ocultar input y evitar que reaparezca el audio cargado
|
| 751 |
+
|
| 752 |
+
# Mostrar controles solo si hay audio cargado
|
| 753 |
+
if st.session_state.audio_data:
|
| 754 |
+
st.audio(st.session_state.audio_data, format="audio/wav")
|
| 755 |
+
col1, col2 = st.columns(2)
|
| 756 |
+
|
| 757 |
+
with col1:
|
| 758 |
+
if st.button("✅ Aceptar grabación"):
|
| 759 |
+
with st.spinner("Convirtiendo y transcribiendo..."):
|
| 760 |
+
m4a_path = converter_bytes_m4a(st.session_state.audio_data)
|
| 761 |
+
|
| 762 |
+
with open(m4a_path, "rb") as f:
|
| 763 |
+
texto = callWhisper(client, m4a_path, f)
|
| 764 |
+
|
| 765 |
+
os.remove(m4a_path)
|
| 766 |
+
|
| 767 |
+
st.session_state.transcripcion = texto
|
| 768 |
+
st.session_state.audio_data = None
|
| 769 |
+
st.session_state.mostrar_grabador = True
|
| 770 |
+
st.rerun()
|
| 771 |
+
|
| 772 |
+
with col2:
|
| 773 |
+
if st.button("❌ Descartar grabación"):
|
| 774 |
+
st.session_state.audio_data = None
|
| 775 |
+
st.session_state.transcripcion = ""
|
| 776 |
+
st.session_state.mostrar_grabador = True
|
| 777 |
+
st.rerun()
|
| 778 |
+
|
| 779 |
+
|
| 780 |
+
def callDeepseek(client, prompt):
|
| 781 |
+
completion = client.chat.completions.create(
|
| 782 |
+
model="deepseek-r1-distill-llama-70b",
|
| 783 |
+
messages=[
|
| 784 |
+
{
|
| 785 |
+
"role": "system",
|
| 786 |
+
"content": (
|
| 787 |
+
"Tu nombre es Protech, el asistente virtual de PRO TECHNOLOGY SOLUTIONS S.A.C. "
|
| 788 |
+
"Saluda al usuario con cordialidad y responde en español de forma clara, profesional y amable. "
|
| 789 |
+
"No expliques tus pensamientos ni cómo generas tus respuestas. "
|
| 790 |
+
"No digas que eres un modelo de lenguaje. "
|
| 791 |
+
"Simplemente responde como un asistente humano capacitado en atención al cliente. "
|
| 792 |
+
"Comienza con un saludo y pregunta: '¿En qué puedo ayudarte hoy?'."
|
| 793 |
+
)
|
| 794 |
+
},
|
| 795 |
+
{"role": "user", "content": prompt}
|
| 796 |
+
],
|
| 797 |
+
temperature=0.6,
|
| 798 |
+
max_tokens=4096,
|
| 799 |
+
top_p=1,
|
| 800 |
+
stream=True,
|
| 801 |
+
)
|
| 802 |
+
return completion
|
| 803 |
+
|
| 804 |
+
|
| 805 |
+
|
| 806 |
+
def callWhisper(client, filename_audio,file):
|
| 807 |
+
transcription = client.audio.transcriptions.create(
|
| 808 |
+
file=(filename_audio, file.read()),
|
| 809 |
+
model="whisper-large-v3",
|
| 810 |
+
response_format="verbose_json",
|
| 811 |
+
)
|
| 812 |
+
return transcription.text
|
| 813 |
+
|
| 814 |
+
def converter_bytes_m4a(audio_bytes: BytesIO) -> str:
|
| 815 |
+
"""
|
| 816 |
+
Convierte un audio en bytes (WAV, etc.) a un archivo M4A temporal.
|
| 817 |
+
Retorna la ruta del archivo .m4a temporal.
|
| 818 |
+
"""
|
| 819 |
+
# Asegurarse de que el cursor del stream esté al inicio
|
| 820 |
+
audio_bytes.seek(0)
|
| 821 |
+
|
| 822 |
+
# Leer el audio desde BytesIO usando pydub
|
| 823 |
+
audio = AudioSegment.from_file(audio_bytes)
|
| 824 |
+
|
| 825 |
+
# Crear archivo temporal para guardar como .m4a
|
| 826 |
+
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".m4a")
|
| 827 |
+
m4a_path = temp_file.name
|
| 828 |
+
temp_file.close() # Cerramos para que pydub pueda escribirlo
|
| 829 |
+
|
| 830 |
+
# Exportar a M4A usando formato compatible con ffmpeg
|
| 831 |
+
audio.export(m4a_path, format="ipod") # 'ipod' genera .m4a
|
| 832 |
+
|
| 833 |
+
return m4a_path
|
| 834 |
+
|
| 835 |
+
# ===========================
|
| 836 |
+
# Función para obtener el número de periodos basado en el filtro
|
| 837 |
+
# ===========================
|
| 838 |
+
def obtener_periodos(filtro):
|
| 839 |
+
opciones_periodos = {
|
| 840 |
+
'5 años': 60,
|
| 841 |
+
'3 años': 36,
|
| 842 |
+
'1 año': 12,
|
| 843 |
+
'5 meses': 5
|
| 844 |
+
}
|
| 845 |
+
return opciones_periodos.get(filtro, 12)
|
| 846 |
+
|
| 847 |
+
|
| 848 |
+
# ===========================
|
| 849 |
+
# Función para generar predicciones
|
| 850 |
+
# ===========================
|
| 851 |
+
def generar_predicciones(modelo, df, periodos):
|
| 852 |
+
ventas = df['Sale']
|
| 853 |
+
predicciones = modelo.forecast(steps=periodos)
|
| 854 |
+
return predicciones
|
| 855 |
+
|
| 856 |
+
# Función para mejorar el diseño de las gráficas
|
| 857 |
+
def mejorar_diseno_grafica(fig, meses_relevantes, nombres_meses_relevantes):
|
| 858 |
+
fig.update_layout(
|
| 859 |
+
title={
|
| 860 |
+
'text': "Ventas vs Mes",
|
| 861 |
+
|
| 862 |
+
'x': 0.5, # Centrado horizontal
|
| 863 |
+
'xanchor': 'center', # Asegura el anclaje central
|
| 864 |
+
'yanchor': 'top' # Anclaje superior (opcional)
|
| 865 |
+
},
|
| 866 |
+
title_font=dict(size=18, family="Arial, sans-serif", color='black'),
|
| 867 |
+
xaxis=dict(
|
| 868 |
+
title='Mes-Año',
|
| 869 |
+
title_font=dict(size=14, family="Arial, sans-serif", color='black'),
|
| 870 |
+
tickangle=-45, # Rotar las etiquetas
|
| 871 |
+
showgrid=True,
|
| 872 |
+
gridwidth=0.5,
|
| 873 |
+
gridcolor='lightgrey',
|
| 874 |
+
showline=True,
|
| 875 |
+
linecolor='black',
|
| 876 |
+
linewidth=2,
|
| 877 |
+
tickmode='array', # Controla qué etiquetas mostrar
|
| 878 |
+
tickvals=meses_relevantes, # Selecciona solo los meses relevantes
|
| 879 |
+
ticktext=nombres_meses_relevantes, # Meses seleccionados
|
| 880 |
+
tickfont=dict(size=10), # Reducir el tamaño de la fuente de las etiquetas
|
| 881 |
+
),
|
| 882 |
+
yaxis=dict(
|
| 883 |
+
title='Ventas',
|
| 884 |
+
title_font=dict(size=14, family="Arial, sans-serif", color='black'),
|
| 885 |
+
showgrid=True,
|
| 886 |
+
gridwidth=0.5,
|
| 887 |
+
gridcolor='lightgrey',
|
| 888 |
+
showline=True,
|
| 889 |
+
linecolor='black',
|
| 890 |
+
linewidth=2
|
| 891 |
+
),
|
| 892 |
+
plot_bgcolor='white', # Fondo blanco
|
| 893 |
+
paper_bgcolor='white', # Fondo del lienzo de la gráfica
|
| 894 |
+
font=dict(family="Arial, sans-serif", size=12, color="black"),
|
| 895 |
+
showlegend=False, # Desactivar la leyenda si no es necesaria
|
| 896 |
+
margin=dict(l=50, r=50, t=50, b=50) # Márgenes ajustados
|
| 897 |
+
)
|
| 898 |
+
|
| 899 |
+
|
| 900 |
+
|
| 901 |
+
return fig
|
| 902 |
+
|
| 903 |
+
# ===========================
|
| 904 |
+
# Función para cerrar sesión
|
| 905 |
+
# ===========================
|
| 906 |
+
def cerrar_sesion():
|
| 907 |
+
st.session_state.logged_in = False
|
| 908 |
+
st.session_state.usuario = None
|
| 909 |
+
st.session_state.pagina_actual = "login"
|
| 910 |
+
st.session_state.archivo_subido = False # Limpiar el archivo subido al cerrar sesión
|
| 911 |
+
st.session_state.df_subido = None # Limpiar datos del archivo
|
| 912 |
+
# Eliminar parámetros de la URL usando st.query_params
|
| 913 |
+
st.query_params.clear() # Método correcto para limpiar parámetros de consulta
|
| 914 |
+
|
| 915 |
+
# Redirigir a la página de login
|
| 916 |
+
st.rerun()
|
paginas/demo.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from base64 import b64encode
|
| 2 |
+
from fpdf import FPDF
|
| 3 |
+
import streamlit as st
|
| 4 |
+
|
| 5 |
+
st.title("Demo of fpdf2 usage with streamlit")
|
| 6 |
+
|
| 7 |
+
@st.cache_data
|
| 8 |
+
def gen_pdf():
|
| 9 |
+
pdf = FPDF()
|
| 10 |
+
pdf.add_page()
|
| 11 |
+
pdf.set_font("Helvetica", size=24)
|
| 12 |
+
pdf.cell(w=40,h=10,border=1,txt="hello world")
|
| 13 |
+
return pdf.output(dest='S').encode('latin1')
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
# Embed PDF to display it:
|
| 17 |
+
base64_pdf = b64encode(gen_pdf()).decode("utf-8")
|
| 18 |
+
pdf_display = f'<embed src="data:application/pdf;base64,{base64_pdf}" width="700" height="400" type="application/pdf">'
|
| 19 |
+
st.markdown(pdf_display, unsafe_allow_html=True)
|
| 20 |
+
|
| 21 |
+
# Add a download button:
|
| 22 |
+
st.download_button(
|
| 23 |
+
label="Download PDF",
|
| 24 |
+
data=gen_pdf(),
|
| 25 |
+
file_name="file_name.pdf",
|
| 26 |
+
mime="application/pdf",
|
| 27 |
+
)
|
paginas/demokaleido.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import plotly.express as px
|
| 2 |
+
|
| 3 |
+
fig = px.line(x=[1, 2, 3], y=[4, 5, 6])
|
| 4 |
+
fig.write_image("test_fig.png", width=900, height=500)
|
| 5 |
+
print("✅ Imagen guardada correctamente")
|
paginas/images/Logo dashboard.png
ADDED
|
Git LFS Details
|
paginas/images/Logo general.png
ADDED
|
paginas/images/Logo.png
ADDED
|
paginas/login.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import json
|
| 4 |
+
from streamlit_lottie import st_lottie
|
| 5 |
+
import os
|
| 6 |
+
import time
|
| 7 |
+
from .userManagement import verifyCredentials
|
| 8 |
+
|
| 9 |
+
def validateCredentials(usuario, contrasena):
|
| 10 |
+
return verifyCredentials(usuario, contrasena)
|
| 11 |
+
|
| 12 |
+
def load_lottiefile(filepath: str):
|
| 13 |
+
with open(filepath, "r") as f:
|
| 14 |
+
return json.load(f)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def showLogin():
|
| 18 |
+
|
| 19 |
+
c1, c2 = st.columns([60, 40])
|
| 20 |
+
|
| 21 |
+
with c1:
|
| 22 |
+
# Ajusta la ruta al archivo JSON de animación
|
| 23 |
+
ruta_animacion_laptop = os.path.join("animations", "laptopUser.json")
|
| 24 |
+
lottie_coding = load_lottiefile(ruta_animacion_laptop)
|
| 25 |
+
st_lottie(
|
| 26 |
+
lottie_coding,
|
| 27 |
+
speed=1,
|
| 28 |
+
reverse=False,
|
| 29 |
+
loop=True,
|
| 30 |
+
quality="low",
|
| 31 |
+
height=None,
|
| 32 |
+
width=None,
|
| 33 |
+
key=None,
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
with c2:
|
| 37 |
+
st.title("🔐 Inicio de :blue[Sesión] :sunglasses:")
|
| 38 |
+
|
| 39 |
+
# Formulario de inicio de sesión
|
| 40 |
+
with st.form("login_form"):
|
| 41 |
+
usuario = st.text_input("Usuario 👇")
|
| 42 |
+
contrasena = st.text_input("Contraseña 👇", type="password")
|
| 43 |
+
boton_login = st.form_submit_button("Iniciar Sesión", type="primary",use_container_width=True)
|
| 44 |
+
|
| 45 |
+
# Validación de credenciales
|
| 46 |
+
if boton_login:
|
| 47 |
+
if validateCredentials(usuario, contrasena):
|
| 48 |
+
st.session_state.logged_in = True
|
| 49 |
+
st.session_state.usuario = usuario
|
| 50 |
+
aviso = st.success("Inicio de sesión exitoso. Redirigiendo al dashboard...")
|
| 51 |
+
time.sleep(3)
|
| 52 |
+
aviso.empty()
|
| 53 |
+
# Simular redirección recargando el flujo principal
|
| 54 |
+
st.session_state.pagina_actual = "dashboard"
|
| 55 |
+
st.rerun()
|
| 56 |
+
else:
|
| 57 |
+
aviso = st.error("Usuario o contraseña incorrectos")
|
| 58 |
+
time.sleep(3)
|
| 59 |
+
aviso.empty()
|
paginas/userManagement.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import MySQLdb
|
| 2 |
+
from .conexionMysql import get_db_connection
|
| 3 |
+
|
| 4 |
+
def verifyCredentials(username: str, password: str) -> bool:
|
| 5 |
+
"""
|
| 6 |
+
Verifica si las credenciales del usuario son válidas.
|
| 7 |
+
Retorna True si el usuario existe y la contraseña es correcta.
|
| 8 |
+
"""
|
| 9 |
+
try:
|
| 10 |
+
with get_db_connection() as conn:
|
| 11 |
+
cursor = conn.cursor()
|
| 12 |
+
query = "SELECT COUNT(*) FROM usuarios WHERE username = %s AND password = %s"
|
| 13 |
+
cursor.execute(query, (username, password))
|
| 14 |
+
resultado = cursor.fetchone()
|
| 15 |
+
return resultado[0] > 0
|
| 16 |
+
except MySQLdb.Error as e:
|
| 17 |
+
print(f"Error en la verificación de credenciales: {e}")
|
| 18 |
+
return False
|
| 19 |
+
|
| 20 |
+
def getDataUser(correo: str) -> dict | None:
|
| 21 |
+
"""
|
| 22 |
+
Devuelve un diccionario con los datos del usuario si existe, o None si no se encuentra.
|
| 23 |
+
"""
|
| 24 |
+
try:
|
| 25 |
+
with get_db_connection() as conn:
|
| 26 |
+
cursor = conn.cursor(MySQLdb.cursors.DictCursor)
|
| 27 |
+
query = "SELECT nombre, apellido, correo, telefono FROM usuarios WHERE correo = %s"
|
| 28 |
+
cursor.execute(query, (correo,))
|
| 29 |
+
return cursor.fetchone()
|
| 30 |
+
except MySQLdb.Error as e:
|
| 31 |
+
print(f"Error al obtener datos del usuario: {e}")
|
| 32 |
+
return None
|