IvanStudent commited on
Commit
103ce4f
·
verified ·
1 Parent(s): 58b7971

Upload 12 files

Browse files
.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

  • SHA256: 02512c7d3aa1a268ef6fde26dba8c0791bb9718212877bafa4bf9e656b374b64
  • Pointer size: 131 Bytes
  • Size of remote file: 387 kB
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