Spaces:
Running
Running
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
|