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 (
|
| 92 |
-
|
| 93 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
else:
|
| 95 |
-
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 |
-
#
|
|
|
|
| 101 |
st.text_area(
|
| 102 |
-
|
| 103 |
-
value=analysis.get('text', '')[:
|
| 104 |
-
height=
|
| 105 |
-
disabled=True
|
|
|
|
| 106 |
)
|
| 107 |
|
| 108 |
-
# Mostrar gráfico si existe
|
| 109 |
if analysis.get('concept_graph'):
|
| 110 |
try:
|
| 111 |
# Manejar diferentes formatos de imagen
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
use_container_width=True
|
| 117 |
-
)
|
| 118 |
-
elif isinstance(analysis['concept_graph'], str):
|
| 119 |
# Decodificar si está en base64
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 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 |
-
|
|
|
|
| 155 |
try:
|
| 156 |
-
#
|
| 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 |
-
#
|
| 162 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
formatted_date = timestamp.strftime("%d/%m/%Y %H:%M:%S")
|
| 164 |
|
| 165 |
-
#
|
| 166 |
-
|
| 167 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
#
|
| 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 |
-
|
| 182 |
-
|
| 183 |
-
# Mostrar imagen
|
| 184 |
st.image(
|
| 185 |
image_bytes,
|
| 186 |
caption=t.get('concept_network', 'Red de Conceptos'),
|
| 187 |
-
|
| 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 (
|
| 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 |
-
#
|
| 229 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 -
|
| 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 |
-
|
| 263 |
-
|
| 264 |
-
|
| 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 |
-
|
| 274 |
-
|
| 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 |
-
|
| 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(
|
| 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 |
-
|
| 312 |
-
|
| 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 |
-
|
| 323 |
-
|
| 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'
|
| 356 |
st.info(t.get('no_concepts', 'No hay conceptos disponibles para comparar'))
|
| 357 |
return
|
| 358 |
|
| 359 |
-
#
|
| 360 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 361 |
try:
|
| 362 |
-
|
| 363 |
-
|
| 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 |
-
#
|
| 378 |
-
|
| 379 |
-
|
|
|
|
|
|
|
| 380 |
try:
|
| 381 |
-
|
| 382 |
-
|
| 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 |
-
|
|
|
|
| 433 |
try:
|
| 434 |
-
#
|
| 435 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 436 |
formatted_date = timestamp.strftime("%d/%m/%Y %H:%M:%S")
|
| 437 |
|
|
|
|
|
|
|
|
|
|
| 438 |
with st.expander(
|
| 439 |
-
f"{t.get('chat_date', '
|
| 440 |
expanded=False
|
| 441 |
):
|
| 442 |
if 'messages' in chat and chat['messages']:
|
| 443 |
-
# Mostrar
|
| 444 |
-
for message in chat['messages']:
|
| 445 |
role = message.get('role', 'unknown')
|
| 446 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 447 |
|
| 448 |
-
# Usar el componente de chat de Streamlit
|
| 449 |
with st.chat_message(role):
|
| 450 |
st.markdown(content)
|
| 451 |
|
| 452 |
-
#
|
| 453 |
-
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|