AIdeaText commited on
Commit
4a2b4af
·
1 Parent(s): a74a9a9

update sync activ

Browse files
modules/studentact/student_activities_v2.py CHANGED
@@ -33,7 +33,6 @@ logger = logging.getLogger(__name__)
33
 
34
  ###################################################################################
35
 
36
-
37
  def display_student_activities(username: str, lang_code: str, t: dict):
38
  """
39
  Muestra todas las actividades del estudiante
@@ -76,7 +75,6 @@ def display_student_activities(username: str, lang_code: str, t: dict):
76
 
77
 
78
  ###############################################################################################
79
-
80
  def display_semantic_live_activities(username: str, t: dict):
81
  """Muestra actividades de análisis semántico en vivo (CORREGIDO)"""
82
  try:
@@ -86,43 +84,50 @@ def display_semantic_live_activities(username: str, t: dict):
86
  st.info(t.get('no_semantic_live_analyses', 'No hay análisis semánticos en vivo registrados'))
87
  return
88
 
89
- for analysis in analyses:
90
  try:
91
- # Manejar formato de fecha (CORREGIDO)
92
- if isinstance(analysis['timestamp'], str):
93
- timestamp = datetime.fromisoformat(analysis['timestamp'].replace('Z', '+00:00'))
 
 
 
 
94
  else:
95
- timestamp = analysis['timestamp']
96
 
97
  formatted_date = timestamp.strftime("%d/%m/%Y %H:%M:%S")
98
 
 
 
 
99
  with st.expander(f"{t.get('analysis_date', 'Fecha')}: {formatted_date}", expanded=False):
100
- # Mostrar texto (primeros 200 caracteres)
 
101
  st.text_area(
102
- "Texto analizado",
103
- value=analysis.get('text', '')[:200] + ("..." if len(analysis.get('text', '')) > 200 else ""),
104
- height=100,
105
- disabled=True
 
106
  )
107
 
108
- # Mostrar gráfico si existe (CORREGIDO)
109
  if analysis.get('concept_graph'):
110
  try:
111
  # Manejar diferentes formatos de imagen
112
- if isinstance(analysis['concept_graph'], bytes):
113
- st.image(
114
- analysis['concept_graph'],
115
- caption=t.get('concept_network', 'Red de Conceptos'),
116
- use_container_width=True
117
- )
118
- elif isinstance(analysis['concept_graph'], str):
119
  # Decodificar si está en base64
120
- image_bytes = base64.b64decode(analysis['concept_graph'])
121
- st.image(
122
- image_bytes,
123
- caption=t.get('concept_network', 'Red de Conceptos'),
124
- use_container_width=True
125
- )
 
126
  except Exception as img_error:
127
  logger.error(f"Error procesando gráfico: {str(img_error)}")
128
  st.error(t.get('error_loading_graph', 'Error al cargar el gráfico'))
@@ -139,7 +144,7 @@ def display_semantic_live_activities(username: str, t: dict):
139
  ###############################################################################################
140
 
141
  def display_semantic_activities(username: str, t: dict):
142
- """Muestra actividades de análisis semántico"""
143
  try:
144
  logger.info(f"Recuperando análisis semántico para {username}")
145
  analyses = get_student_semantic_analysis(username)
@@ -151,42 +156,50 @@ def display_semantic_activities(username: str, t: dict):
151
 
152
  logger.info(f"Procesando {len(analyses)} análisis semánticos")
153
 
154
- for analysis in analyses:
 
155
  try:
156
- # Verificar campos necesarios
157
  if not all(key in analysis for key in ['timestamp', 'concept_graph']):
158
  logger.warning(f"Análisis incompleto: {analysis.keys()}")
159
  continue
160
 
161
- # Formatear fecha
162
- timestamp = datetime.fromisoformat(analysis['timestamp'].replace('Z', '+00:00'))
 
 
 
 
 
 
163
  formatted_date = timestamp.strftime("%d/%m/%Y %H:%M:%S")
164
 
165
- # Crear expander
166
- with st.expander(f"{t.get('analysis_date', 'Fecha')}: {formatted_date}", expanded=False):
167
- # Procesar y mostrar gráfico
 
 
 
 
 
 
168
  if analysis.get('concept_graph'):
169
  try:
170
- # Convertir de base64 a bytes
171
- logger.debug("Decodificando gráfico de conceptos")
172
  image_data = analysis['concept_graph']
173
 
174
- # Si el gráfico ya es bytes, usarlo directamente
175
  if isinstance(image_data, bytes):
176
  image_bytes = image_data
177
  else:
178
- # Si es string base64, decodificar
179
  image_bytes = base64.b64decode(image_data)
180
 
181
- logger.debug(f"Longitud de bytes de imagen: {len(image_bytes)}")
182
-
183
- # Mostrar imagen
184
  st.image(
185
  image_bytes,
186
  caption=t.get('concept_network', 'Red de Conceptos'),
187
- use_container_width=True
188
  )
189
- logger.debug("Gráfico mostrado exitosamente")
190
 
191
  except Exception as img_error:
192
  logger.error(f"Error procesando gráfico: {str(img_error)}")
@@ -202,46 +215,50 @@ def display_semantic_activities(username: str, t: dict):
202
  logger.error(f"Error mostrando análisis semántico: {str(e)}")
203
  st.error(t.get('error_semantic', 'Error al mostrar análisis semántico'))
204
 
205
-
206
  ###################################################################################################
207
 
208
  def display_discourse_activities(username: str, t: dict):
209
- """Muestra actividades de análisis del discurso (mostrado como 'Análisis comparado de textos' en la UI)"""
210
  try:
211
  logger.info(f"Recuperando análisis del discurso para {username}")
212
  analyses = get_student_discourse_analysis(username)
213
 
214
  if not analyses:
215
  logger.info("No se encontraron análisis del discurso")
216
- # Usamos el término "análisis comparado de textos" en la UI
217
  st.info(t.get('no_discourse_analyses', 'No hay análisis comparados de textos registrados'))
218
  return
219
 
220
  logger.info(f"Procesando {len(analyses)} análisis del discurso")
221
- for analysis in analyses:
222
  try:
223
- # Verificar campos mínimos necesarios
224
  if not all(key in analysis for key in ['timestamp']):
225
  logger.warning(f"Análisis incompleto: {analysis.keys()}")
226
  continue
227
 
228
- # Formatear fecha
229
- timestamp = datetime.fromisoformat(analysis['timestamp'].replace('Z', '+00:00'))
 
 
 
 
 
230
  formatted_date = timestamp.strftime("%d/%m/%Y %H:%M:%S")
231
 
 
 
 
232
  with st.expander(f"{t.get('analysis_date', 'Fecha')}: {formatted_date}", expanded=False):
233
- # Crear dos columnas para mostrar los documentos lado a lado
234
  col1, col2 = st.columns(2)
235
 
236
- # Documento 1 - Columna izquierda
237
  with col1:
238
  st.subheader(t.get('doc1_title', 'Documento 1'))
239
- st.markdown(t.get('key_concepts', 'Conceptos Clave'))
240
 
241
- # Mostrar conceptos clave en formato de etiquetas
242
  if 'key_concepts1' in analysis and analysis['key_concepts1']:
 
243
  concepts_html = f"""
244
- <div style="display: flex; flex-wrap: nowrap; gap: 8px; padding: 12px;
245
  background-color: #f8f9fa; border-radius: 8px; overflow-x: auto;
246
  margin-bottom: 15px; white-space: nowrap;">
247
  {''.join([
@@ -253,44 +270,24 @@ def display_discourse_activities(username: str, t: dict):
253
  </div>
254
  """
255
  st.markdown(concepts_html, unsafe_allow_html=True)
256
- else:
257
- st.info(t.get('no_concepts', 'No hay conceptos disponibles'))
258
 
259
- # Mostrar grafo 1
260
  if 'graph1' in analysis:
261
  try:
262
- if isinstance(analysis['graph1'], bytes):
263
- st.image(
264
- analysis['graph1'],
265
- use_container_width=True
266
- )
267
- else:
268
- logger.warning(f"graph1 no es bytes: {type(analysis['graph1'])}")
269
- st.warning(t.get('graph_not_available', 'Gráfico no disponible'))
270
  except Exception as e:
271
  logger.error(f"Error mostrando graph1: {str(e)}")
272
  st.error(t.get('error_loading_graph', 'Error al cargar el gráfico'))
273
- else:
274
- st.info(t.get('no_visualization', 'No hay visualización disponible'))
275
-
276
- # Interpretación del grafo
277
- st.markdown("**📊 Interpretación del grafo:**")
278
- st.markdown("""
279
- - 🔀 Las flechas indican la dirección de la relación entre conceptos
280
- - 🎨 Los colores más intensos indican conceptos más centrales en el texto
281
- - ⭕ El tamaño de los nodos representa la frecuencia del concepto
282
- - ↔️ El grosor de las líneas indica la fuerza de la conexión
283
- """)
284
-
285
- # Documento 2 - Columna derecha
286
  with col2:
287
  st.subheader(t.get('doc2_title', 'Documento 2'))
288
- st.markdown(t.get('key_concepts', 'Conceptos Clave'))
289
 
290
- # Mostrar conceptos clave en formato de etiquetas
291
  if 'key_concepts2' in analysis and analysis['key_concepts2']:
292
- concepts_html = f"""
293
- <div style="display: flex; flex-wrap: nowrap; gap: 8px; padding: 12px;
294
  background-color: #f8f9fa; border-radius: 8px; overflow-x: auto;
295
  margin-bottom: 15px; white-space: nowrap;">
296
  {''.join([
@@ -301,35 +298,18 @@ def display_discourse_activities(username: str, t: dict):
301
  ])}
302
  </div>
303
  """
304
- st.markdown(concepts_html, unsafe_allow_html=True)
305
- else:
306
- st.info(t.get('no_concepts', 'No hay conceptos disponibles'))
307
 
308
- # Mostrar grafo 2
309
  if 'graph2' in analysis:
310
  try:
311
- if isinstance(analysis['graph2'], bytes):
312
- st.image(
313
- analysis['graph2'],
314
- use_container_width=True
315
- )
316
- else:
317
- logger.warning(f"graph2 no es bytes: {type(analysis['graph2'])}")
318
- st.warning(t.get('graph_not_available', 'Gráfico no disponible'))
319
  except Exception as e:
320
  logger.error(f"Error mostrando graph2: {str(e)}")
321
  st.error(t.get('error_loading_graph', 'Error al cargar el gráfico'))
322
- else:
323
- st.info(t.get('no_visualization', 'No hay visualización disponible'))
324
-
325
- # Interpretación del grafo
326
- st.markdown("**📊 Interpretación del grafo:**")
327
- st.markdown("""
328
- - 🔀 Las flechas indican la dirección de la relación entre conceptos
329
- - 🎨 Los colores más intensos indican conceptos más centrales en el texto
330
- - ⭕ El tamaño de los nodos representa la frecuencia del concepto
331
- - ↔️ El grosor de las líneas indica la fuerza de la conexión
332
- """)
333
 
334
  except Exception as e:
335
  logger.error(f"Error procesando análisis individual: {str(e)}")
@@ -337,58 +317,59 @@ def display_discourse_activities(username: str, t: dict):
337
 
338
  except Exception as e:
339
  logger.error(f"Error mostrando análisis del discurso: {str(e)}")
340
- # Usamos el término "análisis comparado de textos" en la UI
341
  st.error(t.get('error_discourse', 'Error al mostrar análisis comparado de textos'))
342
 
343
-
344
-
345
  #################################################################################
346
 
347
  def display_discourse_comparison(analysis: dict, t: dict):
348
  """
349
  Muestra la comparación de conceptos clave en análisis del discurso.
350
- Formato horizontal simplificado.
351
  """
352
  st.subheader(t.get('comparison_results', 'Resultados de la comparación'))
353
 
354
  # Verificar si tenemos los conceptos necesarios
355
- if not ('key_concepts1' in analysis and analysis['key_concepts1']):
356
  st.info(t.get('no_concepts', 'No hay conceptos disponibles para comparar'))
357
  return
358
 
359
- # Conceptos del Texto 1 - Formato horizontal
360
- st.markdown(f"**{t.get('concepts_text_1', 'Conceptos Texto 1')}:**")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
361
  try:
362
- # Comprobar formato y mostrar horizontalmente
363
- if isinstance(analysis['key_concepts1'], list) and len(analysis['key_concepts1']) > 0:
364
- if isinstance(analysis['key_concepts1'][0], list) and len(analysis['key_concepts1'][0]) == 2:
365
- # Formatear como "concepto (valor), concepto2 (valor2), ..."
366
- concepts_text = ", ".join([f"{c[0]} ({c[1]})" for c in analysis['key_concepts1'][:10]])
367
- st.markdown(f"*{concepts_text}*")
368
- else:
369
- # Si no tiene el formato esperado, mostrar como lista simple
370
- st.markdown(", ".join(str(c) for c in analysis['key_concepts1'][:10]))
371
- else:
372
- st.write(str(analysis['key_concepts1']))
373
  except Exception as e:
374
  logger.error(f"Error mostrando key_concepts1: {str(e)}")
375
  st.error(t.get('error_concepts1', 'Error mostrando conceptos del Texto 1'))
376
 
377
- # Conceptos del Texto 2 - Formato horizontal
378
- st.markdown(f"**{t.get('concepts_text_2', 'Conceptos Texto 2')}:**")
379
- if 'key_concepts2' in analysis and analysis['key_concepts2']:
 
 
380
  try:
381
- # Comprobar formato y mostrar horizontalmente
382
- if isinstance(analysis['key_concepts2'], list) and len(analysis['key_concepts2']) > 0:
383
- if isinstance(analysis['key_concepts2'][0], list) and len(analysis['key_concepts2'][0]) == 2:
384
- # Formatear como "concepto (valor), concepto2 (valor2), ..."
385
- concepts_text = ", ".join([f"{c[0]} ({c[1]})" for c in analysis['key_concepts2'][:10]])
386
- st.markdown(f"*{concepts_text}*")
387
- else:
388
- # Si no tiene el formato esperado, mostrar como lista simple
389
- st.markdown(", ".join(str(c) for c in analysis['key_concepts2'][:10]))
390
- else:
391
- st.write(str(analysis['key_concepts2']))
392
  except Exception as e:
393
  logger.error(f"Error mostrando key_concepts2: {str(e)}")
394
  st.error(t.get('error_concepts2', 'Error mostrando conceptos del Texto 2'))
@@ -396,7 +377,6 @@ def display_discourse_comparison(analysis: dict, t: dict):
396
  st.info(t.get('no_concepts2', 'No hay conceptos disponibles para el Texto 2'))
397
 
398
 
399
-
400
  #################################################################################
401
  def clean_chat_content(content: str) -> str:
402
  """Limpia caracteres especiales del contenido del chat"""
@@ -415,7 +395,7 @@ def clean_chat_content(content: str) -> str:
415
  #################################################################################
416
  def display_chat_activities(username: str, t: dict):
417
  """
418
- Muestra historial de conversaciones del chat
419
  """
420
  try:
421
  # Obtener historial del chat
@@ -429,41 +409,57 @@ def display_chat_activities(username: str, t: dict):
429
  st.info(t.get('no_chat_history', 'No hay conversaciones registradas'))
430
  return
431
 
432
- for chat in reversed(chat_history): # Mostrar las más recientes primero
 
433
  try:
434
- # Convertir timestamp a datetime para formato
435
- timestamp = datetime.fromisoformat(chat['timestamp'].replace('Z', '+00:00'))
 
 
 
 
 
 
 
436
  formatted_date = timestamp.strftime("%d/%m/%Y %H:%M:%S")
437
 
 
 
 
438
  with st.expander(
439
- f"{t.get('chat_date', 'Fecha de conversación')}: {formatted_date}",
440
  expanded=False
441
  ):
442
  if 'messages' in chat and chat['messages']:
443
- # Mostrar cada mensaje en la conversación
444
- for message in chat['messages']:
445
  role = message.get('role', 'unknown')
446
- content = clean_chat_content(message.get('content', ''))
 
 
 
 
 
 
 
447
 
448
- # Usar el componente de chat de Streamlit
449
  with st.chat_message(role):
450
  st.markdown(content)
451
 
452
- # Agregar separador entre mensajes
453
- st.divider()
 
454
  else:
455
  st.warning(t.get('invalid_chat_format', 'Formato de chat no válido'))
456
 
457
  except Exception as e:
458
- logger.error(f"Error mostrando conversación: {str(e)}")
459
  continue
460
 
461
  except Exception as e:
462
  logger.error(f"Error mostrando historial del chat: {str(e)}")
463
  st.error(t.get('error_chat', 'Error al mostrar historial del chat'))
464
 
465
-
466
-
467
  #################################################################################
468
 
469
 
 
33
 
34
  ###################################################################################
35
 
 
36
  def display_student_activities(username: str, lang_code: str, t: dict):
37
  """
38
  Muestra todas las actividades del estudiante
 
75
 
76
 
77
  ###############################################################################################
 
78
  def display_semantic_live_activities(username: str, t: dict):
79
  """Muestra actividades de análisis semántico en vivo (CORREGIDO)"""
80
  try:
 
84
  st.info(t.get('no_semantic_live_analyses', 'No hay análisis semánticos en vivo registrados'))
85
  return
86
 
87
+ for i, analysis in enumerate(analyses):
88
  try:
89
+ # 1. Manejar formato de fecha (Optimizado para objetos datetime nativos)
90
+ # Usamos el objeto directamente si ya es datetime, si es string lo convertimos
91
+ ts_raw = analysis.get('timestamp')
92
+ if isinstance(ts_raw, datetime):
93
+ timestamp = ts_raw
94
+ elif isinstance(ts_raw, str):
95
+ timestamp = datetime.fromisoformat(ts_raw.replace('Z', '+00:00'))
96
  else:
97
+ timestamp = datetime.now() # Fallback por seguridad
98
 
99
  formatted_date = timestamp.strftime("%d/%m/%Y %H:%M:%S")
100
 
101
+ # Usamos el ID de MongoDB o el índice como sufijo para asegurar unicidad absoluta
102
+ unique_id = str(analysis.get('_id', i))
103
+
104
  with st.expander(f"{t.get('analysis_date', 'Fecha')}: {formatted_date}", expanded=False):
105
+ # 2. SOLUCIÓN AL ERROR DE KEY DUPLICADO
106
+ # Agregamos 'key' usando generate_unique_key con un sufijo único
107
  st.text_area(
108
+ t.get('analyzed_text', 'Texto analizado'),
109
+ value=analysis.get('text', '')[:500], # Aumentado un poco para mejor lectura
110
+ height=150,
111
+ disabled=True,
112
+ key=generate_unique_key("sem_live", "text", username, suffix=unique_id)
113
  )
114
 
115
+ # 3. Mostrar gráfico si existe
116
  if analysis.get('concept_graph'):
117
  try:
118
  # Manejar diferentes formatos de imagen
119
+ graph_data = analysis['concept_graph']
120
+ if isinstance(graph_data, bytes):
121
+ image_to_show = graph_data
122
+ elif isinstance(graph_data, str):
 
 
 
123
  # Decodificar si está en base64
124
+ image_to_show = base64.b64decode(graph_data)
125
+
126
+ st.image(
127
+ image_to_show,
128
+ caption=t.get('concept_network', 'Red de Conceptos'),
129
+ use_container_width=True # Ajustado según tus logs de advertencia
130
+ )
131
  except Exception as img_error:
132
  logger.error(f"Error procesando gráfico: {str(img_error)}")
133
  st.error(t.get('error_loading_graph', 'Error al cargar el gráfico'))
 
144
  ###############################################################################################
145
 
146
  def display_semantic_activities(username: str, t: dict):
147
+ """Muestra actividades de análisis semántico (ACTUALIZADO)"""
148
  try:
149
  logger.info(f"Recuperando análisis semántico para {username}")
150
  analyses = get_student_semantic_analysis(username)
 
156
 
157
  logger.info(f"Procesando {len(analyses)} análisis semánticos")
158
 
159
+ # Usamos enumerate para tener un índice de respaldo
160
+ for i, analysis in enumerate(analyses):
161
  try:
162
+ # 1. Validación de campos críticos
163
  if not all(key in analysis for key in ['timestamp', 'concept_graph']):
164
  logger.warning(f"Análisis incompleto: {analysis.keys()}")
165
  continue
166
 
167
+ # 2. Manejo de Fecha (Híbrido: Objeto Date o String ISO)
168
+ ts_raw = analysis['timestamp']
169
+ if isinstance(ts_raw, datetime):
170
+ timestamp = ts_raw
171
+ else:
172
+ # Por si hay registros antiguos en formato texto
173
+ timestamp = datetime.fromisoformat(str(ts_raw).replace('Z', '+00:00'))
174
+
175
  formatted_date = timestamp.strftime("%d/%m/%Y %H:%M:%S")
176
 
177
+ # 3. Generar ID único para los widgets internos
178
+ unique_id = str(analysis.get('_id', i))
179
+
180
+ # Crear expander con el ID único en el key por seguridad
181
+ with st.expander(
182
+ f"{t.get('analysis_date', 'Fecha')}: {formatted_date}",
183
+ expanded=False
184
+ ):
185
+ # 4. Procesar y mostrar gráfico
186
  if analysis.get('concept_graph'):
187
  try:
 
 
188
  image_data = analysis['concept_graph']
189
 
190
+ # Decodificación robusta
191
  if isinstance(image_data, bytes):
192
  image_bytes = image_data
193
  else:
 
194
  image_bytes = base64.b64decode(image_data)
195
 
196
+ # 5. Corrección de 'use_container_width' según tus logs (BugOne.txt)
197
+ # El log sugiere usar width='stretch' para versiones nuevas
 
198
  st.image(
199
  image_bytes,
200
  caption=t.get('concept_network', 'Red de Conceptos'),
201
+ width='stretch'
202
  )
 
203
 
204
  except Exception as img_error:
205
  logger.error(f"Error procesando gráfico: {str(img_error)}")
 
215
  logger.error(f"Error mostrando análisis semántico: {str(e)}")
216
  st.error(t.get('error_semantic', 'Error al mostrar análisis semántico'))
217
 
 
218
  ###################################################################################################
219
 
220
  def display_discourse_activities(username: str, t: dict):
221
+ """Muestra actividades de análisis del discurso (Análisis comparado)"""
222
  try:
223
  logger.info(f"Recuperando análisis del discurso para {username}")
224
  analyses = get_student_discourse_analysis(username)
225
 
226
  if not analyses:
227
  logger.info("No se encontraron análisis del discurso")
 
228
  st.info(t.get('no_discourse_analyses', 'No hay análisis comparados de textos registrados'))
229
  return
230
 
231
  logger.info(f"Procesando {len(analyses)} análisis del discurso")
232
+ for i, analysis in enumerate(analyses):
233
  try:
 
234
  if not all(key in analysis for key in ['timestamp']):
235
  logger.warning(f"Análisis incompleto: {analysis.keys()}")
236
  continue
237
 
238
+ # 1. Manejo Híbrido de Fechas
239
+ ts_raw = analysis['timestamp']
240
+ if isinstance(ts_raw, datetime):
241
+ timestamp = ts_raw
242
+ else:
243
+ timestamp = datetime.fromisoformat(str(ts_raw).replace('Z', '+00:00'))
244
+
245
  formatted_date = timestamp.strftime("%d/%m/%Y %H:%M:%S")
246
 
247
+ # 2. ID único para los componentes de este bloque
248
+ unique_id = str(analysis.get('_id', i))
249
+
250
  with st.expander(f"{t.get('analysis_date', 'Fecha')}: {formatted_date}", expanded=False):
 
251
  col1, col2 = st.columns(2)
252
 
253
+ # --- Documento 1 ---
254
  with col1:
255
  st.subheader(t.get('doc1_title', 'Documento 1'))
256
+ st.markdown(f"**{t.get('key_concepts', 'Conceptos Clave')}**")
257
 
 
258
  if 'key_concepts1' in analysis and analysis['key_concepts1']:
259
+ # El HTML no requiere Key de Streamlit, pero es bueno que el contenedor sea único
260
  concepts_html = f"""
261
+ <div id="concepts1_{unique_id}" style="display: flex; flex-wrap: nowrap; gap: 8px; padding: 12px;
262
  background-color: #f8f9fa; border-radius: 8px; overflow-x: auto;
263
  margin-bottom: 15px; white-space: nowrap;">
264
  {''.join([
 
270
  </div>
271
  """
272
  st.markdown(concepts_html, unsafe_allow_html=True)
 
 
273
 
 
274
  if 'graph1' in analysis:
275
  try:
276
+ # 3. Ajuste de imagen y ancho
277
+ img1 = analysis['graph1']
278
+ st.image(img1, width='stretch')
 
 
 
 
 
279
  except Exception as e:
280
  logger.error(f"Error mostrando graph1: {str(e)}")
281
  st.error(t.get('error_loading_graph', 'Error al cargar el gráfico'))
282
+
283
+ # --- Documento 2 ---
 
 
 
 
 
 
 
 
 
 
 
284
  with col2:
285
  st.subheader(t.get('doc2_title', 'Documento 2'))
286
+ st.markdown(f"**{t.get('key_concepts', 'Conceptos Clave')}**")
287
 
 
288
  if 'key_concepts2' in analysis and analysis['key_concepts2']:
289
+ concepts_html2 = f"""
290
+ <div id="concepts2_{unique_id}" style="display: flex; flex-wrap: nowrap; gap: 8px; padding: 12px;
291
  background-color: #f8f9fa; border-radius: 8px; overflow-x: auto;
292
  margin-bottom: 15px; white-space: nowrap;">
293
  {''.join([
 
298
  ])}
299
  </div>
300
  """
301
+ st.markdown(concepts_html2, unsafe_allow_html=True)
 
 
302
 
 
303
  if 'graph2' in analysis:
304
  try:
305
+ img2 = analysis['graph2']
306
+ st.image(img2, width='stretch')
 
 
 
 
 
 
307
  except Exception as e:
308
  logger.error(f"Error mostrando graph2: {str(e)}")
309
  st.error(t.get('error_loading_graph', 'Error al cargar el gráfico'))
310
+
311
+ # Interpretación común para ambos
312
+ st.info("💡 **Interpretación:** Los nodos más grandes representan mayor frecuencia. El grosor de las líneas indica la fuerza de la relación semántica entre términos.")
 
 
 
 
 
 
 
 
313
 
314
  except Exception as e:
315
  logger.error(f"Error procesando análisis individual: {str(e)}")
 
317
 
318
  except Exception as e:
319
  logger.error(f"Error mostrando análisis del discurso: {str(e)}")
 
320
  st.error(t.get('error_discourse', 'Error al mostrar análisis comparado de textos'))
321
 
 
 
322
  #################################################################################
323
 
324
  def display_discourse_comparison(analysis: dict, t: dict):
325
  """
326
  Muestra la comparación de conceptos clave en análisis del discurso.
327
+ Formato horizontal simplificado con validación robusta de tipos.
328
  """
329
  st.subheader(t.get('comparison_results', 'Resultados de la comparación'))
330
 
331
  # Verificar si tenemos los conceptos necesarios
332
+ if not analysis.get('key_concepts1'):
333
  st.info(t.get('no_concepts', 'No hay conceptos disponibles para comparar'))
334
  return
335
 
336
+ # Función auxiliar interna para renderizar conceptos de forma segura
337
+ def render_concepts_horizontal(concepts_list):
338
+ if not isinstance(concepts_list, list) or len(concepts_list) == 0:
339
+ return str(concepts_list)
340
+
341
+ formatted_items = []
342
+ for item in concepts_list[:10]: # Limitamos a los 10 principales
343
+ try:
344
+ # Caso 1: [concepto, valor]
345
+ if isinstance(item, (list, tuple)) and len(item) >= 2:
346
+ val = f"{item[1]:.2f}" if isinstance(item[1], (int, float)) else str(item[1])
347
+ formatted_items.append(f"**{item[0]}** ({val})")
348
+ # Caso 2: Solo el concepto
349
+ else:
350
+ formatted_items.append(f"**{str(item)}**")
351
+ except Exception:
352
+ formatted_items.append(str(item))
353
+
354
+ return " • ".join(formatted_items)
355
+
356
+ # --- Renderizado de Conceptos Texto 1 ---
357
+ st.markdown(f"🔹 **{t.get('concepts_text_1', 'Conceptos Texto 1')}:**")
358
  try:
359
+ concepts1_html = render_concepts_horizontal(analysis['key_concepts1'])
360
+ st.markdown(concepts1_html)
 
 
 
 
 
 
 
 
 
361
  except Exception as e:
362
  logger.error(f"Error mostrando key_concepts1: {str(e)}")
363
  st.error(t.get('error_concepts1', 'Error mostrando conceptos del Texto 1'))
364
 
365
+ st.divider() # Separador visual sutil
366
+
367
+ # --- Renderizado de Conceptos Texto 2 ---
368
+ st.markdown(f"🔸 **{t.get('concepts_text_2', 'Conceptos Texto 2')}:**")
369
+ if analysis.get('key_concepts2'):
370
  try:
371
+ concepts2_html = render_concepts_horizontal(analysis['key_concepts2'])
372
+ st.markdown(concepts2_html)
 
 
 
 
 
 
 
 
 
373
  except Exception as e:
374
  logger.error(f"Error mostrando key_concepts2: {str(e)}")
375
  st.error(t.get('error_concepts2', 'Error mostrando conceptos del Texto 2'))
 
377
  st.info(t.get('no_concepts2', 'No hay conceptos disponibles para el Texto 2'))
378
 
379
 
 
380
  #################################################################################
381
  def clean_chat_content(content: str) -> str:
382
  """Limpia caracteres especiales del contenido del chat"""
 
395
  #################################################################################
396
  def display_chat_activities(username: str, t: dict):
397
  """
398
+ Muestra historial de conversaciones del chat con manejo robusto de fechas
399
  """
400
  try:
401
  # Obtener historial del chat
 
409
  st.info(t.get('no_chat_history', 'No hay conversaciones registradas'))
410
  return
411
 
412
+ # Invertir para mostrar las más recientes primero
413
+ for i, chat in enumerate(reversed(chat_history)):
414
  try:
415
+ # 1. Manejo Híbrido de Fechas (Objeto vs String)
416
+ ts_raw = chat.get('timestamp')
417
+ if isinstance(ts_raw, datetime):
418
+ timestamp = ts_raw
419
+ elif isinstance(ts_raw, str):
420
+ timestamp = datetime.fromisoformat(ts_raw.replace('Z', '+00:00'))
421
+ else:
422
+ timestamp = datetime.now() # Fallback
423
+
424
  formatted_date = timestamp.strftime("%d/%m/%Y %H:%M:%S")
425
 
426
+ # 2. ID único para el expander (previene cierres inesperados al recargar)
427
+ unique_id = str(chat.get('_id', i))
428
+
429
  with st.expander(
430
+ f"💬 {t.get('chat_date', 'Conversación')}: {formatted_date}",
431
  expanded=False
432
  ):
433
  if 'messages' in chat and chat['messages']:
434
+ # 3. Mostrar mensajes de forma limpia
435
+ for msg_idx, message in enumerate(chat['messages']):
436
  role = message.get('role', 'unknown')
437
+ # Aseguramos que el contenido sea string y esté limpio
438
+ content = str(message.get('content', ''))
439
+
440
+ # Intentar usar clean_chat_content si está disponible en el scope
441
+ try:
442
+ content = clean_chat_content(content)
443
+ except NameError:
444
+ pass
445
 
 
446
  with st.chat_message(role):
447
  st.markdown(content)
448
 
449
+ # Solo poner divisor si no es el último mensaje
450
+ if msg_idx < len(chat['messages']) - 1:
451
+ st.divider()
452
  else:
453
  st.warning(t.get('invalid_chat_format', 'Formato de chat no válido'))
454
 
455
  except Exception as e:
456
+ logger.error(f"Error mostrando conversación individual: {str(e)}")
457
  continue
458
 
459
  except Exception as e:
460
  logger.error(f"Error mostrando historial del chat: {str(e)}")
461
  st.error(t.get('error_chat', 'Error al mostrar historial del chat'))
462
 
 
 
463
  #################################################################################
464
 
465
 
modules/utils/widget_utils.py CHANGED
@@ -1,7 +1,11 @@
1
  # modules/utils/widget_utils.py
2
  import streamlit as st
3
 
4
- def generate_unique_key(module_name, element_type="input", username=None):
5
- # Si el nombre de usuario no se pasa explícitamente, lo toma de session_state
6
  username = username or st.session_state.username
7
- return f"{module_name}_{element_type}_{username}"
 
 
 
 
 
 
1
  # modules/utils/widget_utils.py
2
  import streamlit as st
3
 
4
+ def generate_unique_key(module_name, element_type="input", username=None, suffix=None):
 
5
  username = username or st.session_state.username
6
+ base_key = f"{module_name}_{element_type}_{username}"
7
+
8
+ # Si pasamos un sufijo (como un ID de la DB o un índice), lo añadimos
9
+ if suffix:
10
+ return f"{base_key}_{suffix}"
11
+ return base_key